@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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAuth - Access authentication state (component-only)
|
|
3
|
+
*
|
|
4
|
+
* Provides access to authAdapter set via createQdadm bootstrap.
|
|
5
|
+
* Returns neutral values if auth is disabled.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Must be called within component setup() function.
|
|
8
|
+
* For route guards or services, use authAdapter directly.
|
|
9
|
+
*
|
|
10
|
+
* Reactivity:
|
|
11
|
+
* - Kernel.invalidateApp() remounts entire app on auth changes
|
|
12
|
+
* - Composable is re-initialized with fresh state
|
|
13
|
+
* - No signal listeners needed
|
|
14
|
+
*
|
|
15
|
+
* Note: Permission checking (canRead/canWrite) is handled by EntityManager,
|
|
16
|
+
* not by useAuth. This keeps auth simple and delegates permission logic
|
|
17
|
+
* to where it belongs (the entity layer).
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const { isAuthenticated, user, logout } = useAuth()
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { inject, computed, ref, getCurrentInstance, type ComputedRef, type Ref } from 'vue'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Features configuration interface
|
|
27
|
+
*/
|
|
28
|
+
interface FeaturesConfig {
|
|
29
|
+
auth?: boolean
|
|
30
|
+
[key: string]: unknown
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Auth adapter interface
|
|
35
|
+
*/
|
|
36
|
+
interface AuthAdapter {
|
|
37
|
+
login: (credentials?: unknown) => Promise<{ token: string | null; user: unknown }>
|
|
38
|
+
logout: () => void
|
|
39
|
+
getCurrentUser: () => Promise<unknown>
|
|
40
|
+
isAuthenticated: () => boolean
|
|
41
|
+
getUser?: () => unknown
|
|
42
|
+
user?: unknown
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return type for useAuth
|
|
47
|
+
*/
|
|
48
|
+
export interface UseAuthReturn {
|
|
49
|
+
login: (credentials?: unknown) => Promise<{ token: string | null; user: unknown }>
|
|
50
|
+
logout: () => void
|
|
51
|
+
getCurrentUser: () => Promise<unknown>
|
|
52
|
+
isAuthenticated: ComputedRef<boolean>
|
|
53
|
+
user: ComputedRef<unknown> | Ref<null>
|
|
54
|
+
authEnabled: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useAuth(): UseAuthReturn {
|
|
58
|
+
// Strict context check - must be called in component setup()
|
|
59
|
+
const instance = getCurrentInstance()
|
|
60
|
+
if (!instance) {
|
|
61
|
+
throw new Error('[qdadm] useAuth() must be called within component setup()')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const auth = inject<AuthAdapter | null>('authAdapter')
|
|
65
|
+
const features = inject<FeaturesConfig | null>('qdadmFeatures')
|
|
66
|
+
|
|
67
|
+
// If auth disabled or not provided, return neutral values
|
|
68
|
+
if (!features?.auth || !auth) {
|
|
69
|
+
return {
|
|
70
|
+
login: () => Promise.resolve({ token: null, user: null }),
|
|
71
|
+
logout: () => {},
|
|
72
|
+
getCurrentUser: () => Promise.resolve(null),
|
|
73
|
+
isAuthenticated: computed(() => true), // Always "authenticated"
|
|
74
|
+
user: ref(null),
|
|
75
|
+
authEnabled: false,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Note: Auth signal listeners removed - Kernel.invalidateApp() remounts entire app
|
|
80
|
+
// on auth changes, so composable is re-initialized with fresh state
|
|
81
|
+
|
|
82
|
+
// User state - reads current value from authAdapter
|
|
83
|
+
const user = computed(() => {
|
|
84
|
+
if (typeof auth.getUser === 'function') {
|
|
85
|
+
return auth.getUser()
|
|
86
|
+
}
|
|
87
|
+
return auth.user || null
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
login: auth.login,
|
|
92
|
+
logout: auth.logout,
|
|
93
|
+
getCurrentUser: auth.getCurrentUser,
|
|
94
|
+
isAuthenticated: computed(() => auth.isAuthenticated()),
|
|
95
|
+
user,
|
|
96
|
+
authEnabled: true,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBareForm - Base form composable providing common form functionality
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const {
|
|
6
|
+
* // State
|
|
7
|
+
* loading, saving, dirty, isEdit, entityId,
|
|
8
|
+
* // Dirty tracking
|
|
9
|
+
* isFieldDirty, takeSnapshot, checkDirty,
|
|
10
|
+
* // Helpers
|
|
11
|
+
* router, route, toast, cancel
|
|
12
|
+
* } = useBareForm({
|
|
13
|
+
* getState: () => ({ form: form.value }),
|
|
14
|
+
* routePrefix: 'agents', // for cancel() navigation
|
|
15
|
+
* entityName: 'Agent', // for auto-title (optional, or use 'entity' for auto-lookup)
|
|
16
|
+
* labelField: 'name', // field to use as entity label (optional)
|
|
17
|
+
* entity: 'agents', // EntityManager name for auto metadata (optional)
|
|
18
|
+
* guard: true, // enable unsaved changes modal
|
|
19
|
+
* onGuardSave: () => save() // optional save callback for guard modal
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* This composable provides:
|
|
23
|
+
* - Dirty state tracking (form-level and field-level)
|
|
24
|
+
* - isFieldDirty for child FormField components via inject
|
|
25
|
+
* - Unsaved changes guard modal
|
|
26
|
+
* - Common state refs (loading, saving)
|
|
27
|
+
* - Common computed (isEdit, entityId)
|
|
28
|
+
* - Navigation helpers (cancel)
|
|
29
|
+
* - Access to router, route, toast
|
|
30
|
+
* - Auto page title via provide (for PageHeader)
|
|
31
|
+
*/
|
|
32
|
+
import { ref, computed, provide, inject, onUnmounted, type Ref, type ComputedRef } from 'vue'
|
|
33
|
+
import { useRoute, useRouter, type Router, type RouteLocationNormalizedLoaded } from 'vue-router'
|
|
34
|
+
import { useDirtyState } from './useDirtyState'
|
|
35
|
+
import { useUnsavedChangesGuard, type GuardDialogState } from './useUnsavedChangesGuard'
|
|
36
|
+
import { useBreadcrumb, type BreadcrumbDisplayItem } from './useBreadcrumb'
|
|
37
|
+
import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Entity manager interface (minimal for this composable)
|
|
41
|
+
*/
|
|
42
|
+
interface EntityManager {
|
|
43
|
+
label?: string
|
|
44
|
+
labelField?: string
|
|
45
|
+
getEntityLabel: (data: unknown) => string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Orchestrator interface
|
|
50
|
+
*/
|
|
51
|
+
interface Orchestrator {
|
|
52
|
+
get: (entityName: string) => EntityManager
|
|
53
|
+
toast: {
|
|
54
|
+
success: (summary: string, detail?: string, emitter?: unknown) => void
|
|
55
|
+
error: (summary: string, detail?: string, emitter?: unknown) => void
|
|
56
|
+
warn: (summary: string, detail?: string, emitter?: unknown) => void
|
|
57
|
+
info: (summary: string, detail?: string, emitter?: unknown) => void
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Toast helper interface
|
|
63
|
+
*/
|
|
64
|
+
interface ToastHelper {
|
|
65
|
+
add: (options: {
|
|
66
|
+
severity: 'success' | 'error' | 'warn' | 'info'
|
|
67
|
+
summary: string
|
|
68
|
+
detail?: string
|
|
69
|
+
emitter?: unknown
|
|
70
|
+
}) => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Page title parts for PageHeader
|
|
75
|
+
*/
|
|
76
|
+
interface PageTitleParts {
|
|
77
|
+
action: string
|
|
78
|
+
entityName: string | null
|
|
79
|
+
entityLabel: string | null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Options for useBareForm
|
|
84
|
+
*/
|
|
85
|
+
export interface UseBareFormOptions {
|
|
86
|
+
/** Function returning current form state for comparison */
|
|
87
|
+
getState: () => Record<string, unknown>
|
|
88
|
+
/** Route name for cancel navigation (default: '') */
|
|
89
|
+
routePrefix?: string
|
|
90
|
+
/** Display name for entity (default: from manager or derived from routePrefix) */
|
|
91
|
+
entityName?: string | null
|
|
92
|
+
/** Field name or function to extract entity label (default: from manager or 'name') */
|
|
93
|
+
labelField?: string | ((data: unknown) => string) | null
|
|
94
|
+
/** EntityManager name (string) for auto metadata, OR entity data (Ref/Object) for breadcrumb */
|
|
95
|
+
entity?: string | Ref<unknown> | Record<string, unknown> | null
|
|
96
|
+
/** Enable unsaved changes guard (default: true) */
|
|
97
|
+
guard?: boolean
|
|
98
|
+
/** Callback for save button in guard modal */
|
|
99
|
+
onGuardSave?: (() => Promise<void>) | null
|
|
100
|
+
/** Custom function to extract entity ID from route (optional) */
|
|
101
|
+
getId?: (() => string | number | null) | null
|
|
102
|
+
/** Callback (entity) => string for custom breadcrumb label (optional) */
|
|
103
|
+
breadcrumbLabel?: ((entity: unknown) => string) | null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return type for useBareForm
|
|
108
|
+
*/
|
|
109
|
+
export interface UseBareFormReturn {
|
|
110
|
+
// Dependencies (for custom logic in form)
|
|
111
|
+
router: Router
|
|
112
|
+
route: RouteLocationNormalizedLoaded
|
|
113
|
+
toast: ToastHelper
|
|
114
|
+
|
|
115
|
+
// Manager (if resolved from entity string)
|
|
116
|
+
manager: EntityManager | null
|
|
117
|
+
|
|
118
|
+
// State
|
|
119
|
+
loading: Ref<boolean>
|
|
120
|
+
saving: Ref<boolean>
|
|
121
|
+
dirty: Ref<boolean>
|
|
122
|
+
dirtyFields: Ref<string[]>
|
|
123
|
+
isEdit: ComputedRef<boolean>
|
|
124
|
+
entityId: ComputedRef<string | number | null>
|
|
125
|
+
|
|
126
|
+
// Dirty tracking
|
|
127
|
+
isFieldDirty: (fieldPath: string) => boolean
|
|
128
|
+
takeSnapshot: () => void
|
|
129
|
+
checkDirty: () => void
|
|
130
|
+
reset: () => void
|
|
131
|
+
|
|
132
|
+
// Helpers
|
|
133
|
+
cancel: () => void
|
|
134
|
+
|
|
135
|
+
// Breadcrumb
|
|
136
|
+
breadcrumb: ComputedRef<BreadcrumbDisplayItem[]>
|
|
137
|
+
|
|
138
|
+
// Guard dialog (for UnsavedChangesDialog component)
|
|
139
|
+
guardDialog: GuardDialogState | null
|
|
140
|
+
|
|
141
|
+
// Title helpers
|
|
142
|
+
pageTitleParts: ComputedRef<PageTitleParts>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Base form composable providing common form functionality
|
|
147
|
+
*
|
|
148
|
+
* @param options - Configuration options
|
|
149
|
+
* @returns Form utilities and state
|
|
150
|
+
*/
|
|
151
|
+
export function useBareForm(options: UseBareFormOptions): UseBareFormReturn {
|
|
152
|
+
const {
|
|
153
|
+
getState,
|
|
154
|
+
routePrefix = '',
|
|
155
|
+
entityName = null,
|
|
156
|
+
labelField = null,
|
|
157
|
+
guard = true,
|
|
158
|
+
onGuardSave = null,
|
|
159
|
+
getId = null,
|
|
160
|
+
entity = null,
|
|
161
|
+
breadcrumbLabel = null,
|
|
162
|
+
} = options
|
|
163
|
+
|
|
164
|
+
if (!getState || typeof getState !== 'function') {
|
|
165
|
+
throw new Error('useBareForm requires a getState function')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Try to get EntityManager metadata if entity is a string
|
|
169
|
+
let manager: EntityManager | null = null
|
|
170
|
+
if (typeof entity === 'string') {
|
|
171
|
+
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
|
|
172
|
+
if (orchestrator) {
|
|
173
|
+
try {
|
|
174
|
+
manager = orchestrator.get(entity)
|
|
175
|
+
} catch {
|
|
176
|
+
// Manager not found, continue without it
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Router, route, toast - common dependencies
|
|
182
|
+
const router = useRouter()
|
|
183
|
+
const route = useRoute()
|
|
184
|
+
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
|
|
185
|
+
|
|
186
|
+
// Toast helper - wraps orchestrator.toast for legacy compatibility
|
|
187
|
+
const toast: ToastHelper = {
|
|
188
|
+
add({ severity, summary, detail, emitter }) {
|
|
189
|
+
orchestrator?.toast[severity]?.(summary, detail, emitter)
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Common state
|
|
194
|
+
const loading = ref(false)
|
|
195
|
+
const saving = ref(false)
|
|
196
|
+
|
|
197
|
+
// Common computed
|
|
198
|
+
const entityId = computed(() => {
|
|
199
|
+
if (getId) return getId()
|
|
200
|
+
return (route.params.id as string | number) || (route.params.key as string | number) || null
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const isEdit = computed(() => !!entityId.value)
|
|
204
|
+
|
|
205
|
+
// Dirty state tracking
|
|
206
|
+
const { dirty, dirtyFields, isFieldDirty, takeSnapshot, checkDirty, reset } =
|
|
207
|
+
useDirtyState(getState)
|
|
208
|
+
|
|
209
|
+
// Provide isFieldDirty and dirtyFields for child components (FormField)
|
|
210
|
+
provide('isFieldDirty', isFieldDirty)
|
|
211
|
+
provide('dirtyFields', dirtyFields)
|
|
212
|
+
|
|
213
|
+
// Resolve entityName: explicit > manager > derived from routePrefix
|
|
214
|
+
const derivedEntityName = routePrefix
|
|
215
|
+
? routePrefix.charAt(0).toUpperCase() + routePrefix.slice(1).replace(/s$/, '')
|
|
216
|
+
: null
|
|
217
|
+
const effectiveEntityName = entityName || manager?.label || derivedEntityName
|
|
218
|
+
|
|
219
|
+
// Resolve labelField: explicit > manager > default 'name'
|
|
220
|
+
const effectiveLabelField = labelField || manager?.labelField || 'name'
|
|
221
|
+
|
|
222
|
+
// Provide entity context for child components (e.g., SeverityTag auto-discovery)
|
|
223
|
+
if (routePrefix) {
|
|
224
|
+
const entityNameForProvider = routePrefix.endsWith('s') ? routePrefix : routePrefix + 's'
|
|
225
|
+
provide('mainEntity', entityNameForProvider)
|
|
226
|
+
} else if (typeof entity === 'string') {
|
|
227
|
+
provide('mainEntity', entity)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Auto page title parts for PageHeader
|
|
231
|
+
const getEntityLabel = (): string | null => {
|
|
232
|
+
const state = getState()
|
|
233
|
+
const formData = (state.form as Record<string, unknown>) || state
|
|
234
|
+
if (!formData) return null
|
|
235
|
+
// Use manager.getEntityLabel if available, otherwise use effectiveLabelField
|
|
236
|
+
if (manager) {
|
|
237
|
+
return manager.getEntityLabel(formData)
|
|
238
|
+
}
|
|
239
|
+
if (typeof effectiveLabelField === 'function') {
|
|
240
|
+
return effectiveLabelField(formData)
|
|
241
|
+
}
|
|
242
|
+
return (formData[effectiveLabelField as string] as string) || null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const pageTitleParts = computed<PageTitleParts>(() => ({
|
|
246
|
+
action: isEdit.value ? 'Edit' : 'Create',
|
|
247
|
+
entityName: effectiveEntityName,
|
|
248
|
+
entityLabel: isEdit.value ? getEntityLabel() : null,
|
|
249
|
+
}))
|
|
250
|
+
|
|
251
|
+
// Provide title parts for automatic PageHeader consumption
|
|
252
|
+
if (effectiveEntityName) {
|
|
253
|
+
provide('qdadmPageTitleParts', pageTitleParts)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Breadcrumb (auto-generated from route path, with optional entity for dynamic labels)
|
|
257
|
+
// Only pass entity to breadcrumb if it's actual entity data (not a string manager name)
|
|
258
|
+
const breadcrumbEntity = typeof entity === 'string' ? null : entity
|
|
259
|
+
const { breadcrumbItems } = useBreadcrumb({
|
|
260
|
+
entity: breadcrumbEntity as unknown | Ref<unknown> | undefined,
|
|
261
|
+
getEntityLabel: breadcrumbLabel || undefined,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// Unsaved changes guard
|
|
265
|
+
let guardDialog: GuardDialogState | null = null
|
|
266
|
+
if (guard) {
|
|
267
|
+
const guardOptions = onGuardSave ? { onSave: onGuardSave } : {}
|
|
268
|
+
const guardResult = useUnsavedChangesGuard(dirty, guardOptions)
|
|
269
|
+
guardDialog = guardResult.guardDialog
|
|
270
|
+
// Register guardDialog in shared store so AppLayout can render it
|
|
271
|
+
registerGuardDialog(guardDialog)
|
|
272
|
+
onUnmounted(() => unregisterGuardDialog(guardDialog!))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Navigation helper
|
|
276
|
+
function cancel(): void {
|
|
277
|
+
if (routePrefix) {
|
|
278
|
+
router.push({ name: routePrefix })
|
|
279
|
+
} else {
|
|
280
|
+
router.back()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
// Dependencies (for custom logic in form)
|
|
286
|
+
router,
|
|
287
|
+
route,
|
|
288
|
+
toast,
|
|
289
|
+
|
|
290
|
+
// Manager (if resolved from entity string)
|
|
291
|
+
manager,
|
|
292
|
+
|
|
293
|
+
// State
|
|
294
|
+
loading,
|
|
295
|
+
saving,
|
|
296
|
+
dirty,
|
|
297
|
+
dirtyFields,
|
|
298
|
+
isEdit,
|
|
299
|
+
entityId,
|
|
300
|
+
|
|
301
|
+
// Dirty tracking
|
|
302
|
+
isFieldDirty,
|
|
303
|
+
takeSnapshot,
|
|
304
|
+
checkDirty,
|
|
305
|
+
reset,
|
|
306
|
+
|
|
307
|
+
// Helpers
|
|
308
|
+
cancel,
|
|
309
|
+
|
|
310
|
+
// Breadcrumb
|
|
311
|
+
breadcrumb: breadcrumbItems,
|
|
312
|
+
|
|
313
|
+
// Guard dialog (for UnsavedChangesDialog component)
|
|
314
|
+
guardDialog,
|
|
315
|
+
|
|
316
|
+
// Title helpers
|
|
317
|
+
pageTitleParts,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { computed, inject, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
import { useSemanticBreadcrumb, type SemanticBreadcrumbItem } from './useSemanticBreadcrumb'
|
|
4
|
+
import { useStackHydrator, type HydratedLevel } from '../chain/useStackHydrator'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Breadcrumb display item
|
|
8
|
+
*/
|
|
9
|
+
export interface BreadcrumbDisplayItem {
|
|
10
|
+
label: string
|
|
11
|
+
to: { name: string; params?: Record<string, unknown> } | null
|
|
12
|
+
icon: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for useBreadcrumb
|
|
17
|
+
*/
|
|
18
|
+
export interface UseBreadcrumbOptions {
|
|
19
|
+
/** Current entity data for dynamic labels */
|
|
20
|
+
entity?: unknown | Ref<unknown>
|
|
21
|
+
/** Custom label resolver for entities */
|
|
22
|
+
getEntityLabel?: (entity: unknown) => string
|
|
23
|
+
/** Custom label mappings */
|
|
24
|
+
labelMap?: Record<string, string>
|
|
25
|
+
/** Custom icon mappings */
|
|
26
|
+
iconMap?: Record<string, string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return type for useBreadcrumb
|
|
31
|
+
*/
|
|
32
|
+
export interface UseBreadcrumbReturn {
|
|
33
|
+
breadcrumbItems: ComputedRef<BreadcrumbDisplayItem[]>
|
|
34
|
+
semanticBreadcrumb: ComputedRef<SemanticBreadcrumbItem[]>
|
|
35
|
+
setBreadcrumb: (items: BreadcrumbDisplayItem[]) => BreadcrumbDisplayItem[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* useBreadcrumb - Generate display-ready breadcrumb from semantic breadcrumb
|
|
40
|
+
*
|
|
41
|
+
* Uses useSemanticBreadcrumb for the semantic model, then applies adapters
|
|
42
|
+
* to resolve labels, paths, and icons for display.
|
|
43
|
+
*
|
|
44
|
+
* @param options - Options
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const { breadcrumbItems, semanticBreadcrumb } = useBreadcrumb()
|
|
48
|
+
*
|
|
49
|
+
* // With entity data for dynamic labels
|
|
50
|
+
* const { breadcrumbItems } = useBreadcrumb({ entity: bookData })
|
|
51
|
+
*/
|
|
52
|
+
export function useBreadcrumb(options: UseBreadcrumbOptions = {}): UseBreadcrumbReturn {
|
|
53
|
+
const router = useRouter()
|
|
54
|
+
const homeRouteName = inject<string | null>('qdadmHomeRoute', null)
|
|
55
|
+
|
|
56
|
+
// Get semantic breadcrumb
|
|
57
|
+
const { breadcrumb: semanticBreadcrumb } = useSemanticBreadcrumb()
|
|
58
|
+
|
|
59
|
+
// Get hydrator for parent entity labels
|
|
60
|
+
const hydrator = useStackHydrator()
|
|
61
|
+
|
|
62
|
+
// Default label mapping
|
|
63
|
+
const defaultLabelMap: Record<string, string> = {
|
|
64
|
+
home: 'Home',
|
|
65
|
+
dashboard: 'Dashboard',
|
|
66
|
+
create: 'Create',
|
|
67
|
+
edit: 'Edit',
|
|
68
|
+
show: 'View',
|
|
69
|
+
delete: 'Delete',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Default icon mapping for entities
|
|
73
|
+
const defaultIconMap: Record<string, string> = {
|
|
74
|
+
users: 'pi pi-users',
|
|
75
|
+
roles: 'pi pi-shield',
|
|
76
|
+
books: 'pi pi-book',
|
|
77
|
+
genres: 'pi pi-tags',
|
|
78
|
+
loans: 'pi pi-exchange',
|
|
79
|
+
settings: 'pi pi-cog',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Merge with custom mappings
|
|
83
|
+
const labelMap = { ...defaultLabelMap, ...options.labelMap }
|
|
84
|
+
const iconMap = { ...defaultIconMap, ...options.iconMap }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Capitalize first letter
|
|
88
|
+
*/
|
|
89
|
+
function capitalize(str: string): string {
|
|
90
|
+
if (!str) return ''
|
|
91
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get route name for entity
|
|
96
|
+
*/
|
|
97
|
+
function getEntityRouteName(entity: string, kind: string): string {
|
|
98
|
+
// Convention: entity-list -> 'book', entity-edit -> 'book-edit'
|
|
99
|
+
const singular = entity.endsWith('s') ? entity.slice(0, -1) : entity
|
|
100
|
+
if (kind === 'entity-list') return singular
|
|
101
|
+
if (kind === 'entity-show') return `${singular}-show`
|
|
102
|
+
if (kind === 'entity-edit') return `${singular}-edit`
|
|
103
|
+
if (kind === 'entity-create') return `${singular}-create`
|
|
104
|
+
return singular
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a route exists
|
|
109
|
+
*/
|
|
110
|
+
function routeExists(name: string): boolean {
|
|
111
|
+
return router.getRoutes().some((r) => r.name === name)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve semantic item to display item
|
|
116
|
+
* @param item - Semantic breadcrumb item
|
|
117
|
+
* @param index - Index in the breadcrumb array
|
|
118
|
+
* @param entity - Current entity data (for labels)
|
|
119
|
+
* @param isLast - Whether this is the last item (no link)
|
|
120
|
+
*/
|
|
121
|
+
function resolveItem(
|
|
122
|
+
item: SemanticBreadcrumbItem,
|
|
123
|
+
index: number,
|
|
124
|
+
entity: unknown,
|
|
125
|
+
isLast: boolean
|
|
126
|
+
): BreadcrumbDisplayItem {
|
|
127
|
+
const displayItem: BreadcrumbDisplayItem = {
|
|
128
|
+
label: '',
|
|
129
|
+
to: null,
|
|
130
|
+
icon: null,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (item.kind === 'route') {
|
|
134
|
+
// Generic route
|
|
135
|
+
const routeName = item.route!
|
|
136
|
+
displayItem.label = item.label || labelMap[routeName] || capitalize(routeName)
|
|
137
|
+
displayItem.to = isLast ? null : routeExists(routeName) ? { name: routeName } : null
|
|
138
|
+
if (routeName === 'home') {
|
|
139
|
+
displayItem.icon = 'pi pi-home'
|
|
140
|
+
}
|
|
141
|
+
} else if (item.kind.startsWith('entity-')) {
|
|
142
|
+
// Entity-related item
|
|
143
|
+
const entityName = item.entity!
|
|
144
|
+
|
|
145
|
+
if (item.kind === 'entity-list') {
|
|
146
|
+
// Entity list - use plural label
|
|
147
|
+
displayItem.label = labelMap[entityName] || capitalize(entityName)
|
|
148
|
+
displayItem.icon = index === 1 ? iconMap[entityName] || null : null
|
|
149
|
+
const routeName = getEntityRouteName(entityName, item.kind)
|
|
150
|
+
displayItem.to = isLast ? null : routeExists(routeName) ? { name: routeName } : null
|
|
151
|
+
} else if (item.id) {
|
|
152
|
+
// Entity instance (show/edit/delete)
|
|
153
|
+
// Try to get label from hydrator first (works for all levels)
|
|
154
|
+
const hydratedLevel = hydrator.getLevelByEntity(entityName)
|
|
155
|
+
if (hydratedLevel?.label) {
|
|
156
|
+
displayItem.label = hydratedLevel.label
|
|
157
|
+
} else if (entity && options.getEntityLabel) {
|
|
158
|
+
displayItem.label = options.getEntityLabel(entity)
|
|
159
|
+
} else if (entity && typeof entity === 'object' && (entity as Record<string, unknown>).name) {
|
|
160
|
+
displayItem.label = String((entity as Record<string, unknown>).name)
|
|
161
|
+
} else if (
|
|
162
|
+
entity &&
|
|
163
|
+
typeof entity === 'object' &&
|
|
164
|
+
(entity as Record<string, unknown>).title
|
|
165
|
+
) {
|
|
166
|
+
displayItem.label = String((entity as Record<string, unknown>).title)
|
|
167
|
+
} else {
|
|
168
|
+
displayItem.label = `#${item.id}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Last item = no link. Otherwise link to show page if exists
|
|
172
|
+
if (!isLast && (item.kind === 'entity-edit' || item.kind === 'entity-delete')) {
|
|
173
|
+
const showRouteName = getEntityRouteName(entityName, 'entity-show')
|
|
174
|
+
if (routeExists(showRouteName)) {
|
|
175
|
+
displayItem.to = { name: showRouteName, params: { id: item.id } }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else if (item.kind === 'entity-create') {
|
|
179
|
+
// Create page - label as action
|
|
180
|
+
displayItem.label = labelMap.create || 'Create'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return displayItem
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get home breadcrumb item
|
|
189
|
+
*/
|
|
190
|
+
function getHomeItem(): BreadcrumbDisplayItem | null {
|
|
191
|
+
if (!homeRouteName || !routeExists(homeRouteName)) return null
|
|
192
|
+
return {
|
|
193
|
+
label: labelMap.home || 'Home',
|
|
194
|
+
to: { name: homeRouteName },
|
|
195
|
+
icon: 'pi pi-home',
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Computed display-ready breadcrumb items
|
|
201
|
+
*/
|
|
202
|
+
const breadcrumbItems = computed(() => {
|
|
203
|
+
const entity =
|
|
204
|
+
(options.entity as Ref<unknown>)?.value !== undefined
|
|
205
|
+
? (options.entity as Ref<unknown>).value
|
|
206
|
+
: options.entity
|
|
207
|
+
const items: BreadcrumbDisplayItem[] = []
|
|
208
|
+
|
|
209
|
+
// Access hydrator levels deeply to create Vue dependencies
|
|
210
|
+
// This ensures breadcrumb recomputes when hydration completes
|
|
211
|
+
// We need to touch each level's label to track changes
|
|
212
|
+
const _hydratorLevels = hydrator.levels.value
|
|
213
|
+
// Touch labels to track changes (result intentionally unused for reactivity)
|
|
214
|
+
_hydratorLevels.map((l: HydratedLevel) => l.label).join()
|
|
215
|
+
|
|
216
|
+
// Add home first (display concern, not in semantic breadcrumb)
|
|
217
|
+
const homeItem = getHomeItem()
|
|
218
|
+
if (homeItem) {
|
|
219
|
+
items.push(homeItem)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Resolve semantic items to display items
|
|
223
|
+
const semanticItems = semanticBreadcrumb.value
|
|
224
|
+
for (let i = 0; i < semanticItems.length; i++) {
|
|
225
|
+
const semanticItem = semanticItems[i]
|
|
226
|
+
if (!semanticItem) continue
|
|
227
|
+
const isLast = i === semanticItems.length - 1
|
|
228
|
+
const displayItem = resolveItem(semanticItem, i, entity, isLast)
|
|
229
|
+
items.push(displayItem)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return items
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Manual override - set custom breadcrumb items
|
|
237
|
+
*/
|
|
238
|
+
function setBreadcrumb(items: BreadcrumbDisplayItem[]): BreadcrumbDisplayItem[] {
|
|
239
|
+
const homeItem: BreadcrumbDisplayItem = {
|
|
240
|
+
label: labelMap.home || 'Home',
|
|
241
|
+
to: { name: homeRouteName! },
|
|
242
|
+
icon: 'pi pi-home',
|
|
243
|
+
}
|
|
244
|
+
return homeRouteName && routeExists(homeRouteName) ? [homeItem, ...items] : items
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
breadcrumbItems,
|
|
249
|
+
semanticBreadcrumb,
|
|
250
|
+
setBreadcrumb,
|
|
251
|
+
}
|
|
252
|
+
}
|