MenuManager
This commit is contained in:
parent
e9903d5dc0
commit
b2dc2251d8
17 changed files with 814 additions and 63 deletions
|
|
@ -2468,13 +2468,13 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
{
|
||||
CultureName = LanguageCodes.En,
|
||||
ListFormCode = ListFormCodes.Lists.Menu,
|
||||
Name = AppCodes.Menus,
|
||||
Title = AppCodes.Menus,
|
||||
Name = AppCodes.Menus.Menu,
|
||||
Title = AppCodes.Menus.Menu,
|
||||
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||
IsTenant = false,
|
||||
IsBranch = false,
|
||||
IsOrganizationUnit = false,
|
||||
Description = AppCodes.Menus,
|
||||
Description = AppCodes.Menus.Menu,
|
||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||
SelectCommand = SelectCommandByTableName("Menu"),
|
||||
KeyFieldName = "Id",
|
||||
|
|
@ -2509,11 +2509,11 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
}),
|
||||
PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
D = AppCodes.Menus + ".Delete",
|
||||
E = AppCodes.Menus + ".Export"
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
D = AppCodes.Menus.Menu + ".Delete",
|
||||
E = AppCodes.Menus.Menu + ".Export"
|
||||
}),
|
||||
DeleteCommand = $"UPDATE \"{DbTablePrefix}Menu\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
|
||||
DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||
|
|
@ -2615,9 +2615,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
IsDeleted = false,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2647,9 +2647,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
}),
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2684,9 +2684,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
}),
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2713,9 +2713,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
}),
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2740,9 +2740,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
AllowSearch = true,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2767,9 +2767,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
AllowSearch = true,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2801,9 +2801,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
}),
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2828,9 +2828,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
AllowSearch = true,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2862,9 +2862,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
}),
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2889,9 +2889,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
AllowSearch = true,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2915,9 +2915,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
IsDeleted = false,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
@ -2942,9 +2942,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
|||
AllowSearch = true,
|
||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||
{
|
||||
C = AppCodes.Menus + ".Create",
|
||||
R = AppCodes.Menus,
|
||||
U = AppCodes.Menus + ".Update",
|
||||
C = AppCodes.Menus.Menu + ".Create",
|
||||
R = AppCodes.Menus.Menu,
|
||||
U = AppCodes.Menus.Menu + ".Update",
|
||||
E = true,
|
||||
Deny = false
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -606,6 +606,12 @@
|
|||
"en": "Update",
|
||||
"tr": "Değiştir"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Manager",
|
||||
"en": "Manager",
|
||||
"tr": "Yönetici"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Cancel",
|
||||
|
|
@ -960,6 +966,18 @@
|
|||
"en": "Menu Management",
|
||||
"tr": "Menü Yönetimi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Menus.Menu",
|
||||
"en": "Menu List",
|
||||
"tr": "Menü Listesi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Menus.Manager",
|
||||
"en": "Menu Manager",
|
||||
"tr": "Menü Yöneticisi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Setting",
|
||||
|
|
@ -6285,9 +6303,29 @@
|
|||
"Code": "App.Menus",
|
||||
"DisplayName": "App.Menus",
|
||||
"Order": 5,
|
||||
"Url": null,
|
||||
"Icon": "FaSchlix",
|
||||
"RequiredPermissionName": null,
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
"ParentCode": "App.Menus",
|
||||
"Code": "App.Menus.Menu",
|
||||
"DisplayName": "App.Menus.Menu",
|
||||
"Order": 1,
|
||||
"Url": "/list/list-menu",
|
||||
"Icon": "FcMenu",
|
||||
"RequiredPermissionName": "App.Menus",
|
||||
"RequiredPermissionName": "App.Menus.Menu",
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
"ParentCode": "App.Menus",
|
||||
"Code": "App.Menus.Manager",
|
||||
"DisplayName": "App.Menus.Manager",
|
||||
"Order": 2,
|
||||
"Url": "/menumanager",
|
||||
"Icon": "FaRegListAlt",
|
||||
"RequiredPermissionName": "App.Menus.Manager",
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
|
|
@ -6392,7 +6430,7 @@
|
|||
},
|
||||
{
|
||||
"ParentCode": "App.Saas",
|
||||
"Code": "AApp.BlogManagement",
|
||||
"Code": "App.BlogManagement",
|
||||
"DisplayName": "App.BlogManagement",
|
||||
"Order": 10,
|
||||
"Url": "/admin/blogmanagement",
|
||||
|
|
@ -6857,9 +6895,9 @@
|
|||
},
|
||||
{
|
||||
"GroupName": "App.Menus",
|
||||
"Name": "App.Menus",
|
||||
"Name": "App.Menus.Menu",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Menus",
|
||||
"DisplayName": "App.Menus.Menu",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
|
@ -7609,36 +7647,44 @@
|
|||
},
|
||||
{
|
||||
"GroupName": "App.Menus",
|
||||
"Name": "App.Menus.Create",
|
||||
"ParentName": "App.Menus",
|
||||
"Name": "App.Menus.Menu.Create",
|
||||
"ParentName": "App.Menus.Menu",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Menus",
|
||||
"Name": "App.Menus.Delete",
|
||||
"ParentName": "App.Menus",
|
||||
"Name": "App.Menus.Menu.Delete",
|
||||
"ParentName": "App.Menus.Menu",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Menus",
|
||||
"Name": "App.Menus.Export",
|
||||
"ParentName": "App.Menus",
|
||||
"Name": "App.Menus.Menu.Export",
|
||||
"ParentName": "App.Menus.Menu",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Menus",
|
||||
"Name": "App.Menus.Update",
|
||||
"ParentName": "App.Menus",
|
||||
"Name": "App.Menus.Menu.Update",
|
||||
"ParentName": "App.Menus.Menu",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Menus",
|
||||
"Name": "App.Menus.Manager",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Menus.Manager",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Notifications.Notification",
|
||||
"Name": "App.Notifications.Notification.Create",
|
||||
|
|
|
|||
|
|
@ -323,7 +323,13 @@ public static class SeedConsts
|
|||
public const string Language = Default + ".Language";
|
||||
public const string LanguageText = Default + ".LanguageText";
|
||||
}
|
||||
public const string Menus = Prefix.App + ".Menus";
|
||||
public static class Menus
|
||||
{
|
||||
public const string Default = Prefix.App + ".Menus";
|
||||
|
||||
public const string Menu = Default + ".Menu";
|
||||
public const string Manager = Default + ".Manager";
|
||||
}
|
||||
public static class Listforms
|
||||
{
|
||||
public const string Default = Prefix.App + ".Listforms";
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
|||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.e088p05bgmo"
|
||||
"revision": "0.scg6n9r90k8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
|
|
|||
56
ui/package-lock.json
generated
56
ui/package-lock.json
generated
|
|
@ -8,6 +8,9 @@
|
|||
"name": "kurs-platform-ui",
|
||||
"version": "1.0.4",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@floating-ui/react": "^0.27.2",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.8",
|
||||
|
|
@ -1653,6 +1656,59 @@
|
|||
"inferno-hydrate": "^7.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
|
||||
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
"format": "npm run prettier:fix && npm run lint:fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@floating-ui/react": "^0.27.2",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.8",
|
||||
|
|
|
|||
31
ui/src/@types/menu.ts
Normal file
31
ui/src/@types/menu.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { MenuDto } from "@/proxy/menus";
|
||||
|
||||
export interface MenuItem extends MenuDto {
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
export interface MenuApiResponse {
|
||||
totalCount: number;
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
export interface DragEndEvent {
|
||||
active: {
|
||||
id: string;
|
||||
data: {
|
||||
current: {
|
||||
type: string;
|
||||
item: MenuItem;
|
||||
};
|
||||
};
|
||||
};
|
||||
over: {
|
||||
id: string;
|
||||
data: {
|
||||
current: {
|
||||
type: string;
|
||||
item?: MenuItem;
|
||||
};
|
||||
};
|
||||
} | null;
|
||||
}
|
||||
|
|
@ -75,4 +75,10 @@ export const protectedRoutes: Routes = [
|
|||
component: lazy(() => import('@/views/docs/ChangeLog')),
|
||||
authority: [],
|
||||
},
|
||||
{
|
||||
key: ROUTES_ENUM.menumanager,
|
||||
path: ROUTES_ENUM.menumanager,
|
||||
component: lazy(() => import('@/views/menu/MenuManager')),
|
||||
authority: [],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -53,4 +53,5 @@ export const ROUTES_ENUM = {
|
|||
docs: {
|
||||
changelog: '/docs/changelog'
|
||||
},
|
||||
menumanager: '/menumanager',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from './menu.service'
|
||||
export * from './models'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy/abp'
|
||||
import { MenuDto } from '@/proxy/menus/models'
|
||||
import apiService, { Config } from '@/services/api.service'
|
||||
import type { MenuDto } from './models'
|
||||
import { PagedAndSortedResultRequestDto, PagedResultDto } from '../abp'
|
||||
|
||||
export class MenuService {
|
||||
apiName = 'Default'
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { refreshToken } from '@/proxy/account/auth.service'
|
||||
import { MenuService } from '@/proxy/menus'
|
||||
import { createStore, createTypedHooks, persist } from 'easy-peasy'
|
||||
import {
|
||||
Config as ReduxStateSyncConfig,
|
||||
|
|
@ -14,6 +13,7 @@ import { AuthModel, authModel } from './auth.model'
|
|||
import { BaseModel, baseModel } from './base.model'
|
||||
import { LocaleModel, localeModel } from './locale.model'
|
||||
import { ThemeModel, themeModel } from './theme.model'
|
||||
import { MenuService } from '@/services/menu.service'
|
||||
|
||||
export interface StoreModel {
|
||||
abpConfig: AbpConfigModel
|
||||
|
|
|
|||
116
ui/src/utils/hooks/useMenuData.ts
Normal file
116
ui/src/utils/hooks/useMenuData.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { MenuApiResponse, MenuItem } from '@/@types/menu'
|
||||
import { getMenus } from '@/services/menu.service'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const useMenuData = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const buildHierarchy = (items: MenuItem[]): MenuItem[] => {
|
||||
const itemMap = new Map<string, MenuItem>()
|
||||
const rootItems: MenuItem[] = []
|
||||
|
||||
// Create a map for quick lookup and initialize children arrays
|
||||
items.forEach((item) => {
|
||||
itemMap.set(item.code!!, { ...item, children: [] })
|
||||
})
|
||||
|
||||
// Build the hierarchy
|
||||
items.forEach((item) => {
|
||||
const menuItem = itemMap.get(item.code!!)!
|
||||
|
||||
if (item.parentCode && itemMap.has(item.parentCode)) {
|
||||
const parent = itemMap.get(item.parentCode)!
|
||||
if (!parent.children) {
|
||||
parent.children = []
|
||||
}
|
||||
parent.children.push(menuItem)
|
||||
} else {
|
||||
rootItems.push(menuItem)
|
||||
}
|
||||
})
|
||||
|
||||
// Sort items by order recursively
|
||||
const sortItems = (items: MenuItem[]): MenuItem[] => {
|
||||
return items
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children && item.children.length > 0 ? sortItems(item.children) : [],
|
||||
}))
|
||||
}
|
||||
|
||||
return sortItems(rootItems)
|
||||
}
|
||||
|
||||
const fetchMenuData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// Simulate API call with mock data
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const response = await getMenus()
|
||||
|
||||
if (response.data) {
|
||||
const hierarchicalMenu = buildHierarchy(response.data.items)
|
||||
setMenuItems(hierarchicalMenu)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load menu data')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveMenuData = async (updatedMenuItems: MenuItem[]) => {
|
||||
try {
|
||||
// Flatten the hierarchy for API
|
||||
const flatten = (items: MenuItem[], parentCode: string | null = null): MenuItem[] => {
|
||||
const result: MenuItem[] = []
|
||||
items.forEach((item, index) => {
|
||||
const flatItem = {
|
||||
...item,
|
||||
parentCode,
|
||||
order: index + 1,
|
||||
children: undefined,
|
||||
}
|
||||
result.push(flatItem)
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
result.push(...flatten(item.children, item.code))
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const flatMenuItems = flatten(updatedMenuItems)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
console.log('Saving menu data:', flatMenuItems)
|
||||
// In real implementation: await api.saveMenus(flatMenuItems);
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to save menu data')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchMenuData()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
menuItems,
|
||||
setMenuItems,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchMenuData,
|
||||
saveMenuData,
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import { postListFormWizard } from '@/proxy/admin/list-form/list-form.service'
|
|||
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||
import { getDataSources } from '@/proxy/data-source'
|
||||
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form'
|
||||
import { getMenus } from '@/proxy/menus'
|
||||
import { SelectBoxOption } from '@/shared/types'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||
|
|
@ -24,6 +23,7 @@ import { useNavigate } from 'react-router-dom'
|
|||
import CreatableSelect from 'react-select/creatable'
|
||||
import * as Yup from 'yup'
|
||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options'
|
||||
import { getMenus } from '@/services/menu.service'
|
||||
|
||||
const initialValues: ListFormWizardDto = {
|
||||
listFormCode: '',
|
||||
|
|
|
|||
100
ui/src/views/menu/MenuItemComponent.tsx
Normal file
100
ui/src/views/menu/MenuItemComponent.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import React from 'react'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { MenuItem } from '@/@types/menu'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
interface MenuItemComponentProps {
|
||||
item: MenuItem
|
||||
isDesignMode: boolean
|
||||
depth: number
|
||||
children?: React.ReactNode
|
||||
isDragOverlay?: boolean
|
||||
}
|
||||
|
||||
export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||
item,
|
||||
isDesignMode,
|
||||
depth,
|
||||
children,
|
||||
isDragOverlay = false,
|
||||
}) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: item.id || '',
|
||||
data: {
|
||||
type: 'menu-item',
|
||||
item,
|
||||
},
|
||||
disabled: !isDesignMode,
|
||||
})
|
||||
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
const getIcon = (iconName: string) => {
|
||||
const IconComponent = (Icons as any)[iconName]
|
||||
return IconComponent ? <IconComponent size={16} /> : <Icons.FileText size={16} />
|
||||
}
|
||||
|
||||
const [isExpanded, setIsExpanded] = React.useState(true)
|
||||
|
||||
const toggleExpanded = () => {
|
||||
if (!isDesignMode) {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="select-none">
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`
|
||||
flex items-center gap-1 p-1 rounded-lg transition-all duration-200 group min-h-[30px]
|
||||
${isDesignMode ? 'cursor-move hover:bg-blue-50 border border-transparent hover:border-blue-200' : 'cursor-pointer hover:bg-gray-50'}
|
||||
${isDragOverlay ? 'shadow-lg bg-white border border-blue-300 z-50' : ''}
|
||||
${isDragging ? 'opacity-50' : ''}
|
||||
${item.children && item.children.length > 0 ? 'bg-blue-50' : depth === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||
`}
|
||||
{...(isDesignMode ? { ...attributes, ...listeners } : { onClick: toggleExpanded })}
|
||||
>
|
||||
<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>
|
||||
|
||||
<span
|
||||
className={`
|
||||
truncate text-gray-800 leading-6 text-sm
|
||||
${isDesignMode ? 'font-normal' : item.children && item.children.length > 0 ? 'font-semibold' : 'font-normal'}
|
||||
`}
|
||||
>
|
||||
{translate('::' + item.displayName)}
|
||||
</span>
|
||||
|
||||
{item.url && <Icons.ExternalLink size={12} className="flex-shrink-0 text-gray-400" />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{isDesignMode && (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span className="bg-gray-200 px-2 py-1 rounded">#{item.order}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.children && item.children.length > 0 && (
|
||||
<span className="text-xs text-gray-500 bg-blue-100 px-2 py-1 rounded-full">
|
||||
{item.children.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children && (!isDesignMode ? isExpanded : true) && <div className="mt-1">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
160
ui/src/views/menu/MenuManager.tsx
Normal file
160
ui/src/views/menu/MenuManager.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import React, { useState } from 'react'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { SortableMenuTree } from './SortableMenuTree'
|
||||
import { MenuItem } from '@/@types/menu'
|
||||
import { useMenuData } from '@/utils/hooks/useMenuData'
|
||||
|
||||
export const MenuManager = () => {
|
||||
const { menuItems, setMenuItems, loading, error, refetch, saveMenuData } = useMenuData()
|
||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [saveMessage, setSaveMessage] = useState<{
|
||||
type: 'success' | 'error'
|
||||
text: string
|
||||
} | null>(null)
|
||||
|
||||
const handleMenuChange = (updatedItems: MenuItem[]) => {
|
||||
setMenuItems(updatedItems)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isDesignMode) return
|
||||
|
||||
try {
|
||||
setIsSaving(true)
|
||||
setSaveMessage(null)
|
||||
|
||||
await saveMenuData(menuItems)
|
||||
|
||||
setSaveMessage({ type: 'success', text: 'Menu configuration saved successfully!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to save menu configuration',
|
||||
})
|
||||
setTimeout(() => setSaveMessage(null), 5000)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleDesignMode = () => {
|
||||
setIsDesignMode(!isDesignMode)
|
||||
setSaveMessage(null)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="flex items-center gap-3 text-gray-600">
|
||||
<Icons.Loader2 size={24} className="animate-spin" />
|
||||
<span className="text-lg">Loading menu configuration...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full mx-4">
|
||||
<div className="flex items-center gap-3 text-red-600 mb-4">
|
||||
<Icons.AlertCircle size={24} />
|
||||
<h2 className="text-lg font-semibold">Error Loading Menu</h2>
|
||||
</div>
|
||||
<p className="text-gray-600 mb-6">{error}</p>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="w-full h-full p-6">
|
||||
{/* Menu Tree */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
||||
{/* Sol kısım: Başlık */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.Menu size={20} className="text-gray-600" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Menu Manager</h2>
|
||||
<span className="text-sm text-gray-500">({menuItems.length} root items)</span>
|
||||
</div>
|
||||
|
||||
{/* Sağ kısım: Design Mode + Save butonu */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`text-sm font-medium ${isDesignMode ? 'text-blue-600' : 'text-gray-500'}`}
|
||||
>
|
||||
Design Mode
|
||||
</span>
|
||||
<button
|
||||
onClick={handleToggleDesignMode}
|
||||
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
|
||||
${isDesignMode ? 'bg-blue-600' : 'bg-gray-200'}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
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'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!isDesignMode || isSaving}
|
||||
className={`
|
||||
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'}
|
||||
${isSaving ? 'opacity-50' : ''}
|
||||
`}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Icons.Loader2 size={16} className="animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icons.Save size={16} />
|
||||
Save Changes
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuManager
|
||||
227
ui/src/views/menu/SortableMenuTree.tsx
Normal file
227
ui/src/views/menu/SortableMenuTree.tsx
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragStartEvent,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { MenuItemComponent } from './MenuItemComponent'
|
||||
import { MenuItem } from '@/@types/menu'
|
||||
|
||||
interface SortableMenuTreeProps {
|
||||
items: MenuItem[]
|
||||
onItemsChange: (items: MenuItem[]) => void
|
||||
isDesignMode: boolean
|
||||
}
|
||||
|
||||
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||
items,
|
||||
onItemsChange,
|
||||
isDesignMode,
|
||||
}) => {
|
||||
const [activeItem, setActiveItem] = React.useState<MenuItem | null>(null)
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
|
||||
// Flatten the tree structure to get all items with their paths
|
||||
const flattenItems = (
|
||||
items: MenuItem[],
|
||||
parentPath: string[] = [],
|
||||
): Array<{ item: MenuItem; path: number[] }> => {
|
||||
const result: Array<{ item: MenuItem; path: number[] }> = []
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const currentPath = [...parentPath, index]
|
||||
result.push({ item, path: currentPath })
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
result.push(...flattenItems(item.children, currentPath))
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Find item by ID in the tree
|
||||
const findItemById = (items: MenuItem[], id: string): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.id === id) {
|
||||
return item
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findItemById(item.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Remove item from tree by ID
|
||||
const removeItemFromTree = (items: MenuItem[], id: string): MenuItem[] => {
|
||||
return items.reduce((acc: MenuItem[], item) => {
|
||||
if (item.id === id) {
|
||||
return acc // Skip this item (remove it)
|
||||
}
|
||||
|
||||
const newItem = { ...item }
|
||||
if (newItem.children && newItem.children.length > 0) {
|
||||
newItem.children = removeItemFromTree(newItem.children, id)
|
||||
}
|
||||
|
||||
acc.push(newItem)
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
// Insert item at specific position
|
||||
const insertItemAtPath = (
|
||||
items: MenuItem[],
|
||||
item: MenuItem,
|
||||
targetPath: number[],
|
||||
): MenuItem[] => {
|
||||
if (targetPath.length === 1) {
|
||||
const newItems = [...items]
|
||||
newItems.splice(targetPath[0], 0, item)
|
||||
return newItems
|
||||
}
|
||||
|
||||
const [firstIndex, ...restPath] = targetPath
|
||||
const newItems = [...items]
|
||||
|
||||
if (newItems[firstIndex]) {
|
||||
newItems[firstIndex] = {
|
||||
...newItems[firstIndex],
|
||||
children: insertItemAtPath(newItems[firstIndex].children || [], item, restPath),
|
||||
}
|
||||
}
|
||||
|
||||
return newItems
|
||||
}
|
||||
|
||||
// Get the path where an item should be inserted based on over item
|
||||
const getInsertionPath = (
|
||||
items: MenuItem[],
|
||||
activeId: string,
|
||||
overId: string,
|
||||
): number[] | null => {
|
||||
const flatItems = flattenItems(items)
|
||||
const activeIndex = flatItems.findIndex(({ item }) => item.id === activeId)
|
||||
const overIndex = flatItems.findIndex(({ item }) => item.id === overId)
|
||||
|
||||
if (overIndex === -1) return null
|
||||
|
||||
const overItem = flatItems[overIndex]
|
||||
const insertPath = [...overItem.path]
|
||||
|
||||
// Aktif item, listedeki over item'den sonra geliyorsa, yukarı taşınıyordur → over item'ın yerine ekle
|
||||
// Aktif item, listedeki over item'den önceyse, aşağı taşınıyordur → bir SONRASINA ekle ki yeniden aynı yere düşmesin
|
||||
if (activeIndex < overIndex) {
|
||||
insertPath[insertPath.length - 1] += 1
|
||||
}
|
||||
|
||||
return insertPath
|
||||
}
|
||||
|
||||
const updateOrderNumbers = (items: MenuItem[]): MenuItem[] => {
|
||||
return items.map((item, index) => ({
|
||||
...item,
|
||||
order: index + 1,
|
||||
children: item.children ? updateOrderNumbers(item.children) : [],
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event
|
||||
const activeItem = findItemById(items, active.id as string)
|
||||
setActiveItem(activeItem)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveItem(null)
|
||||
|
||||
if (!over || active.id === over.id || !isDesignMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const activeId = active.id as string
|
||||
const overId = over.id as string
|
||||
|
||||
const activeItem = findItemById(items, activeId)
|
||||
if (!activeItem) return
|
||||
|
||||
// ⛳️ Kullanılması gereken liste: `items`
|
||||
const insertionPath = getInsertionPath(items, activeId, overId)
|
||||
if (!insertionPath) return
|
||||
|
||||
// Şimdi aktif elemanı çıkar
|
||||
let newItems = removeItemFromTree(items, activeId)
|
||||
|
||||
// ve hedef konuma ekle
|
||||
newItems = insertItemAtPath(newItems, activeItem, insertionPath)
|
||||
|
||||
// Sıra numaralarını güncelle
|
||||
const finalItems = updateOrderNumbers(newItems)
|
||||
onItemsChange(finalItems)
|
||||
}
|
||||
|
||||
const renderMenuItem = (item: MenuItem, depth: number = 0): React.ReactNode => {
|
||||
return (
|
||||
<MenuItemComponent key={item.id} item={item} isDesignMode={isDesignMode} depth={depth}>
|
||||
{item.children && item.children.length > 0 && (
|
||||
<div className="ml-4">
|
||||
{item.children.map((child) => renderMenuItem(child, depth + 1))}
|
||||
</div>
|
||||
)}
|
||||
</MenuItemComponent>
|
||||
)
|
||||
}
|
||||
|
||||
const allItems = flattenItems(items)
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={allItems.map(({ item }) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-1">{items.map((item) => renderMenuItem(item))}</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeItem ? (
|
||||
<MenuItemComponent
|
||||
item={activeItem}
|
||||
isDesignMode={isDesignMode}
|
||||
depth={0}
|
||||
isDragOverlay={true}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue