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,
|
CultureName = LanguageCodes.En,
|
||||||
ListFormCode = ListFormCodes.Lists.Menu,
|
ListFormCode = ListFormCodes.Lists.Menu,
|
||||||
Name = AppCodes.Menus,
|
Name = AppCodes.Menus.Menu,
|
||||||
Title = AppCodes.Menus,
|
Title = AppCodes.Menus.Menu,
|
||||||
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||||
IsTenant = false,
|
IsTenant = false,
|
||||||
IsBranch = false,
|
IsBranch = false,
|
||||||
IsOrganizationUnit = false,
|
IsOrganizationUnit = false,
|
||||||
Description = AppCodes.Menus,
|
Description = AppCodes.Menus.Menu,
|
||||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
SelectCommand = SelectCommandByTableName("Menu"),
|
SelectCommand = SelectCommandByTableName("Menu"),
|
||||||
KeyFieldName = "Id",
|
KeyFieldName = "Id",
|
||||||
|
|
@ -2509,11 +2509,11 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}),
|
}),
|
||||||
PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto
|
PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
D = AppCodes.Menus + ".Delete",
|
D = AppCodes.Menus.Menu + ".Delete",
|
||||||
E = AppCodes.Menus + ".Export"
|
E = AppCodes.Menus.Menu + ".Export"
|
||||||
}),
|
}),
|
||||||
DeleteCommand = $"UPDATE \"{DbTablePrefix}Menu\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
|
DeleteCommand = $"UPDATE \"{DbTablePrefix}Menu\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
|
||||||
DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||||
|
|
@ -2615,9 +2615,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
IsDeleted = false,
|
IsDeleted = false,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2647,9 +2647,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}),
|
}),
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2684,9 +2684,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}),
|
}),
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2713,9 +2713,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}),
|
}),
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2740,9 +2740,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2767,9 +2767,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2801,9 +2801,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}),
|
}),
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2828,9 +2828,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2862,9 +2862,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}),
|
}),
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2889,9 +2889,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2915,9 +2915,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
IsDeleted = false,
|
IsDeleted = false,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
@ -2942,9 +2942,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
|
||||||
{
|
{
|
||||||
C = AppCodes.Menus + ".Create",
|
C = AppCodes.Menus.Menu + ".Create",
|
||||||
R = AppCodes.Menus,
|
R = AppCodes.Menus.Menu,
|
||||||
U = AppCodes.Menus + ".Update",
|
U = AppCodes.Menus.Menu + ".Update",
|
||||||
E = true,
|
E = true,
|
||||||
Deny = false
|
Deny = false
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -606,6 +606,12 @@
|
||||||
"en": "Update",
|
"en": "Update",
|
||||||
"tr": "Değiştir"
|
"tr": "Değiştir"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "Manager",
|
||||||
|
"en": "Manager",
|
||||||
|
"tr": "Yönetici"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "Cancel",
|
"key": "Cancel",
|
||||||
|
|
@ -960,6 +966,18 @@
|
||||||
"en": "Menu Management",
|
"en": "Menu Management",
|
||||||
"tr": "Menü Yönetimi"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Setting",
|
"key": "App.Setting",
|
||||||
|
|
@ -6285,9 +6303,29 @@
|
||||||
"Code": "App.Menus",
|
"Code": "App.Menus",
|
||||||
"DisplayName": "App.Menus",
|
"DisplayName": "App.Menus",
|
||||||
"Order": 5,
|
"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",
|
"Url": "/list/list-menu",
|
||||||
"Icon": "FcMenu",
|
"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
|
"IsDisabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -6392,7 +6430,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ParentCode": "App.Saas",
|
"ParentCode": "App.Saas",
|
||||||
"Code": "AApp.BlogManagement",
|
"Code": "App.BlogManagement",
|
||||||
"DisplayName": "App.BlogManagement",
|
"DisplayName": "App.BlogManagement",
|
||||||
"Order": 10,
|
"Order": 10,
|
||||||
"Url": "/admin/blogmanagement",
|
"Url": "/admin/blogmanagement",
|
||||||
|
|
@ -6857,9 +6895,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Menus",
|
"GroupName": "App.Menus",
|
||||||
"Name": "App.Menus",
|
"Name": "App.Menus.Menu",
|
||||||
"ParentName": null,
|
"ParentName": null,
|
||||||
"DisplayName": "App.Menus",
|
"DisplayName": "App.Menus.Menu",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2
|
"MultiTenancySide": 2
|
||||||
},
|
},
|
||||||
|
|
@ -7609,36 +7647,44 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Menus",
|
"GroupName": "App.Menus",
|
||||||
"Name": "App.Menus.Create",
|
"Name": "App.Menus.Menu.Create",
|
||||||
"ParentName": "App.Menus",
|
"ParentName": "App.Menus.Menu",
|
||||||
"DisplayName": "Create",
|
"DisplayName": "Create",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2
|
"MultiTenancySide": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Menus",
|
"GroupName": "App.Menus",
|
||||||
"Name": "App.Menus.Delete",
|
"Name": "App.Menus.Menu.Delete",
|
||||||
"ParentName": "App.Menus",
|
"ParentName": "App.Menus.Menu",
|
||||||
"DisplayName": "Delete",
|
"DisplayName": "Delete",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2
|
"MultiTenancySide": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Menus",
|
"GroupName": "App.Menus",
|
||||||
"Name": "App.Menus.Export",
|
"Name": "App.Menus.Menu.Export",
|
||||||
"ParentName": "App.Menus",
|
"ParentName": "App.Menus.Menu",
|
||||||
"DisplayName": "Export",
|
"DisplayName": "Export",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2
|
"MultiTenancySide": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Menus",
|
"GroupName": "App.Menus",
|
||||||
"Name": "App.Menus.Update",
|
"Name": "App.Menus.Menu.Update",
|
||||||
"ParentName": "App.Menus",
|
"ParentName": "App.Menus.Menu",
|
||||||
"DisplayName": "Update",
|
"DisplayName": "Update",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2
|
"MultiTenancySide": 2
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Menus",
|
||||||
|
"Name": "App.Menus.Manager",
|
||||||
|
"ParentName": null,
|
||||||
|
"DisplayName": "App.Menus.Manager",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Notifications.Notification",
|
"GroupName": "App.Notifications.Notification",
|
||||||
"Name": "App.Notifications.Notification.Create",
|
"Name": "App.Notifications.Notification.Create",
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,13 @@ public static class SeedConsts
|
||||||
public const string Language = Default + ".Language";
|
public const string Language = Default + ".Language";
|
||||||
public const string LanguageText = Default + ".LanguageText";
|
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 static class Listforms
|
||||||
{
|
{
|
||||||
public const string Default = Prefix.App + ".Listforms";
|
public const string Default = Prefix.App + ".Listforms";
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.e088p05bgmo"
|
"revision": "0.scg6n9r90k8"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
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",
|
"name": "kurs-platform-ui",
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"dependencies": {
|
"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",
|
"@floating-ui/react": "^0.27.2",
|
||||||
"@fullcalendar/daygrid": "^6.1.8",
|
"@fullcalendar/daygrid": "^6.1.8",
|
||||||
"@fullcalendar/interaction": "^6.1.8",
|
"@fullcalendar/interaction": "^6.1.8",
|
||||||
|
|
@ -1653,6 +1656,59 @@
|
||||||
"inferno-hydrate": "^7.4.6"
|
"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": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.11.0",
|
"version": "11.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
"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"
|
"format": "npm run prettier:fix && npm run lint:fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@floating-ui/react": "^0.27.2",
|
||||||
"@fullcalendar/daygrid": "^6.1.8",
|
"@fullcalendar/daygrid": "^6.1.8",
|
||||||
"@fullcalendar/interaction": "^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')),
|
component: lazy(() => import('@/views/docs/ChangeLog')),
|
||||||
authority: [],
|
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: {
|
docs: {
|
||||||
changelog: '/docs/changelog'
|
changelog: '/docs/changelog'
|
||||||
},
|
},
|
||||||
|
menumanager: '/menumanager',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export * from './menu.service'
|
|
||||||
export * from './models'
|
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 apiService, { Config } from '@/services/api.service'
|
||||||
import type { MenuDto } from './models'
|
|
||||||
import { PagedAndSortedResultRequestDto, PagedResultDto } from '../abp'
|
|
||||||
|
|
||||||
export class MenuService {
|
export class MenuService {
|
||||||
apiName = 'Default'
|
apiName = 'Default'
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { refreshToken } from '@/proxy/account/auth.service'
|
import { refreshToken } from '@/proxy/account/auth.service'
|
||||||
import { MenuService } from '@/proxy/menus'
|
|
||||||
import { createStore, createTypedHooks, persist } from 'easy-peasy'
|
import { createStore, createTypedHooks, persist } from 'easy-peasy'
|
||||||
import {
|
import {
|
||||||
Config as ReduxStateSyncConfig,
|
Config as ReduxStateSyncConfig,
|
||||||
|
|
@ -14,6 +13,7 @@ import { AuthModel, authModel } from './auth.model'
|
||||||
import { BaseModel, baseModel } from './base.model'
|
import { BaseModel, baseModel } from './base.model'
|
||||||
import { LocaleModel, localeModel } from './locale.model'
|
import { LocaleModel, localeModel } from './locale.model'
|
||||||
import { ThemeModel, themeModel } from './theme.model'
|
import { ThemeModel, themeModel } from './theme.model'
|
||||||
|
import { MenuService } from '@/services/menu.service'
|
||||||
|
|
||||||
export interface StoreModel {
|
export interface StoreModel {
|
||||||
abpConfig: AbpConfigModel
|
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 { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||||
import { getDataSources } from '@/proxy/data-source'
|
import { getDataSources } from '@/proxy/data-source'
|
||||||
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form'
|
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form'
|
||||||
import { getMenus } from '@/proxy/menus'
|
|
||||||
import { SelectBoxOption } from '@/shared/types'
|
import { SelectBoxOption } from '@/shared/types'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||||
|
|
@ -24,6 +23,7 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import CreatableSelect from 'react-select/creatable'
|
import CreatableSelect from 'react-select/creatable'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options'
|
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options'
|
||||||
|
import { getMenus } from '@/services/menu.service'
|
||||||
|
|
||||||
const initialValues: ListFormWizardDto = {
|
const initialValues: ListFormWizardDto = {
|
||||||
listFormCode: '',
|
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