@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * useNavContext - Route-aware navigation context for breadcrumb and navlinks
3
+ *
4
+ * Uses semantic breadcrumb as the source of truth for navigation structure.
5
+ * Entity data comes from activeStack (set by useEntityItemPage/useForm).
6
+ *
7
+ * Semantic breadcrumb kinds:
8
+ * - entity-list: Entity collection (e.g., /books)
9
+ * - entity-show: Entity instance view (e.g., /books/1)
10
+ * - entity-edit: Entity instance edit (e.g., /books/1/edit)
11
+ * - entity-create: Entity creation (e.g., /books/create)
12
+ * - route: Generic route (e.g., /settings)
13
+ *
14
+ * Examples:
15
+ * Path: /books → [{ kind: 'entity-list', entity: 'books' }]
16
+ * Path: /books/1/edit → [{ kind: 'entity-list', entity: 'books' }, { kind: 'entity-edit', entity: 'books', id: '1' }]
17
+ * Path: /books/stats → [{ kind: 'entity-list', entity: 'books' }, { kind: 'route', route: 'book-stats' }]
18
+ */
19
+ import { ref, computed, watch, inject, type Ref, type ComputedRef } from 'vue'
20
+ import { useRoute, useRouter, type RouteParamsRawGeneric } from 'vue-router'
21
+ import { getSiblingRoutes } from '../module/moduleRegistry'
22
+ import { useSemanticBreadcrumb, type SemanticBreadcrumbItem } from './useSemanticBreadcrumb'
23
+ import { useStackHydrator, type HydratedLevel } from '../chain/useStackHydrator'
24
+
25
+ /**
26
+ * Navigation chain segment types
27
+ */
28
+ export type NavChainType = 'list' | 'item' | 'create' | 'route' | 'child-list'
29
+
30
+ /**
31
+ * Navigation chain segment
32
+ */
33
+ export interface NavChainSegment {
34
+ type: NavChainType
35
+ entity?: string
36
+ manager?: EntityManager | null
37
+ id?: string
38
+ label?: string
39
+ routeName?: string | null
40
+ navLabel?: string
41
+ route?: string
42
+ }
43
+
44
+ /**
45
+ * Entity manager interface
46
+ */
47
+ interface EntityManager {
48
+ routePrefix?: string
49
+ labelPlural?: string
50
+ idField?: string
51
+ readOnly?: boolean
52
+ getEntityLabel: (data: unknown) => string
53
+ }
54
+
55
+ /**
56
+ * Orchestrator interface
57
+ */
58
+ interface Orchestrator {
59
+ get: (entityName: string) => EntityManager | null
60
+ }
61
+
62
+ /**
63
+ * Breadcrumb item
64
+ */
65
+ export interface BreadcrumbItem {
66
+ label: string
67
+ to?: { name: string; params?: RouteParamsRawGeneric } | null
68
+ icon?: string
69
+ }
70
+
71
+ /**
72
+ * Nav link item
73
+ */
74
+ export interface NavLinkItem {
75
+ label: string
76
+ to: { name: string; params?: RouteParamsRawGeneric }
77
+ active: boolean
78
+ }
79
+
80
+ /**
81
+ * Parent config from route meta
82
+ */
83
+ interface ParentConfig {
84
+ entity: string
85
+ param: string
86
+ itemRoute?: string
87
+ }
88
+
89
+ /**
90
+ * Options for useNavContext
91
+ */
92
+ export interface UseNavContextOptions {
93
+ [key: string]: unknown
94
+ }
95
+
96
+ /**
97
+ * Return type for useNavContext
98
+ */
99
+ export interface UseNavContextReturn {
100
+ // Analysis
101
+ navChain: ComputedRef<NavChainSegment[]>
102
+ semanticBreadcrumb: ComputedRef<SemanticBreadcrumbItem[]>
103
+
104
+ // Data
105
+ entityData: ComputedRef<unknown | null>
106
+ parentData: ComputedRef<unknown | null>
107
+ chainData: Ref<Map<number, unknown>>
108
+
109
+ // Navigation
110
+ breadcrumb: ComputedRef<BreadcrumbItem[]>
111
+ navlinks: ComputedRef<NavLinkItem[]>
112
+
113
+ // Helpers
114
+ parentConfig: ComputedRef<ParentConfig | undefined>
115
+ parentId: ComputedRef<unknown | null>
116
+ }
117
+
118
+ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContextReturn {
119
+ const route = useRoute()
120
+ const router = useRouter()
121
+
122
+ // Semantic breadcrumb as source of truth
123
+ const { breadcrumb: semanticBreadcrumb } = useSemanticBreadcrumb()
124
+
125
+ // Injected dependencies
126
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
127
+ const homeRouteName = inject<string | null>('qdadmHomeRoute', null)
128
+
129
+ // Stack hydrator for entity data (has async-loaded data and labels)
130
+ const hydrator = useStackHydrator()
131
+
132
+ function getManager(entityName: string): EntityManager | null {
133
+ return orchestrator?.get(entityName) ?? null
134
+ }
135
+
136
+ function routeExists(name: string): boolean {
137
+ return router.getRoutes().some((r) => r.name === name)
138
+ }
139
+
140
+ /**
141
+ * Get default item route for an entity manager
142
+ * Checks which routes actually exist and returns the appropriate one:
143
+ * - Prefers -edit if it exists (editable entity)
144
+ * - Falls back to -show if -edit doesn't exist
145
+ * @param manager - Entity manager
146
+ * @returns Route name or null if neither exists
147
+ */
148
+ function getDefaultItemRoute(manager: EntityManager | null): string | null {
149
+ if (!manager) return null
150
+
151
+ const editRoute = `${manager.routePrefix}-edit`
152
+ const showRoute = `${manager.routePrefix}-show`
153
+
154
+ // Check which routes actually exist and return the first available
155
+ if (router.hasRoute(editRoute)) return editRoute
156
+ if (router.hasRoute(showRoute)) return showRoute
157
+
158
+ return null
159
+ }
160
+
161
+ /**
162
+ * Convert semantic breadcrumb to navigation chain format
163
+ * Maps semantic kinds to chain types for compatibility
164
+ */
165
+ function semanticToNavChain(semantic: SemanticBreadcrumbItem[]): NavChainSegment[] {
166
+ const chain: NavChainSegment[] = []
167
+
168
+ for (const item of semantic) {
169
+ if (item.kind === 'entity-list') {
170
+ const manager = getManager(item.entity!)
171
+ // For child routes, prefer route's navLabel over manager's labelPlural
172
+ const routeRecord = item.route
173
+ ? router.getRoutes().find((r) => r.name === item.route)
174
+ : null
175
+ const label =
176
+ (routeRecord?.meta?.navLabel as string) || manager?.labelPlural || item.entity!
177
+ chain.push({
178
+ type: 'list',
179
+ entity: item.entity,
180
+ manager,
181
+ label,
182
+ routeName: item.route || manager?.routePrefix || item.entity!.slice(0, -1),
183
+ })
184
+ } else if (item.kind?.startsWith('entity-') && item.id) {
185
+ // entity-show, entity-edit, entity-delete
186
+ const manager = getManager(item.entity!)
187
+ chain.push({
188
+ type: 'item',
189
+ entity: item.entity,
190
+ manager,
191
+ id: item.id,
192
+ routeName: getDefaultItemRoute(manager),
193
+ })
194
+ } else if (item.kind === 'entity-create') {
195
+ // Create page - treat as special list
196
+ const manager = getManager(item.entity!)
197
+ chain.push({
198
+ type: 'create',
199
+ entity: item.entity,
200
+ manager,
201
+ label: 'Create',
202
+ routeName: manager ? `${manager.routePrefix}-create` : null,
203
+ })
204
+ } else if (item.kind === 'route') {
205
+ // Generic route (like /books/stats)
206
+ // Use label from semantic item if provided (custom breadcrumb), else lookup route
207
+ const routeRecord = router.getRoutes().find((r) => r.name === item.route)
208
+ chain.push({
209
+ type: 'route',
210
+ route: item.route,
211
+ label:
212
+ item.label ||
213
+ (routeRecord?.meta?.navLabel as string) ||
214
+ (routeRecord?.meta?.title as string) ||
215
+ item.route,
216
+ routeName: item.route,
217
+ })
218
+ }
219
+ }
220
+
221
+ return chain
222
+ }
223
+
224
+ // ============================================================================
225
+ // COMPUTED NAVIGATION CHAIN
226
+ // ============================================================================
227
+
228
+ /**
229
+ * Navigation chain built from semantic breadcrumb
230
+ */
231
+ const navChain = computed(() => {
232
+ return semanticToNavChain(semanticBreadcrumb.value)
233
+ })
234
+
235
+ // ============================================================================
236
+ // ENTITY DATA FROM ACTIVESTACK
237
+ // ============================================================================
238
+
239
+ const chainData = ref<Map<number, unknown>>(new Map()) // Map: chainIndex -> entityData
240
+
241
+ /**
242
+ * Build chainData from stackHydrator levels
243
+ *
244
+ * StackHydrator fetches entity data asynchronously and provides labels.
245
+ * Each hydrated level has .data and .label fields when loaded.
246
+ */
247
+ watch(
248
+ [navChain, hydrator.levels],
249
+ ([chain, levels]: [NavChainSegment[], HydratedLevel[]]) => {
250
+ // Build new Map (reassignment triggers Vue reactivity)
251
+ const newChainData = new Map<number, unknown>()
252
+
253
+ for (let i = 0; i < chain.length; i++) {
254
+ const segment = chain[i]
255
+ if (!segment || segment.type !== 'item') continue
256
+
257
+ // Find matching data in hydrator by entity name and id
258
+ const hydratedLevel = levels.find(
259
+ (l) => l.entity === segment.entity && String(l.id) === String(segment.id)
260
+ )
261
+ if (hydratedLevel?.data) {
262
+ newChainData.set(i, hydratedLevel.data)
263
+ }
264
+ }
265
+
266
+ // Assign new Map to trigger reactivity
267
+ chainData.value = newChainData
268
+ },
269
+ { immediate: true, deep: true }
270
+ )
271
+
272
+ // ============================================================================
273
+ // BREADCRUMB
274
+ // ============================================================================
275
+
276
+ const homeItem = computed(() => {
277
+ if (!homeRouteName || !routeExists(homeRouteName)) return null
278
+ return {
279
+ label: homeRouteName === 'dashboard' ? 'Dashboard' : 'Home',
280
+ to: { name: homeRouteName },
281
+ icon: 'pi pi-home',
282
+ }
283
+ })
284
+
285
+ const breadcrumb = computed(() => {
286
+ const items: BreadcrumbItem[] = []
287
+ const chain = navChain.value
288
+
289
+ // Home
290
+ if (homeItem.value) {
291
+ items.push(homeItem.value)
292
+ }
293
+
294
+ // Build from chain
295
+ for (let i = 0; i < chain.length; i++) {
296
+ const segment = chain[i]
297
+ if (!segment) continue
298
+ const isLast = i === chain.length - 1
299
+
300
+ if (segment.type === 'list') {
301
+ items.push({
302
+ label: segment.label!,
303
+ to:
304
+ isLast || !segment.routeName || !routeExists(segment.routeName)
305
+ ? null
306
+ : { name: segment.routeName },
307
+ })
308
+ } else if (segment.type === 'item') {
309
+ const data = chainData.value.get(i)
310
+ const label = data && segment.manager ? segment.manager.getEntityLabel(data) : '...'
311
+ const idField = segment.manager?.idField || 'id'
312
+
313
+ // Only create link if we have a valid id
314
+ const canLink = !isLast && segment.id && segment.routeName && routeExists(segment.routeName)
315
+ items.push({
316
+ label,
317
+ to: canLink ? { name: segment.routeName!, params: { [idField]: segment.id } } : null,
318
+ })
319
+ } else if (segment.type === 'create') {
320
+ items.push({
321
+ label: segment.label!,
322
+ })
323
+ } else if (segment.type === 'route') {
324
+ // Generic route (e.g., /books/stats)
325
+ items.push({
326
+ label: segment.label!,
327
+ to:
328
+ isLast || !segment.routeName || !routeExists(segment.routeName)
329
+ ? null
330
+ : { name: segment.routeName },
331
+ })
332
+ } else if (segment.type === 'child-list') {
333
+ items.push({
334
+ label: segment.navLabel || segment.label!,
335
+ })
336
+ }
337
+ }
338
+
339
+ return items
340
+ })
341
+
342
+ // ============================================================================
343
+ // NAVLINKS (for child routes)
344
+ // ============================================================================
345
+
346
+ const parentConfig = computed(() => route.meta?.parent as ParentConfig | undefined)
347
+
348
+ const parentId = computed(() => {
349
+ if (!parentConfig.value) return null
350
+ return route.params[parentConfig.value.param]
351
+ })
352
+
353
+ const navlinks = computed(() => {
354
+ if (!parentConfig.value) return []
355
+
356
+ const { entity: parentEntity, param, itemRoute } = parentConfig.value
357
+ const parentManager = getManager(parentEntity)
358
+
359
+ // Guard: need valid manager and parentId to build links
360
+ if (!parentManager || !parentId.value) return []
361
+
362
+ const parentRouteName = itemRoute || getDefaultItemRoute(parentManager)
363
+ const isOnParent = route.name === parentRouteName
364
+
365
+ // Details link - use manager's idField for param name
366
+ const links: NavLinkItem[] = [
367
+ {
368
+ label: 'Details',
369
+ to: {
370
+ name: parentRouteName!,
371
+ params: { [parentManager.idField!]: parentId.value as string },
372
+ },
373
+ active: isOnParent,
374
+ },
375
+ ]
376
+
377
+ // Sibling routes - only include list routes (not create/edit/show)
378
+ // List routes are navigable tabs, while create/edit/show are accessed via the list
379
+ const siblings = getSiblingRoutes(parentEntity, param).filter(
380
+ (sibling) => sibling.meta?.layout === 'list' || sibling.meta?.layout === 'page'
381
+ )
382
+ for (const sibling of siblings) {
383
+ const sibManager = sibling.meta?.entity ? getManager(sibling.meta.entity as string) : null
384
+ links.push({
385
+ label:
386
+ (sibling.meta?.navLabel as string) || sibManager?.labelPlural || (sibling.name as string),
387
+ to: { name: sibling.name as string, params: route.params as RouteParamsRawGeneric },
388
+ active: route.name === sibling.name,
389
+ })
390
+ }
391
+
392
+ return links
393
+ })
394
+
395
+ // ============================================================================
396
+ // CONVENIENCE ACCESSORS
397
+ // ============================================================================
398
+
399
+ const entityData = computed(() => {
400
+ const chain = navChain.value
401
+ for (let i = chain.length - 1; i >= 0; i--) {
402
+ if (chain[i]?.type === 'item') {
403
+ return chainData.value.get(i) || null
404
+ }
405
+ }
406
+ return null
407
+ })
408
+
409
+ const parentData = computed(() => {
410
+ const chain = navChain.value
411
+ let foundCurrent = false
412
+ for (let i = chain.length - 1; i >= 0; i--) {
413
+ if (chain[i]?.type === 'item') {
414
+ if (foundCurrent) return chainData.value.get(i) || null
415
+ foundCurrent = true
416
+ }
417
+ }
418
+ return null
419
+ })
420
+
421
+ return {
422
+ // Analysis
423
+ navChain,
424
+ semanticBreadcrumb,
425
+
426
+ // Data
427
+ entityData,
428
+ parentData,
429
+ chainData,
430
+
431
+ // Navigation
432
+ breadcrumb,
433
+ navlinks,
434
+
435
+ // Helpers
436
+ parentConfig,
437
+ parentId,
438
+ }
439
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * useNavigation - Navigation composable for AppLayout
3
+ *
4
+ * Provides reactive navigation state from moduleRegistry.
5
+ * All data comes from module init declarations.
6
+ * Nav items are filtered based on EntityManager.canRead() permissions.
7
+ *
8
+ * Invokes 'menu:alter' hook on first access to allow modules to modify
9
+ * the navigation structure dynamically.
10
+ */
11
+
12
+ import { computed, inject, ref, onMounted, type ComputedRef, type Ref } from 'vue'
13
+ import { useRoute, useRouter } from 'vue-router'
14
+ import { getNavSections, alterMenuSections, isMenuAltered } from '../module/moduleRegistry'
15
+ import { useSemanticBreadcrumb } from './useSemanticBreadcrumb'
16
+ import { useI18n } from '../i18n/useI18n'
17
+ import type { HookRegistry } from '../hooks/HookRegistry'
18
+
19
+ /**
20
+ * Navigation item interface
21
+ */
22
+ export interface NavItem {
23
+ label: string
24
+ route: string
25
+ icon?: string
26
+ entity?: string
27
+ exact?: boolean
28
+ }
29
+
30
+ /**
31
+ * Navigation section interface
32
+ */
33
+ export interface NavSection {
34
+ label?: string
35
+ title?: string
36
+ items: NavItem[]
37
+ }
38
+
39
+ /**
40
+ * Entity manager interface
41
+ */
42
+ interface EntityManager {
43
+ canRead: () => boolean
44
+ }
45
+
46
+ /**
47
+ * Orchestrator interface
48
+ */
49
+ interface Orchestrator {
50
+ signals?: unknown
51
+ get: (entityName: string) => EntityManager | null
52
+ }
53
+
54
+ /**
55
+ * Return type for useNavigation
56
+ */
57
+ export interface UseNavigationReturn {
58
+ /** Navigation sections (filtered by permissions) */
59
+ navSections: ComputedRef<NavSection[]>
60
+ /** Whether menu:alter has completed */
61
+ isReady: Ref<boolean>
62
+ /** Check if nav item is active */
63
+ isNavActive: (item: NavItem) => boolean
64
+ /** Check if section has active item */
65
+ sectionHasActiveItem: (section: NavSection) => boolean
66
+ /** Handle nav item click */
67
+ handleNavClick: (event: Event, item: NavItem) => void
68
+ /** Current route name */
69
+ currentRouteName: ComputedRef<string | symbol | null | undefined>
70
+ /** Current route path */
71
+ currentRoutePath: ComputedRef<string>
72
+ }
73
+
74
+ /**
75
+ * Navigation composable
76
+ *
77
+ * @returns Navigation state and helpers
78
+ */
79
+ export function useNavigation(): UseNavigationReturn {
80
+ const route = useRoute()
81
+ const router = useRouter()
82
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
83
+ const hooks = inject<HookRegistry | null>('qdadmHooks', null)
84
+
85
+ // i18n integration — labels resolve through nav.sections.* / nav.routes.*
86
+ const { i18n: kernelI18n, locale: i18nLocale } = useI18n()
87
+
88
+ // Semantic breadcrumb for entity-based active detection
89
+ const { breadcrumb } = useSemanticBreadcrumb()
90
+
91
+ // Track whether menu:alter has completed
92
+ const isReady = ref(isMenuAltered())
93
+
94
+ // Trigger version to force reactivity after alteration or auth change
95
+ const alterVersion = ref(0)
96
+
97
+ /**
98
+ * Check if user can access a nav item based on its entity's canRead()
99
+ */
100
+ function canAccessNavItem(item: NavItem): boolean {
101
+ if (!item.entity || !orchestrator) return true
102
+ const manager = orchestrator.get(item.entity)
103
+ if (!manager) return true
104
+ return manager.canRead()
105
+ }
106
+
107
+ // Note: Auth signal listeners removed - Kernel.invalidateApp() remounts entire app
108
+ // on auth changes, so composable is re-initialized with fresh state
109
+
110
+ // Invoke menu:alter hook on mount
111
+ onMounted(async () => {
112
+ if (!isMenuAltered()) {
113
+ await alterMenuSections(hooks)
114
+ alterVersion.value++
115
+ isReady.value = true
116
+ }
117
+ })
118
+
119
+ /**
120
+ * Resolve a label through i18n with the given convention key, falling back
121
+ * to the inline label when the bundle has no matching translation.
122
+ */
123
+ function resolveNavLabel(key: string, fallback: string): string {
124
+ if (!kernelI18n) return fallback
125
+ const trace = kernelI18n.resolve(key)
126
+ return trace.hit ? trace.result : fallback
127
+ }
128
+
129
+ // Get nav sections from registry, filtering items based on permissions
130
+ // Depends on alterVersion + i18nLocale to trigger re-computation after
131
+ // alteration or locale change.
132
+ const navSections = computed(() => {
133
+ // Force dependencies for reactivity
134
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
135
+ alterVersion.value
136
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
137
+ i18nLocale.value
138
+
139
+ const sections = getNavSections()
140
+ return sections
141
+ .map((section) => ({
142
+ ...section,
143
+ title: resolveNavLabel(`nav.sections.${section.title}`, section.title),
144
+ items: section.items
145
+ .filter((item) => canAccessNavItem(item))
146
+ .map((item) => ({
147
+ ...item,
148
+ label: resolveNavLabel(`nav.routes.${item.route}`, item.label),
149
+ })),
150
+ }))
151
+ .filter((section) => section.items.length > 0) // Hide empty sections
152
+ })
153
+
154
+ /**
155
+ * Check if a nav item is currently active
156
+ * Uses semantic breadcrumb only - no route segment deduction
157
+ */
158
+ function isNavActive(item: NavItem): boolean {
159
+ // Exact match mode
160
+ if (item.exact) {
161
+ return route.name === item.route
162
+ }
163
+
164
+ // Check semantic breadcrumb for match
165
+ const bc = breadcrumb.value
166
+
167
+ // Entity-based menu item: match ONLY by entity (don't fall through)
168
+ if (item.entity) {
169
+ const firstEntityItem = bc.find((b) => b.kind.startsWith('entity-'))
170
+ return firstEntityItem ? firstEntityItem.entity === item.entity : false
171
+ }
172
+
173
+ // Route-based menu item: check if route is in breadcrumb
174
+ if (item.route) {
175
+ return bc.some((b) => b.kind === 'route' && b.route === item.route)
176
+ }
177
+
178
+ return false
179
+ }
180
+
181
+ /**
182
+ * Check if section contains active item
183
+ */
184
+ function sectionHasActiveItem(section: NavSection): boolean {
185
+ return section.items.some((item) => isNavActive(item))
186
+ }
187
+
188
+ /**
189
+ * Handle nav click - force navigation if on same route with query params
190
+ */
191
+ function handleNavClick(event: Event, item: NavItem): void {
192
+ if (route.name === item.route && Object.keys(route.query).length > 0) {
193
+ event.preventDefault()
194
+ router.push({ name: item.route })
195
+ }
196
+ }
197
+
198
+ return {
199
+ // Data (from moduleRegistry)
200
+ navSections,
201
+
202
+ // Ready state (menu:alter completed)
203
+ isReady,
204
+
205
+ // Active state
206
+ isNavActive,
207
+ sectionHasActiveItem,
208
+
209
+ // Event handlers
210
+ handleNavClick,
211
+
212
+ // Current route info (reactive)
213
+ currentRouteName: computed(() => route.name),
214
+ currentRoutePath: computed(() => route.path),
215
+ }
216
+ }