@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
import { useRoute, useRouter, type RouteRecordNormalized, type RouteLocationNormalized } from 'vue-router'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Semantic breadcrumb item kinds
|
|
6
|
+
*/
|
|
7
|
+
export type SemanticBreadcrumbKind =
|
|
8
|
+
| 'route'
|
|
9
|
+
| 'entity-list'
|
|
10
|
+
| 'entity-show'
|
|
11
|
+
| 'entity-edit'
|
|
12
|
+
| 'entity-create'
|
|
13
|
+
| 'entity-delete'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Semantic breadcrumb item
|
|
17
|
+
*/
|
|
18
|
+
export interface SemanticBreadcrumbItem {
|
|
19
|
+
kind: SemanticBreadcrumbKind
|
|
20
|
+
entity?: string
|
|
21
|
+
id?: string
|
|
22
|
+
route?: string
|
|
23
|
+
label?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Action segments mapping to entity kinds
|
|
28
|
+
*/
|
|
29
|
+
const ACTION_MAP: Record<string, SemanticBreadcrumbKind> = {
|
|
30
|
+
edit: 'entity-edit',
|
|
31
|
+
create: 'entity-create',
|
|
32
|
+
new: 'entity-create',
|
|
33
|
+
show: 'entity-show',
|
|
34
|
+
view: 'entity-show',
|
|
35
|
+
delete: 'entity-delete',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute semantic breadcrumb from route path and routes list
|
|
40
|
+
*
|
|
41
|
+
* Pure function that can be used both in Vue composables and outside components.
|
|
42
|
+
*
|
|
43
|
+
* If the matched route has `meta.breadcrumb`, that array is used directly.
|
|
44
|
+
* This allows routes to define a custom breadcrumb structure.
|
|
45
|
+
*
|
|
46
|
+
* @param path - Current route path (e.g., '/books/1/edit')
|
|
47
|
+
* @param routes - List of all registered routes from router.getRoutes()
|
|
48
|
+
* @param currentRoute - Current route object (optional, for meta.breadcrumb support)
|
|
49
|
+
* @returns Semantic breadcrumb items
|
|
50
|
+
*/
|
|
51
|
+
export function computeSemanticBreadcrumb(
|
|
52
|
+
path: string,
|
|
53
|
+
routes: RouteRecordNormalized[],
|
|
54
|
+
currentRoute: RouteLocationNormalized | null = null
|
|
55
|
+
): SemanticBreadcrumbItem[] {
|
|
56
|
+
if (!path) return []
|
|
57
|
+
|
|
58
|
+
// Check if route defines a custom breadcrumb
|
|
59
|
+
if (currentRoute?.meta?.breadcrumb) {
|
|
60
|
+
return currentRoute.meta.breadcrumb as SemanticBreadcrumbItem[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const items: SemanticBreadcrumbItem[] = []
|
|
64
|
+
|
|
65
|
+
// Filter out catch-all and not-found routes
|
|
66
|
+
const validRoutes = routes.filter(
|
|
67
|
+
(r) => r.name !== 'not-found' && !r.path.includes('*') && !r.path.includes(':pathMatch')
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Note: Home is NOT included in semantic breadcrumb
|
|
71
|
+
// It's a display concern - add it in useBreadcrumb if needed
|
|
72
|
+
|
|
73
|
+
const segments = path.split('/').filter(Boolean)
|
|
74
|
+
let currentPath = ''
|
|
75
|
+
let lastEntity: string | null = null
|
|
76
|
+
let pendingId: string | null = null // Track unmatched segments that could be IDs
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < segments.length; i++) {
|
|
79
|
+
const segment = segments[i]
|
|
80
|
+
const nextSegment = segments[i + 1]
|
|
81
|
+
if (!segment) continue
|
|
82
|
+
currentPath += `/${segment}`
|
|
83
|
+
|
|
84
|
+
// Check if this is an action segment
|
|
85
|
+
const actionKind = ACTION_MAP[segment.toLowerCase()]
|
|
86
|
+
if (actionKind && lastEntity) {
|
|
87
|
+
// If we have a pending ID, add a new entity item with action kind
|
|
88
|
+
if (pendingId) {
|
|
89
|
+
items.push({ kind: actionKind, entity: lastEntity, id: pendingId })
|
|
90
|
+
pendingId = null
|
|
91
|
+
} else {
|
|
92
|
+
// No pending ID - update last item's kind (e.g., /books/create)
|
|
93
|
+
const lastItem = items[items.length - 1]
|
|
94
|
+
if (lastItem && lastItem.kind.startsWith('entity-') && !lastItem.id) {
|
|
95
|
+
lastItem.kind = actionKind
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Find matching route
|
|
102
|
+
const matchedRoute = validRoutes.find((r) => {
|
|
103
|
+
if (r.path === currentPath) return true
|
|
104
|
+
const routeSegs = r.path.split('/').filter(Boolean)
|
|
105
|
+
const pathSegs = currentPath.split('/').filter(Boolean)
|
|
106
|
+
if (routeSegs.length !== pathSegs.length) return false
|
|
107
|
+
return routeSegs.every((rs, idx) => rs.startsWith(':') || rs === pathSegs[idx])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (!matchedRoute) {
|
|
111
|
+
// No route found - this segment could be an ID (e.g., /books/1 when route is /books/:id/edit)
|
|
112
|
+
if (lastEntity && segment) {
|
|
113
|
+
pendingId = segment
|
|
114
|
+
}
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entity = matchedRoute.meta?.entity as string | null
|
|
119
|
+
const isParam = matchedRoute.path
|
|
120
|
+
.split('/')
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.some((s, idx) => {
|
|
123
|
+
const pathSegs = currentPath.split('/').filter(Boolean)
|
|
124
|
+
return s.startsWith(':') && idx === pathSegs.length - 1
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (entity) {
|
|
128
|
+
lastEntity = entity
|
|
129
|
+
pendingId = null // Clear pending ID when we match a route
|
|
130
|
+
if (isParam) {
|
|
131
|
+
// Entity instance - get param value (the ID)
|
|
132
|
+
const paramValue = segment
|
|
133
|
+
// Determine kind based on next segment or route name
|
|
134
|
+
let kind: SemanticBreadcrumbKind = 'entity-show'
|
|
135
|
+
const nextAction = nextSegment ? ACTION_MAP[nextSegment.toLowerCase()] : undefined
|
|
136
|
+
if (nextAction) {
|
|
137
|
+
kind = nextAction
|
|
138
|
+
} else if ((matchedRoute.name as string)?.includes('edit')) {
|
|
139
|
+
kind = 'entity-edit'
|
|
140
|
+
}
|
|
141
|
+
items.push({ kind, entity, id: paramValue, route: matchedRoute.name as string })
|
|
142
|
+
} else {
|
|
143
|
+
// Entity list - include route name for child routes (e.g., 'bot-command' vs 'commands')
|
|
144
|
+
items.push({ kind: 'entity-list', entity, route: matchedRoute.name as string })
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// Flush pending ID as entity-show before generic route
|
|
148
|
+
if (pendingId && lastEntity) {
|
|
149
|
+
items.push({ kind: 'entity-show', entity: lastEntity, id: pendingId })
|
|
150
|
+
pendingId = null
|
|
151
|
+
}
|
|
152
|
+
// Generic route
|
|
153
|
+
items.push({ kind: 'route', route: (matchedRoute.name as string) || segment })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return items
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Return type for useSemanticBreadcrumb
|
|
162
|
+
*/
|
|
163
|
+
export interface UseSemanticBreadcrumbReturn {
|
|
164
|
+
breadcrumb: ComputedRef<SemanticBreadcrumbItem[]>
|
|
165
|
+
isEntityActive: (entity: string) => boolean
|
|
166
|
+
activeEntity: ComputedRef<string | null>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* useSemanticBreadcrumb - Vue composable for semantic breadcrumb
|
|
171
|
+
*
|
|
172
|
+
* Returns semantic objects per level - adapters resolve labels/paths for display.
|
|
173
|
+
*
|
|
174
|
+
* Kinds:
|
|
175
|
+
* - route: Generic route (e.g., home, dashboard)
|
|
176
|
+
* - entity-list: Entity collection (e.g., /books)
|
|
177
|
+
* - entity-show: Entity instance view (e.g., /books/1)
|
|
178
|
+
* - entity-edit: Entity instance edit (e.g., /books/1/edit)
|
|
179
|
+
* - entity-create: Entity creation (e.g., /books/create)
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* const { breadcrumb } = useSemanticBreadcrumb()
|
|
183
|
+
* // For /books/1/edit returns:
|
|
184
|
+
* // [
|
|
185
|
+
* // { kind: 'route', route: 'home' },
|
|
186
|
+
* // { kind: 'entity-list', entity: 'books' },
|
|
187
|
+
* // { kind: 'entity-edit', entity: 'books', id: '1' }
|
|
188
|
+
* // ]
|
|
189
|
+
*/
|
|
190
|
+
export function useSemanticBreadcrumb(): UseSemanticBreadcrumbReturn {
|
|
191
|
+
const route = useRoute()
|
|
192
|
+
const router = useRouter()
|
|
193
|
+
|
|
194
|
+
const breadcrumb = computed(() => {
|
|
195
|
+
return computeSemanticBreadcrumb(route.path, router.getRoutes(), route)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if an entity is active (present in current breadcrumb)
|
|
200
|
+
* Useful for menu highlighting
|
|
201
|
+
*
|
|
202
|
+
* @param entity - Entity name to check
|
|
203
|
+
* @returns True if entity is in breadcrumb
|
|
204
|
+
*/
|
|
205
|
+
function isEntityActive(entity: string): boolean {
|
|
206
|
+
return breadcrumb.value.some((item) => item.kind.startsWith('entity-') && item.entity === entity)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the active entity from breadcrumb (first entity found)
|
|
211
|
+
*/
|
|
212
|
+
const activeEntity = computed(() => {
|
|
213
|
+
const entityItem = breadcrumb.value.find((item) => item.kind.startsWith('entity-'))
|
|
214
|
+
return entityItem?.entity || null
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
breadcrumb,
|
|
219
|
+
isEntityActive,
|
|
220
|
+
activeEntity,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSidebarState - Access sidebar collapsed state from any component
|
|
3
|
+
*
|
|
4
|
+
* The sidebar collapsed ref is provided by AppLayout via Vue's provide/inject.
|
|
5
|
+
* This composable gives blocks rendered inside the sidebar (including zone blocks)
|
|
6
|
+
* programmatic access to the collapsed state.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { useSidebarState } from '@quazardous/qdadm'
|
|
11
|
+
*
|
|
12
|
+
* const { collapsed } = useSidebarState()
|
|
13
|
+
* // collapsed.value === true when sidebar is in icon-only mode
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { inject, ref, type Ref } from 'vue'
|
|
18
|
+
|
|
19
|
+
export const SIDEBAR_COLLAPSED_KEY = Symbol('qdadm-sidebar-collapsed') as symbol & { __type: Ref<boolean> }
|
|
20
|
+
|
|
21
|
+
export function useSidebarState(): { collapsed: Ref<boolean> } {
|
|
22
|
+
const collapsed = inject<Ref<boolean>>(SIDEBAR_COLLAPSED_KEY, ref(false))
|
|
23
|
+
return { collapsed }
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignals - Access the signal bus for event-driven communication
|
|
3
|
+
*
|
|
4
|
+
* Provides access to the SignalBus created by Kernel during bootstrap.
|
|
5
|
+
* Components can subscribe to signals without direct imports.
|
|
6
|
+
*
|
|
7
|
+
* @returns The signal bus instance or null if not available
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Basic usage
|
|
11
|
+
* const signals = useSignals()
|
|
12
|
+
* signals.on('entity:created', (payload) => {
|
|
13
|
+
* console.log('Entity created:', payload.entity, payload.data)
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Subscribe to specific entity
|
|
18
|
+
* const signals = useSignals()
|
|
19
|
+
* signals.on('books:updated', ({ data }) => {
|
|
20
|
+
* refreshBookList()
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Wildcard subscription
|
|
25
|
+
* const signals = useSignals()
|
|
26
|
+
* signals.on('*:deleted', ({ entity, data }) => {
|
|
27
|
+
* showDeletedNotification(entity)
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // Auto-cleanup on unmount
|
|
32
|
+
* import { onUnmounted } from 'vue'
|
|
33
|
+
*
|
|
34
|
+
* const signals = useSignals()
|
|
35
|
+
* const unbind = signals.on('entity:created', handler)
|
|
36
|
+
* onUnmounted(() => unbind())
|
|
37
|
+
*/
|
|
38
|
+
import { inject } from 'vue'
|
|
39
|
+
import type { SignalBus } from '../kernel/SignalBus'
|
|
40
|
+
|
|
41
|
+
export function useSignals(): SignalBus | null {
|
|
42
|
+
const signals = inject<SignalBus | null>('qdadmSignals', null)
|
|
43
|
+
|
|
44
|
+
if (!signals) {
|
|
45
|
+
console.warn(
|
|
46
|
+
'[qdadm] useSignals: signal bus not available. Ensure Kernel is initialized.'
|
|
47
|
+
)
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return signals
|
|
52
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable for guarding against unsaved changes when leaving a page
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const { dirty, takeSnapshot, checkDirty } = useDirtyState(...)
|
|
6
|
+
* useUnsavedChangesGuard(dirty, {
|
|
7
|
+
* onSave: () => handleSubmit(false),
|
|
8
|
+
* message: 'You have unsaved changes.'
|
|
9
|
+
* })
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { onBeforeUnmount, ref, type Ref } from 'vue'
|
|
13
|
+
import { onBeforeRouteLeave, type NavigationGuardNext } from 'vue-router'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Guard dialog state
|
|
17
|
+
*/
|
|
18
|
+
export interface GuardDialogState {
|
|
19
|
+
visible: Ref<boolean>
|
|
20
|
+
saving: Ref<boolean>
|
|
21
|
+
message: string
|
|
22
|
+
hasOnSave: boolean
|
|
23
|
+
onSaveAndLeave: () => Promise<void>
|
|
24
|
+
onLeave: () => void
|
|
25
|
+
onStay: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for useUnsavedChangesGuard
|
|
30
|
+
*/
|
|
31
|
+
export interface UseUnsavedChangesGuardOptions {
|
|
32
|
+
/** Async function to save changes */
|
|
33
|
+
onSave?: (() => Promise<void>) | null
|
|
34
|
+
/** Custom message */
|
|
35
|
+
message?: string
|
|
36
|
+
/** Show native browser dialog on tab close (default: false) */
|
|
37
|
+
browserGuard?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Return type for useUnsavedChangesGuard
|
|
42
|
+
*/
|
|
43
|
+
export interface UseUnsavedChangesGuardReturn {
|
|
44
|
+
dirty: Ref<boolean>
|
|
45
|
+
guardDialog: GuardDialogState
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Guard against leaving page with unsaved changes
|
|
50
|
+
*
|
|
51
|
+
* Shows a dialog with up to 3 options:
|
|
52
|
+
* - "Save & Leave" (if onSave provided) - saves then navigates
|
|
53
|
+
* - "Leave" - navigates without saving (discards changes)
|
|
54
|
+
* - "Stay" - cancels navigation
|
|
55
|
+
*
|
|
56
|
+
* @param dirty - Reactive dirty state
|
|
57
|
+
* @param options - Options
|
|
58
|
+
*/
|
|
59
|
+
export function useUnsavedChangesGuard(
|
|
60
|
+
dirty: Ref<boolean>,
|
|
61
|
+
options: UseUnsavedChangesGuardOptions = {}
|
|
62
|
+
): UseUnsavedChangesGuardReturn {
|
|
63
|
+
const {
|
|
64
|
+
onSave = null,
|
|
65
|
+
message = 'You have unsaved changes that will be lost.',
|
|
66
|
+
browserGuard = false, // Disabled by default - use soft JS modal only
|
|
67
|
+
} = options
|
|
68
|
+
|
|
69
|
+
// State for custom dialog
|
|
70
|
+
const showDialog = ref(false)
|
|
71
|
+
const pendingNext = ref<NavigationGuardNext | null>(null)
|
|
72
|
+
const saving = ref(false)
|
|
73
|
+
|
|
74
|
+
// Browser beforeunload event (for closing tab/window) - optional
|
|
75
|
+
function handleBeforeUnload(e: BeforeUnloadEvent): string | void {
|
|
76
|
+
if (dirty.value) {
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
e.returnValue = message
|
|
79
|
+
return message
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add browser event listener only if browserGuard is enabled
|
|
84
|
+
if (browserGuard && typeof window !== 'undefined') {
|
|
85
|
+
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Clean up on unmount
|
|
89
|
+
onBeforeUnmount(() => {
|
|
90
|
+
if (browserGuard && typeof window !== 'undefined') {
|
|
91
|
+
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Dialog actions
|
|
96
|
+
async function handleSaveAndLeave(): Promise<void> {
|
|
97
|
+
if (!onSave || !pendingNext.value) return
|
|
98
|
+
|
|
99
|
+
saving.value = true
|
|
100
|
+
try {
|
|
101
|
+
await onSave()
|
|
102
|
+
showDialog.value = false
|
|
103
|
+
pendingNext.value()
|
|
104
|
+
pendingNext.value = null
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error('Save failed:', e)
|
|
107
|
+
// Stay on page - don't close dialog, let user retry or choose Leave
|
|
108
|
+
} finally {
|
|
109
|
+
saving.value = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function handleLeave(): void {
|
|
114
|
+
if (!pendingNext.value) return
|
|
115
|
+
showDialog.value = false
|
|
116
|
+
pendingNext.value()
|
|
117
|
+
pendingNext.value = null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleStay(): void {
|
|
121
|
+
showDialog.value = false
|
|
122
|
+
if (pendingNext.value) {
|
|
123
|
+
pendingNext.value(false)
|
|
124
|
+
pendingNext.value = null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Vue Router navigation guard
|
|
129
|
+
onBeforeRouteLeave((_to, _from, next) => {
|
|
130
|
+
if (!dirty.value) {
|
|
131
|
+
next()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Store the next callback and show dialog
|
|
136
|
+
pendingNext.value = next
|
|
137
|
+
showDialog.value = true
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
dirty,
|
|
142
|
+
// Dialog state and handlers for custom dialog component
|
|
143
|
+
guardDialog: {
|
|
144
|
+
visible: showDialog,
|
|
145
|
+
saving,
|
|
146
|
+
message,
|
|
147
|
+
hasOnSave: !!onSave,
|
|
148
|
+
onSaveAndLeave: handleSaveAndLeave,
|
|
149
|
+
onLeave: handleLeave,
|
|
150
|
+
onStay: handleStay,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default useUnsavedChangesGuard
|