@vc-shell/framework 1.1.0-alpha.5 → 1.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/core/composables/useGlobalSearch/index.ts +4 -4
  2. package/core/composables/useMenuService/index.ts +20 -110
  3. package/core/composables/useWidgets/index.ts +2 -1
  4. package/core/constants/index.ts +2 -0
  5. package/core/plugins/modularity/index.ts +4 -4
  6. package/core/services/menu-service.ts +459 -0
  7. package/core/services/widget-service.ts +20 -0
  8. package/core/types/index.ts +14 -1
  9. package/dist/core/composables/useGlobalSearch/index.d.ts +2 -2
  10. package/dist/core/composables/useMenuService/index.d.ts +4 -10
  11. package/dist/core/composables/useMenuService/index.d.ts.map +1 -1
  12. package/dist/core/composables/useWidgets/index.d.ts +2 -1
  13. package/dist/core/composables/useWidgets/index.d.ts.map +1 -1
  14. package/dist/core/constants/index.d.ts +1 -0
  15. package/dist/core/constants/index.d.ts.map +1 -1
  16. package/dist/core/plugins/modularity/index.d.ts.map +1 -1
  17. package/dist/core/services/menu-service.d.ts +50 -0
  18. package/dist/core/services/menu-service.d.ts.map +1 -0
  19. package/dist/core/services/widget-service.d.ts +4 -0
  20. package/dist/core/services/widget-service.d.ts.map +1 -1
  21. package/dist/core/types/index.d.ts +14 -1
  22. package/dist/core/types/index.d.ts.map +1 -1
  23. package/dist/framework.js +227 -216
  24. package/dist/{index-DftzTUkM.js → index-4fNoXD3u.js} +1 -1
  25. package/dist/{index-NzMAKJjd.js → index-7YHBATKO.js} +1 -1
  26. package/dist/{index-CVRw68l1.js → index-7gQRbSrG.js} +1 -1
  27. package/dist/{index-CabhPQ7N.js → index-B1NfXGpu.js} +1 -1
  28. package/dist/{index-CebrwV8l.js → index-B42ra4oQ.js} +26625 -26111
  29. package/dist/{index-Bvk6Bxh6.js → index-Bixm_Atu.js} +1 -1
  30. package/dist/{index-BzlggnM4.js → index-C9NLyptv.js} +1 -1
  31. package/dist/{index-T0rTYtsD.js → index-CPjPhNQr.js} +1 -1
  32. package/dist/{index-ClLnTkeM.js → index-CR62_U0-.js} +1 -1
  33. package/dist/{index-z-TY6ELw.js → index-DHUS6fMi.js} +1 -1
  34. package/dist/{index-BRw_Gtzh.js → index-DKMXMXmO.js} +1 -1
  35. package/dist/{index-XpeCWx3L.js → index-DxhPupsj.js} +1 -1
  36. package/dist/{index-BTjwjD_q.js → index-DyDl-e3b.js} +1 -1
  37. package/dist/{index-Dho76GDx.js → index-IdAZC2W6.js} +1 -1
  38. package/dist/{index-4hH_NTkO.js → index-QiYxGOfR.js} +1 -1
  39. package/dist/{index-BqAfhE0O.js → index-qjW_Kc_I.js} +1 -1
  40. package/dist/{index-CPfPePmU.js → index-tHx2asQS.js} +1 -1
  41. package/dist/index.css +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/injection-keys.d.ts +2 -0
  44. package/dist/injection-keys.d.ts.map +1 -1
  45. package/dist/shared/components/blade-navigation/components/vc-blade-view/vc-blade-view.d.ts.map +1 -1
  46. package/dist/shared/components/draggable-dashboard/DraggableDashboard.vue.d.ts +2 -0
  47. package/dist/shared/components/draggable-dashboard/DraggableDashboard.vue.d.ts.map +1 -1
  48. package/dist/shared/components/draggable-dashboard/composables/useCellSizeCalculator.d.ts +25 -0
  49. package/dist/shared/components/draggable-dashboard/composables/useCellSizeCalculator.d.ts.map +1 -0
  50. package/dist/shared/components/draggable-dashboard/composables/useCollisionDetection.d.ts +27 -0
  51. package/dist/shared/components/draggable-dashboard/composables/useCollisionDetection.d.ts.map +1 -0
  52. package/dist/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.d.ts +22 -0
  53. package/dist/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.d.ts.map +1 -1
  54. package/dist/shared/components/draggable-dashboard/composables/useDashboardGrid.d.ts +12 -4
  55. package/dist/shared/components/draggable-dashboard/composables/useDashboardGrid.d.ts.map +1 -1
  56. package/dist/shared/components/draggable-dashboard/composables/useDragClone.d.ts +15 -0
  57. package/dist/shared/components/draggable-dashboard/composables/useDragClone.d.ts.map +1 -0
  58. package/dist/shared/components/draggable-dashboard/composables/useEventCoordinates.d.ts +33 -0
  59. package/dist/shared/components/draggable-dashboard/composables/useEventCoordinates.d.ts.map +1 -0
  60. package/dist/shared/components/draggable-dashboard/composables/useGridPosition.d.ts +57 -0
  61. package/dist/shared/components/draggable-dashboard/composables/useGridPosition.d.ts.map +1 -0
  62. package/dist/shared/components/draggable-dashboard/composables/useGridSystem.d.ts +22 -0
  63. package/dist/shared/components/draggable-dashboard/composables/useGridSystem.d.ts.map +1 -0
  64. package/dist/shared/components/draggable-dashboard/composables/useLayoutPersistence.d.ts +19 -0
  65. package/dist/shared/components/draggable-dashboard/composables/useLayoutPersistence.d.ts.map +1 -0
  66. package/dist/shared/components/draggable-dashboard/composables/useResizeObserver.d.ts +18 -0
  67. package/dist/shared/components/draggable-dashboard/composables/useResizeObserver.d.ts.map +1 -0
  68. package/dist/shared/components/draggable-dashboard/composables/useWidgetLayout.d.ts +14 -0
  69. package/dist/shared/components/draggable-dashboard/composables/useWidgetLayout.d.ts.map +1 -0
  70. package/dist/shared/components/draggable-dashboard/composables/useWidgetStyles.d.ts +21 -0
  71. package/dist/shared/components/draggable-dashboard/composables/useWidgetStyles.d.ts.map +1 -0
  72. package/dist/shared/components/draggable-dashboard/types.d.ts +5 -1
  73. package/dist/shared/components/draggable-dashboard/types.d.ts.map +1 -1
  74. package/dist/shared/components/notification-dropdown/notification-dropdown.vue.d.ts.map +1 -1
  75. package/dist/shared/components/sidebar/sidebar.vue.d.ts.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/dist/ui/components/atoms/vc-badge/vc-badge.vue.d.ts.map +1 -1
  78. package/dist/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue.d.ts +18 -0
  79. package/dist/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue.d.ts.map +1 -0
  80. package/dist/ui/components/atoms/vc-icon/icons/OffersIcon.vue.d.ts +18 -0
  81. package/dist/ui/components/atoms/vc-icon/icons/OffersIcon.vue.d.ts.map +1 -0
  82. package/dist/ui/components/atoms/vc-icon/icons/OrdersIcon.vue.d.ts +18 -0
  83. package/dist/ui/components/atoms/vc-icon/icons/OrdersIcon.vue.d.ts.map +1 -0
  84. package/dist/ui/components/atoms/vc-icon/icons/PeopleIcon.vue.d.ts +18 -0
  85. package/dist/ui/components/atoms/vc-icon/icons/PeopleIcon.vue.d.ts.map +1 -0
  86. package/dist/ui/components/atoms/vc-icon/icons/ProductsIcon.vue.d.ts +18 -0
  87. package/dist/ui/components/atoms/vc-icon/icons/ProductsIcon.vue.d.ts.map +1 -0
  88. package/dist/ui/components/atoms/vc-icon/icons/ProfileIcon.vue.d.ts +18 -0
  89. package/dist/ui/components/atoms/vc-icon/icons/ProfileIcon.vue.d.ts.map +1 -0
  90. package/dist/ui/components/atoms/vc-icon/icons/index.d.ts +6 -0
  91. package/dist/ui/components/atoms/vc-icon/icons/index.d.ts.map +1 -1
  92. package/dist/ui/components/atoms/vc-icon/vc-icon.vue.d.ts.map +1 -1
  93. package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/_internal/AppBarHeader.vue.d.ts.map +1 -1
  94. package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/_internal/AppBarOverlay.vue.d.ts.map +1 -1
  95. package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/vc-app-bar.vue.d.ts.map +1 -1
  96. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts +2 -1
  97. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts.map +1 -1
  98. package/dist/ui/components/organisms/vc-app/vc-app.vue.d.ts.map +1 -1
  99. package/dist/ui/components/organisms/vc-blade/_internal/vc-blade-header/vc-blade-header.vue.d.ts.map +1 -1
  100. package/dist/ui/components/organisms/vc-blade/_internal/vc-blade-toolbar/vc-blade-toolbar.vue.d.ts.map +1 -1
  101. package/dist/ui/components/organisms/vc-blade/vc-blade.vue.d.ts.map +1 -1
  102. package/dist/ui/components/organisms/vc-table/_internal/vc-table-base-header/vc-table-base-header.vue.d.ts.map +1 -1
  103. package/dist/ui/components/organisms/vc-table/_internal/vc-table-desktop-view/_internal/vc-table-columns-header/vc-table-columns-header.vue.d.ts.map +1 -1
  104. package/dist/ui/components/organisms/vc-table/_internal/vc-table-filter/vc-table-filter.vue.d.ts.map +1 -1
  105. package/dist/ui/components/organisms/vc-table/vc-table.vue.d.ts.map +1 -1
  106. package/dist/ui/composables/useVisibleElements.d.ts.map +1 -1
  107. package/package.json +4 -4
  108. package/shared/components/blade-navigation/components/vc-blade-view/vc-blade-view.ts +2 -2
  109. package/shared/components/blade-navigation/types/index.ts +117 -117
  110. package/shared/components/draggable-dashboard/DraggableDashboard.vue +116 -153
  111. package/shared/components/draggable-dashboard/_internal/DashboardWidget.vue +16 -32
  112. package/shared/components/draggable-dashboard/composables/useCellSizeCalculator.ts +121 -0
  113. package/shared/components/draggable-dashboard/composables/useCollisionDetection.ts +219 -0
  114. package/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.ts +126 -331
  115. package/shared/components/draggable-dashboard/composables/useDashboardGrid.ts +74 -220
  116. package/shared/components/draggable-dashboard/composables/useDragClone.ts +97 -0
  117. package/shared/components/draggable-dashboard/composables/useEventCoordinates.ts +91 -0
  118. package/shared/components/draggable-dashboard/composables/useGridPosition.ts +150 -0
  119. package/shared/components/draggable-dashboard/composables/useGridSystem.ts +169 -0
  120. package/shared/components/draggable-dashboard/composables/useLayoutPersistence.ts +89 -0
  121. package/shared/components/draggable-dashboard/composables/useResizeObserver.ts +105 -0
  122. package/shared/components/draggable-dashboard/composables/useWidgetLayout.ts +264 -0
  123. package/shared/components/draggable-dashboard/composables/useWidgetStyles.ts +120 -0
  124. package/shared/components/draggable-dashboard/types.ts +6 -1
  125. package/shared/components/notification-dropdown/notification-dropdown.vue +11 -1
  126. package/shared/components/sidebar/sidebar.vue +36 -34
  127. package/shared/pages/LoginPage/components/login/Login.vue +0 -2
  128. package/ui/components/atoms/vc-badge/vc-badge.vue +26 -10
  129. package/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue +27 -0
  130. package/ui/components/atoms/vc-icon/icons/OffersIcon.vue +23 -0
  131. package/ui/components/atoms/vc-icon/icons/OrdersIcon.vue +19 -0
  132. package/ui/components/atoms/vc-icon/icons/PeopleIcon.vue +21 -0
  133. package/ui/components/atoms/vc-icon/icons/ProductsIcon.vue +23 -0
  134. package/ui/components/atoms/vc-icon/icons/ProfileIcon.vue +18 -0
  135. package/ui/components/atoms/vc-icon/icons/index.ts +6 -0
  136. package/ui/components/atoms/vc-icon/vc-icon.vue +101 -82
  137. package/ui/components/atoms/vc-tooltip/vc-tooltip.vue +2 -2
  138. package/ui/components/organisms/vc-app/_internal/vc-app-bar/_internal/AppBarHeader.vue +31 -2
  139. package/ui/components/organisms/vc-app/_internal/vc-app-bar/_internal/AppBarOverlay.vue +15 -13
  140. package/ui/components/organisms/vc-app/_internal/vc-app-bar/vc-app-bar.vue +26 -15
  141. package/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue +2 -2
  142. package/ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue +1 -0
  143. package/ui/components/organisms/vc-app/vc-app.vue +16 -5
  144. package/ui/components/organisms/vc-blade/_internal/vc-blade-header/vc-blade-header.vue +4 -5
  145. package/ui/components/organisms/vc-blade/_internal/vc-blade-toolbar/_internal/vc-blade-toolbar-buttons/_internal/vc-blade-toolbar-button/vc-blade-toolbar-circle-button.vue +2 -2
  146. package/ui/components/organisms/vc-blade/_internal/vc-blade-toolbar/_internal/vc-blade-toolbar-buttons/mobile/vc-blade-toolbar-mobile.vue +1 -1
  147. package/ui/components/organisms/vc-blade/_internal/vc-blade-toolbar/vc-blade-toolbar.vue +0 -3
  148. package/ui/components/organisms/vc-blade/vc-blade.vue +7 -2
  149. package/ui/components/organisms/vc-login-form/vc-login-form.vue +99 -99
  150. package/ui/components/organisms/vc-table/_internal/vc-table-base-header/vc-table-base-header.vue +5 -5
  151. package/ui/components/organisms/vc-table/_internal/vc-table-desktop-view/_internal/vc-table-columns-header/vc-table-columns-header.vue +1 -0
  152. package/ui/components/organisms/vc-table/_internal/vc-table-filter/vc-table-filter.vue +40 -28
  153. package/ui/components/organisms/vc-table/_internal/vc-table-header/vc-table-header.vue +1 -1
  154. package/ui/components/organisms/vc-table/vc-table.vue +9 -5
  155. package/ui/composables/useVisibleElements.ts +2 -0
@@ -4,19 +4,19 @@ import { GlobalSearchKey } from "../../../injection-keys";
4
4
  export interface GlobalSearchState {
5
5
  isSearchVisible: Ref<Record<string, boolean>>;
6
6
  searchQuery: Ref<Record<string, string>>;
7
- toggleSearch: (bladeId: number) => void;
8
- setSearchQuery: (bladeId: number, query: string) => void;
7
+ toggleSearch: (bladeId: string) => void;
8
+ setSearchQuery: (bladeId: string, query: string) => void;
9
9
  }
10
10
 
11
11
  export function createGlobalSearch() {
12
12
  const isSearchVisible = ref<Record<string, boolean>>({});
13
13
  const searchQuery = ref<Record<string, string>>({});
14
14
 
15
- const toggleSearch = (bladeId: number) => {
15
+ const toggleSearch = (bladeId: string) => {
16
16
  isSearchVisible.value[bladeId] = !isSearchVisible.value[bladeId];
17
17
  };
18
18
 
19
- const setSearchQuery = (bladeId: number, query: string) => {
19
+ const setSearchQuery = (bladeId: string, query: string) => {
20
20
  searchQuery.value[bladeId] = query;
21
21
  };
22
22
 
@@ -1,110 +1,20 @@
1
- import { createSharedComposable, createUnrefFn, useArrayFind } from "@vueuse/core";
2
- import { Ref, ref, computed } from "vue";
3
- import * as _ from "lodash-es";
4
- import { i18n } from "./../../plugins/i18n";
5
- import { MenuItem } from "../../types";
6
-
7
- interface MenuService {
8
- addMenuItem: (item: MenuItem) => void;
9
- menuItems: Ref<MenuItem[]>;
10
- removeMenuItem: (item: MenuItem) => void;
11
- }
12
-
13
- const menuItems: Ref<MenuItem[]> = ref([]);
14
- const rawMenu: Ref<MenuItem[]> = ref([]);
15
-
16
- function useMenuServiceFn(): MenuService {
17
- const { t } = i18n.global;
18
-
19
- function addMenuItem(item: MenuItem): void {
20
- rawMenu.value.push(item);
21
- constructMenu();
22
- }
23
-
24
- const upsert = createUnrefFn(
25
- (array: (MenuItem | Omit<MenuItem, "icon">)[], element: MenuItem | Omit<MenuItem, "icon">) => {
26
- const index = array.findIndex((_element) => _.isEqual(_.omit(_element, "title"), _.omit(element, "title")));
27
- if (index > -1) {
28
- array[index] = { ...element };
29
- } else {
30
- array.push({ ...element });
31
- }
32
- },
33
- );
34
-
35
- function sortByPriority(a: MenuItem, b: MenuItem): number {
36
- const getPriority = (item: MenuItem): number => item.priority ?? Infinity;
37
- return getPriority(a) - getPriority(b);
38
- }
39
-
40
- function sortByGroupPriority(a: MenuItem, b: MenuItem): number {
41
- const getGroupPriority = (item: MenuItem): number => item.inGroupPriority ?? Infinity;
42
- return getGroupPriority(a) - getGroupPriority(b);
43
- }
44
-
45
- function constructMenu(): void {
46
- const constructedMenu: Ref<MenuItem[]> = ref([]);
47
-
48
- rawMenu.value.forEach((item) => {
49
- if (item.group) {
50
- const isGroupExist = useArrayFind(
51
- constructedMenu,
52
- (m) => m.groupId === "group_" + _.snakeCase(t(item.group ?? "")),
53
- );
54
-
55
- const groupItem = _.omit(
56
- {
57
- ...item,
58
- title: computed(() => t(item.title as string)),
59
- },
60
- "group",
61
- "groupIcon",
62
- "groupPriority",
63
- );
64
-
65
- if (isGroupExist.value && isGroupExist.value.children) {
66
- upsert(isGroupExist.value.children, groupItem);
67
- } else {
68
- const group: Omit<MenuItem, "icon"> = {
69
- groupId: "group_" + _.snakeCase(t(item.group)),
70
- groupIcon: item.groupIcon ?? "",
71
- title: computed(() => t(item.group as string)),
72
- children: [_.omit(groupItem)],
73
- priority: item.priority,
74
- };
75
- upsert(constructedMenu.value, group);
76
- }
77
- } else {
78
- if (item.title) {
79
- upsert(constructedMenu.value, { ...item });
80
- }
81
- }
82
- });
83
-
84
- menuItems.value = constructedMenu.value
85
- .map(
86
- (x): MenuItem => ({
87
- ...x,
88
- title: computed(() => t(x.title as string)),
89
- id: _.snakeCase(t(x.title as string)),
90
- children: x.children?.sort(sortByGroupPriority),
91
- }),
92
- )
93
- .sort(sortByPriority);
94
- }
95
-
96
- function removeMenuItem(item: MenuItem): void {
97
- const index = menuItems.value.indexOf(item);
98
- menuItems.value.splice(index, 1);
99
- }
100
-
101
- return {
102
- addMenuItem,
103
- menuItems,
104
- removeMenuItem,
105
- };
106
- }
107
-
108
- const useMenuService = createSharedComposable(useMenuServiceFn);
109
-
110
- export { useMenuService };
1
+ import { provide, inject, getCurrentInstance } from "vue";
2
+ import { MenuService, createMenuService, addMenuItem } from "../../services/menu-service";
3
+ import { MenuServiceKey } from "../../../injection-keys";
4
+
5
+ export function provideMenuService(): MenuService {
6
+ const service = createMenuService();
7
+ provide(MenuServiceKey, service);
8
+ return service;
9
+ }
10
+
11
+ export function useMenuService(): MenuService {
12
+ const service = inject(MenuServiceKey);
13
+ if (!service) {
14
+ console.error("Menu service not found in current context. Injection chain:", getCurrentInstance());
15
+ throw new Error("MenuService not provided");
16
+ }
17
+ return service;
18
+ }
19
+
20
+ export { addMenuItem };
@@ -1,5 +1,5 @@
1
1
  import { getCurrentInstance, inject, provide } from "vue";
2
- import { createWidgetService, IWidgetService } from "./../../services/widget-service";
2
+ import { createWidgetService, IWidgetService, registerWidget } from "./../../services/widget-service";
3
3
  import { WidgetServiceKey } from "./../../../injection-keys";
4
4
 
5
5
  export function provideWidgetService(): IWidgetService {
@@ -17,3 +17,4 @@ export function useWidgets(): IWidgetService {
17
17
  return service;
18
18
  }
19
19
 
20
+ export { registerWidget };
@@ -1 +1,3 @@
1
+ export const FALLBACK_BLADE_ID = "fallback-blade-id";
2
+
1
3
  export * from "./locale";
@@ -1,9 +1,9 @@
1
- import { App, Component, h, watch } from "vue";
1
+ import { App, Component, h, resolveComponent, watch } from "vue";
2
2
  import { i18n } from "./../i18n";
3
3
  import { Router } from "vue-router";
4
4
  import { BladeInstanceConstructor, BladeVNode } from "./../../../shared/components/blade-navigation/types";
5
5
  import { kebabToPascal } from "./../../utilities";
6
- import { useMenuService, useNotifications } from "../../composables";
6
+ import { addMenuItem, useMenuService, useNotifications } from "../../composables";
7
7
  import * as _ from "lodash-es";
8
8
  import { notification } from "../../../shared";
9
9
 
@@ -200,12 +200,12 @@ export function createAppModule(
200
200
 
201
201
  // Add to menu
202
202
  if (page.menuItem) {
203
- const { addMenuItem } = useMenuService();
204
203
  addMenuItem({
205
204
  ...page.menuItem,
205
+ icon: resolveComponent(page.menuItem.icon as string),
206
206
  url: page.url,
207
207
  routeId: routeName,
208
- permissions: page.permissions,
208
+ permissions: page.permissions || page.menuItem.permissions,
209
209
  });
210
210
  }
211
211
  }
@@ -0,0 +1,459 @@
1
+ import { computed, ComputedRef, ref, type Ref, type Component } from "vue";
2
+ import * as _ from "lodash-es";
3
+ import { i18n } from "./../plugins/i18n";
4
+ import type { MenuItem, MenuItemConfig } from "../types";
5
+ import { createUnrefFn, useArrayFind } from "@vueuse/core";
6
+ import { usePermissions } from "../composables";
7
+
8
+ // Global state for pre-registering menu items
9
+ const preregisteredMenuItems: Ref<MenuItem[]> = ref([]);
10
+
11
+ // Separate interface for creating a group where id can be optional
12
+ export interface MenuItemConfigCreateOptions extends Omit<MenuItemConfig, "id"> {
13
+ id?: string;
14
+ }
15
+
16
+ /**
17
+ * Configuration for creating a group together with a menu item
18
+ */
19
+ export interface MenuItemWithGroupConfig extends Omit<MenuItemConfig, "group" | "groupId" | "groupIcon"> {
20
+ /**
21
+ * Required group configuration for automatically creating a group.
22
+ * The item will be added to this group.
23
+ */
24
+ groupConfig: MenuItemConfigCreateOptions;
25
+ }
26
+
27
+ // Global state for menu groups
28
+ const MenuItemConfigs: Ref<Record<string, MenuItemConfig>> = ref({});
29
+
30
+ /**
31
+ * Helper function to create a menu item with group in one step
32
+ * @param item - The menu item properties
33
+ * @param group - The group properties
34
+ * @param inGroupPriority - Optional priority within the group, defaults to item.priority
35
+ * @returns A menu item configuration with embedded group
36
+ */
37
+ export function createMenuItemWithGroup(
38
+ item: Omit<MenuItemConfig, "group" | "groupId">,
39
+ group: MenuItemConfigCreateOptions,
40
+ inGroupPriority?: number,
41
+ ): MenuItemWithGroupConfig {
42
+ return {
43
+ ...item,
44
+ // Use explicitly passed inGroupPriority or item.priority as a fallback
45
+ inGroupPriority: inGroupPriority ?? item.priority,
46
+ groupConfig: {
47
+ id: group.id || _.uniqueId("group_"),
48
+ ...group,
49
+ },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Registers a menu item before the service is initialized
55
+ */
56
+ export function addMenuItem(item: MenuItem | MenuItemWithGroupConfig): void {
57
+ // We need to convert the type because the external function can accept MenuItemWithGroupConfig,
58
+ // but preregisteredMenuItems expects MenuItem
59
+ if ("groupConfig" in item) {
60
+ // Conversion will be performed in the addMenuItem function inside createMenuService
61
+ preregisteredMenuItems.value.push(item as unknown as MenuItem);
62
+ } else {
63
+ preregisteredMenuItems.value.push(item);
64
+ }
65
+ }
66
+
67
+ export interface MenuService {
68
+ /**
69
+ * Adds a menu item to the menu.
70
+ * If the item contains a `groupConfig` property, the group will be created automatically
71
+ * if it doesn't exist yet.
72
+ * @param item - The menu item to add or a config with both item and group
73
+ */
74
+ addMenuItem: (item: MenuItem | MenuItemWithGroupConfig) => void;
75
+ menuItems: Ref<MenuItem[]>;
76
+ removeMenuItem: (item: MenuItem) => void;
77
+ updateMenuItem: (id: string, updatedItem: Partial<MenuItem>) => boolean;
78
+ getMenuItem: (id: string) => MenuItem | undefined;
79
+ createMenuItemConfig: (group: MenuItemConfigCreateOptions) => string;
80
+ updateMenuItemConfig: (id: string, group: Partial<MenuItemConfig>) => boolean;
81
+ removeMenuItemConfig: (id: string) => boolean;
82
+ getMenuItemConfigs: () => Record<string, MenuItemConfig>;
83
+ }
84
+
85
+ // Default priority values
86
+ const DEFAULT_PRIORITY = Infinity;
87
+ const DEFAULT_GROUP_PRIORITY = Infinity;
88
+
89
+ // State
90
+ const menuItems: Ref<MenuItem[]> = ref([]);
91
+ const rawMenu: Ref<MenuItem[]> = ref([]);
92
+
93
+ /**
94
+ * Menu service implementation
95
+ * Handles the registration and organization of menu items
96
+ */
97
+ export function createMenuService(): MenuService {
98
+ const { t } = i18n.global;
99
+ const { hasAccess } = usePermissions();
100
+
101
+ /**
102
+ * Add a new menu item to the raw menu and rebuild the menu structure
103
+ * @param item - The menu item to add, or a menu item with group configuration
104
+ */
105
+ function addMenuItem(item: MenuItem | MenuItemWithGroupConfig): void {
106
+ // Check if item contains group configuration
107
+ if ("groupConfig" in item && item.groupConfig) {
108
+ // Either use existing group or create a new one
109
+ let groupId: string;
110
+ const groupConfig = item.groupConfig;
111
+
112
+ // If the group already exists with the same id, use that
113
+ if (groupConfig.id && MenuItemConfigs.value[groupConfig.id]) {
114
+ groupId = groupConfig.id;
115
+ // Optionally update the group with any new properties
116
+ updateMenuItemConfig(groupId, groupConfig);
117
+ } else {
118
+ // Create a new group
119
+ groupId = createMenuItemConfig(groupConfig);
120
+ }
121
+
122
+ // Create the menu item with the group ID
123
+ const menuItem: MenuItem = {
124
+ ..._.omit(item, ["groupConfig"]),
125
+ groupId,
126
+ // Если не указан inGroupPriority, используем priority для совместимости
127
+ inGroupPriority: item.inGroupPriority ?? item.priority,
128
+ } as MenuItem;
129
+
130
+ // Add it to the raw menu
131
+ rawMenu.value.push(menuItem);
132
+ } else {
133
+ // Regular menu item, just add it
134
+ rawMenu.value.push(item as MenuItem);
135
+ }
136
+
137
+ constructMenu();
138
+ }
139
+
140
+ /**
141
+ * Removes a menu item from the menu
142
+ * @param item - The menu item to remove
143
+ */
144
+ function removeMenuItem(item: MenuItem): void {
145
+ const index = rawMenu.value.findIndex(
146
+ (menuItem) => menuItem.id === item.id || (menuItem.title === item.title && menuItem.group === item.group),
147
+ );
148
+
149
+ if (index !== -1) {
150
+ rawMenu.value.splice(index, 1);
151
+ constructMenu();
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Updates an existing menu item by id
157
+ * @param id - The id of the menu item to update
158
+ * @param updatedItem - The updated properties
159
+ * @returns true if the item was found and updated, false otherwise
160
+ */
161
+ function updateMenuItem(id: string, updatedItem: Partial<MenuItem>): boolean {
162
+ const index = rawMenu.value.findIndex((item) => item.id === id);
163
+ if (index !== -1) {
164
+ rawMenu.value[index] = { ...rawMenu.value[index], ...updatedItem };
165
+ constructMenu();
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * Get a menu item by id
173
+ * @param id - The id of the menu item
174
+ * @returns The menu item or undefined if not found
175
+ */
176
+ function getMenuItem(id: string): MenuItem | undefined {
177
+ // First try to find in top level menu items
178
+ const topLevelItem = menuItems.value.find((item) => item.id === id);
179
+ if (topLevelItem) return topLevelItem;
180
+
181
+ // Then look in children
182
+ for (const item of menuItems.value) {
183
+ if (item.children) {
184
+ const childItem = item.children.find((child) => child.id === id);
185
+ if (childItem) return childItem;
186
+ }
187
+ }
188
+
189
+ return undefined;
190
+ }
191
+
192
+ /**
193
+ * Creates a new menu group
194
+ * @param group - The group to create
195
+ * @returns The id of the created group
196
+ */
197
+ function createMenuItemConfig(group: MenuItemConfigCreateOptions): string {
198
+ // Use provided id or generate a new one
199
+ const groupId = group.id || _.uniqueId("group_");
200
+
201
+ MenuItemConfigs.value[groupId] = {
202
+ ...group,
203
+ id: groupId,
204
+ } as MenuItemConfig;
205
+
206
+ // Rebuild menu to incorporate the new group
207
+ constructMenu();
208
+
209
+ return groupId;
210
+ }
211
+
212
+ /**
213
+ * Updates an existing menu group
214
+ * @param id - The id of the group to update
215
+ * @param group - The updated properties
216
+ * @returns true if the group was found and updated, false otherwise
217
+ */
218
+ function updateMenuItemConfig(id: string, group: Partial<MenuItemConfig>): boolean {
219
+ if (MenuItemConfigs.value[id]) {
220
+ MenuItemConfigs.value[id] = { ...MenuItemConfigs.value[id], ...group };
221
+ constructMenu();
222
+ return true;
223
+ }
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Removes a menu group
229
+ * @param id - The id of the group to remove
230
+ * @returns true if the group was found and removed, false otherwise
231
+ */
232
+ function removeMenuItemConfig(id: string): boolean {
233
+ if (MenuItemConfigs.value[id]) {
234
+ delete MenuItemConfigs.value[id];
235
+
236
+ // Update menu items that belonged to this group
237
+ rawMenu.value = rawMenu.value.filter((item) => item.groupId !== id);
238
+
239
+ constructMenu();
240
+ return true;
241
+ }
242
+ return false;
243
+ }
244
+
245
+ /**
246
+ * Gets all menu groups
247
+ * @returns All menu groups
248
+ */
249
+ function getMenuItemConfigs(): Record<string, MenuItemConfig> {
250
+ return { ...MenuItemConfigs.value };
251
+ }
252
+
253
+ /**
254
+ * Updates an element in an array or adds it if not found
255
+ */
256
+ const upsert = createUnrefFn(
257
+ (array: (MenuItem | Omit<MenuItem, "icon">)[], element: MenuItem | Omit<MenuItem, "icon">) => {
258
+ const index = array.findIndex((_element) => _.isEqual(_.omit(_element, "title"), _.omit(element, "title")));
259
+ if (index > -1) {
260
+ array[index] = { ...element };
261
+ } else {
262
+ array.push({ ...element });
263
+ }
264
+ },
265
+ );
266
+
267
+ /**
268
+ * Sorts menu items by their priority
269
+ */
270
+ function sortByPriority(a: MenuItem, b: MenuItem): number {
271
+ const getPriority = (item: MenuItem): number => item.priority ?? DEFAULT_PRIORITY;
272
+ return getPriority(a) - getPriority(b);
273
+ }
274
+
275
+ /**
276
+ * Sorts items within a group by their inGroupPriority
277
+ */
278
+ function sortByGroupPriority(a: MenuItem, b: MenuItem): number {
279
+ const getGroupPriority = (item: MenuItem): number =>
280
+ item.inGroupPriority ?? item.groupConfig?.priority ?? DEFAULT_GROUP_PRIORITY;
281
+ return getGroupPriority(a) - getGroupPriority(b);
282
+ }
283
+
284
+ /**
285
+ * Creates a localized ID from a title
286
+ */
287
+ function createItemId(title: string | ComputedRef<string>): string {
288
+ const titleValue = typeof title === "string" ? t(title) : title.value;
289
+ return _.snakeCase(titleValue);
290
+ }
291
+
292
+ /**
293
+ * Creates a localized computed title
294
+ */
295
+ function createLocalizedTitle(title: string): ComputedRef<string> {
296
+ return computed(() => t(title));
297
+ }
298
+
299
+ /**
300
+ * Checks if the current user has permission to see the menu item
301
+ * @param item - The menu item to check
302
+ * @returns true if the user has permission, false otherwise
303
+ */
304
+ function hasPermissionForItem(item: MenuItem): boolean {
305
+ // If no permissions are specified, the item is visible to everyone
306
+ if (!item.permissions || (Array.isArray(item.permissions) && item.permissions.length === 0)) {
307
+ return true;
308
+ }
309
+
310
+ // Check if user has any of the required permissions
311
+ return hasAccess(item.permissions);
312
+ }
313
+
314
+ /**
315
+ * Processes a menu item that belongs to a group
316
+ */
317
+ function processGroupItem(item: MenuItem, constructedMenu: Ref<MenuItem[]>): void {
318
+ // Skip items the user doesn't have permission to see
319
+ if (!hasPermissionForItem(item)) return;
320
+
321
+ // Handle both legacy (group by name) and new (group by id) approaches
322
+ let groupId: string;
323
+ let groupTitle: string;
324
+ let groupIcon: string | Component = "";
325
+ let groupPriority: number = DEFAULT_PRIORITY;
326
+ let groupPermissions: string | string[] | undefined;
327
+
328
+ // If using groupId (new way)
329
+ if (item.groupId && MenuItemConfigs.value[item.groupId]) {
330
+ const group = MenuItemConfigs.value[item.groupId];
331
+ groupId = "group_" + group.id;
332
+ groupTitle = group.title;
333
+ groupIcon = group.icon || "";
334
+ groupPriority = group.priority || DEFAULT_PRIORITY;
335
+ groupPermissions = group.permissions;
336
+ }
337
+ // If using group string (legacy way)
338
+ else if (item.group) {
339
+ console.warn("Using item.group is deprecated, please use item.groupId instead");
340
+ groupId = "group_" + createItemId(item.group);
341
+ groupTitle = item.group;
342
+ groupIcon = item.groupIcon || "";
343
+ groupPriority = item.priority || DEFAULT_PRIORITY;
344
+ groupPermissions = item.permissions;
345
+ }
346
+ // Skip if no valid group reference found
347
+ else {
348
+ return;
349
+ }
350
+
351
+ const existingGroup = useArrayFind(constructedMenu, (m) => m.groupId === groupId);
352
+
353
+ // Create the item to be added to the group
354
+ const groupItem = {
355
+ ..._.omit(item, ["group", "groupIcon", "priority", "groupId"]),
356
+ title: createLocalizedTitle(item.title as string),
357
+ // Ensure inGroupPriority is preserved and used
358
+ inGroupPriority: item.inGroupPriority || item.priority || DEFAULT_GROUP_PRIORITY,
359
+ } as MenuItem;
360
+
361
+ if (existingGroup.value && existingGroup.value.children) {
362
+ // Add to existing group
363
+ upsert(existingGroup.value.children, groupItem);
364
+ } else {
365
+ // Skip creating group if user doesn't have permission for the group
366
+ if (groupPermissions && !hasAccess(groupPermissions)) {
367
+ return;
368
+ }
369
+
370
+ // Create a new group with this item
371
+ const group = {
372
+ groupId,
373
+ groupIcon,
374
+ title: createLocalizedTitle(groupTitle),
375
+ children: [groupItem],
376
+ priority: groupPriority,
377
+ permissions: groupPermissions,
378
+ } as MenuItem;
379
+ upsert(constructedMenu.value, group);
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Processes a standalone menu item (not in a group)
385
+ */
386
+ function processStandaloneItem(item: MenuItem, constructedMenu: Ref<MenuItem[]>): void {
387
+ // Skip items the user doesn't have permission to see
388
+ if (!hasPermissionForItem(item)) return;
389
+
390
+ if (item.title) {
391
+ const standaloneItem = {
392
+ ...item,
393
+ title: createLocalizedTitle(item.title as string),
394
+ } as MenuItem;
395
+ upsert(constructedMenu.value, standaloneItem);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Finalizes menu items by adding IDs and sorting children
401
+ * Also filters out groups with no visible children
402
+ */
403
+ function finalizeMenuItems(items: MenuItem[]): MenuItem[] {
404
+ return (
405
+ items
406
+ .map(
407
+ (item): MenuItem => ({
408
+ ...item,
409
+ title: createLocalizedTitle(item.title as string),
410
+ id: item.id || createItemId(item.title as ComputedRef<string>),
411
+ children: item.children?.filter((child) => hasPermissionForItem(child)).sort(sortByGroupPriority),
412
+ }),
413
+ )
414
+ // Filter out groups with no visible children
415
+ .filter((item) => !item.children || item.children.length > 0)
416
+ .sort(sortByPriority)
417
+ );
418
+ }
419
+
420
+ /**
421
+ * Constructs the complete menu structure from raw menu items
422
+ */
423
+ function constructMenu(): void {
424
+ const constructedMenu: Ref<MenuItem[]> = ref([]);
425
+
426
+ // Process each raw menu item
427
+ rawMenu.value.forEach((item) => {
428
+ if (item.group || item.groupId) {
429
+ processGroupItem(item, constructedMenu);
430
+ } else {
431
+ processStandaloneItem(item, constructedMenu);
432
+ }
433
+ });
434
+
435
+ // Finalize and set the menu items
436
+ menuItems.value = finalizeMenuItems(constructedMenu.value);
437
+ }
438
+
439
+ // Process any pre-registered menu items, including those with group configurations
440
+ preregisteredMenuItems.value.forEach((item) => {
441
+ try {
442
+ addMenuItem(item);
443
+ } catch (e) {
444
+ console.warn(`Failed to register preregistered menu item ${item.id || item.title}:`, e);
445
+ }
446
+ });
447
+
448
+ return {
449
+ addMenuItem,
450
+ menuItems,
451
+ removeMenuItem,
452
+ updateMenuItem,
453
+ getMenuItem,
454
+ createMenuItemConfig,
455
+ updateMenuItemConfig,
456
+ removeMenuItemConfig,
457
+ getMenuItemConfigs,
458
+ };
459
+ }
@@ -33,6 +33,18 @@ export interface IWidgetService {
33
33
  isWidgetRegistered: (id: string) => boolean;
34
34
  }
35
35
 
36
+ // Global state for pre-registering widgets
37
+ const preregisteredWidgets: IWidgetRegistration[] = [];
38
+ const preregisteredIds = new Set<string>();
39
+
40
+ /**
41
+ * Registers a widget before the service is initialized
42
+ */
43
+ export function registerWidget(widget: IWidget, bladeId: string): void {
44
+ preregisteredWidgets.push({ bladeId, widget });
45
+ preregisteredIds.add(widget.id);
46
+ }
47
+
36
48
  export function createWidgetService(): IWidgetService {
37
49
  const widgetRegistry = reactive<Record<string, IWidget[]>>({});
38
50
  const registeredWidgets = reactive<IWidgetRegistration[]>([]);
@@ -113,6 +125,14 @@ export function createWidgetService(): IWidgetService {
113
125
  return activeWidgetRef.value?.id === id;
114
126
  };
115
127
 
128
+ preregisteredWidgets.forEach((widget) => {
129
+ try {
130
+ registerWidget(widget.widget, widget.bladeId);
131
+ } catch (e) {
132
+ console.warn(`Failed to register preregistered widget ${widget.widget.id}:`, e);
133
+ }
134
+ });
135
+
116
136
  return {
117
137
  registerWidget,
118
138
  unregisterWidget,