qdadm 0.13.0 → 0.14.1

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/README.md CHANGED
@@ -1,14 +1,10 @@
1
1
  # qdadm
2
2
 
3
- Vue 3 framework for building admin dashboards with PrimeVue.
3
+ **Vue 3 admin framework. PrimeVue. Zero boilerplate.**
4
4
 
5
- ## Features
5
+ Full documentation: [../../README.md](../../README.md)
6
6
 
7
- - **Kernel**: All-in-one bootstrap (Vue app, router, Pinia, PrimeVue, auth guard)
8
- - **EntityManager**: CRUD operations with permission control (`canRead`/`canCreate`/`canUpdate`/`canDelete`)
9
- - **Module System**: Auto-discovery of modules with routes and navigation
10
- - **Components**: Forms, lists, dialogs, editors ready to use
11
- - **Composables**: `useForm`, `useListPageBuilder`, `useBareForm`, etc.
7
+ Changelog: [../../CHANGELOG.md](../../CHANGELOG.md)
12
8
 
13
9
  ## Installation
14
10
 
@@ -16,7 +12,7 @@ Vue 3 framework for building admin dashboards with PrimeVue.
16
12
  npm install qdadm
17
13
  ```
18
14
 
19
- ## Quick Start with Kernel
15
+ ## Quick Start
20
16
 
21
17
  ```js
22
18
  import { Kernel, EntityManager, LocalStorage } from 'qdadm'
@@ -24,135 +20,48 @@ import PrimeVue from 'primevue/config'
24
20
  import Aura from '@primeuix/themes/aura'
25
21
  import 'qdadm/styles'
26
22
 
27
- import App from './App.vue'
28
- import { authAdapter } from './adapters/authAdapter'
29
-
30
- const managers = {
31
- books: new EntityManager({
32
- name: 'books',
33
- storage: new LocalStorage({ key: 'my_books' }),
34
- fields: {
35
- title: { type: 'text', label: 'Title', required: true },
36
- author: { type: 'text', label: 'Author' }
37
- }
38
- })
39
- }
40
-
41
23
  const kernel = new Kernel({
42
24
  root: App,
43
25
  modules: import.meta.glob('./modules/*/init.js', { eager: true }),
44
- sectionOrder: ['Library'],
45
- managers,
46
- authAdapter,
47
- pages: {
48
- login: () => import('./pages/LoginPage.vue'),
49
- layout: () => import('./pages/MainLayout.vue')
26
+ managers: {
27
+ books: new EntityManager({
28
+ name: 'books',
29
+ storage: new LocalStorage({ key: 'books' }),
30
+ labelField: 'title'
31
+ })
50
32
  },
33
+ authAdapter,
34
+ pages: { login: LoginPage, layout: MainLayout },
51
35
  homeRoute: 'book',
52
- app: { name: 'My App', version: '1.0.0' },
36
+ app: { name: 'My App' },
53
37
  primevue: { plugin: PrimeVue, theme: Aura }
54
38
  })
55
39
 
56
40
  kernel.createApp().mount('#app')
57
41
  ```
58
42
 
59
- ## Manual Bootstrap (without Kernel)
43
+ ## Exports
60
44
 
61
45
  ```js
62
- import { createQdadm, initModules, getRoutes } from 'qdadm'
63
-
64
- // Init modules
65
- initModules(import.meta.glob('./modules/*/init.js', { eager: true }))
66
-
67
- // Create router with getRoutes()
68
- const router = createRouter({
69
- history: createWebHistory(),
70
- routes: [
71
- { path: '/login', component: LoginPage },
72
- { path: '/', component: Layout, children: getRoutes() }
73
- ]
74
- })
75
-
76
- // Install plugin
77
- app.use(createQdadm({ managers, authAdapter, router, toast }))
78
- ```
46
+ // Main
47
+ import { Kernel, createQdadm, EntityManager, ApiStorage, LocalStorage } from 'qdadm'
79
48
 
80
- ## Module Structure
49
+ // Composables
50
+ import { useForm, useBareForm, useListPageBuilder } from 'qdadm/composables'
81
51
 
82
- ```
83
- modules/
84
- └── books/
85
- ├── init.js # Route & nav registration
86
- └── pages/
87
- ├── BookList.vue
88
- └── BookForm.vue
89
- ```
52
+ // Components
53
+ import { ListPage, PageLayout, FormField, FormActions } from 'qdadm/components'
90
54
 
91
- **init.js:**
92
- ```js
93
- export function init(registry) {
94
- registry.addRoutes('books', [
95
- { path: '', name: 'book', component: () => import('./pages/BookList.vue') },
96
- { path: 'create', name: 'book-create', component: () => import('./pages/BookForm.vue') },
97
- { path: ':id/edit', name: 'book-edit', component: () => import('./pages/BookForm.vue') }
98
- ], { entity: 'books' })
99
-
100
- registry.addNavItem({
101
- section: 'Library',
102
- route: 'book',
103
- icon: 'pi pi-book',
104
- label: 'Books',
105
- entity: 'books'
106
- })
107
-
108
- registry.addRouteFamily('book', ['book-'])
109
- }
110
- ```
55
+ // Module system
56
+ import { initModules, getRoutes, setSectionOrder } from 'qdadm/module'
111
57
 
112
- ## EntityManager Permissions
58
+ // Utilities
59
+ import { formatDate, truncate } from 'qdadm/utils'
113
60
 
114
- ```js
115
- class UsersManager extends EntityManager {
116
- canRead() {
117
- return authAdapter.getUser()?.role === 'admin'
118
- }
119
- canCreate() {
120
- return authAdapter.getUser()?.role === 'admin'
121
- }
122
- canUpdate(entity) {
123
- return authAdapter.getUser()?.role === 'admin'
124
- }
125
- canDelete(entity) {
126
- return authAdapter.getUser()?.role === 'admin'
127
- }
128
- }
61
+ // Styles
62
+ import 'qdadm/styles'
129
63
  ```
130
64
 
131
- When `canRead()` returns false:
132
- - Navigation items are hidden
133
- - Routes redirect to `/`
134
-
135
- ## Components
136
-
137
- | Category | Components |
138
- |----------|------------|
139
- | Layout | `AppLayout`, `PageLayout`, `PageHeader`, `Breadcrumb` |
140
- | Forms | `FormField`, `FormActions`, `FormTabs`, `FormTab` |
141
- | Lists | `ListPage`, `ActionButtons`, `FilterBar` |
142
- | Editors | `JsonViewer`, `KeyValueEditor`, `VanillaJsonEditor` |
143
- | Dialogs | `SimpleDialog`, `MultiStepDialog`, `UnsavedChangesDialog` |
144
- | Display | `CardsGrid`, `CopyableId`, `EmptyState` |
145
-
146
- ## Composables
147
-
148
- | Composable | Description |
149
- |------------|-------------|
150
- | `useForm` | Form with validation, dirty state, navigation guard |
151
- | `useBareForm` | Lightweight form without routing |
152
- | `useListPageBuilder` | Paginated list with filters and actions |
153
- | `useTabSync` | Sync tabs with URL query params |
154
- | `useBreadcrumb` | Dynamic breadcrumb from route |
155
-
156
65
  ## Peer Dependencies
157
66
 
158
67
  - vue ^3.3.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -25,7 +25,6 @@
25
25
  "files": [
26
26
  "src",
27
27
  "README.md",
28
- "CHANGELOG.md",
29
28
  "LICENSE"
30
29
  ],
31
30
  "peerDependencies": {
@@ -0,0 +1,71 @@
1
+ <script setup>
2
+ /**
3
+ * SeverityTag - Auto-discovers severity from EntityManager
4
+ *
5
+ * Entity can be specified explicitly or auto-discovered from list context.
6
+ *
7
+ * @example
8
+ * // Explicit entity
9
+ * <SeverityTag entity="jobs" field="status" :value="job.status" />
10
+ *
11
+ * // Auto-discover from list context (inside ListPage)
12
+ * <SeverityTag field="status" :value="data.status" />
13
+ *
14
+ * // With custom label
15
+ * <SeverityTag entity="stories" field="status" :value="story.status" label="Published" />
16
+ */
17
+ import { computed, inject } from 'vue'
18
+ import Tag from 'primevue/tag'
19
+
20
+ const props = defineProps({
21
+ // Entity name (e.g., 'jobs', 'stories') - optional if inside ListPage context
22
+ entity: {
23
+ type: String,
24
+ default: null
25
+ },
26
+ // Field name for severity lookup (e.g., 'status')
27
+ field: {
28
+ type: String,
29
+ required: true
30
+ },
31
+ // Field value
32
+ value: {
33
+ type: [String, Number, Boolean],
34
+ default: null
35
+ },
36
+ // Optional custom label (defaults to value)
37
+ label: {
38
+ type: String,
39
+ default: null
40
+ },
41
+ // Default severity if no mapping found
42
+ defaultSeverity: {
43
+ type: String,
44
+ default: 'secondary'
45
+ }
46
+ })
47
+
48
+ const orchestrator = inject('qdadmOrchestrator')
49
+ // Auto-discover entity from page context (provided by useListPageBuilder, useBareForm, etc.)
50
+ const mainEntity = inject('mainEntity', null)
51
+
52
+ const resolvedEntity = computed(() => props.entity || mainEntity)
53
+
54
+ const manager = computed(() => {
55
+ if (!resolvedEntity.value) return null
56
+ return orchestrator?.get(resolvedEntity.value)
57
+ })
58
+
59
+ const severity = computed(() => {
60
+ if (!manager.value) return props.defaultSeverity
61
+ return manager.value.getSeverity(props.field, props.value, props.defaultSeverity)
62
+ })
63
+
64
+ const displayLabel = computed(() => {
65
+ return props.label ?? props.value
66
+ })
67
+ </script>
68
+
69
+ <template>
70
+ <Tag :value="displayLabel" :severity="severity" />
71
+ </template>
@@ -20,7 +20,7 @@ const props = defineProps({
20
20
  },
21
21
  scopePrefix: {
22
22
  type: String,
23
- default: 'faketual' // Prefix for scope strings (e.g., "faketual.resource:action")
23
+ default: 'app' // Prefix for scope strings (e.g., "app.resource:action")
24
24
  },
25
25
  // Default resources/actions if API not available
26
26
  defaultResources: {
@@ -42,3 +42,4 @@ export { default as CopyableId } from './display/CopyableId.vue'
42
42
  export { default as EmptyState } from './display/EmptyState.vue'
43
43
  export { default as IntensityBar } from './display/IntensityBar.vue'
44
44
  export { default as BoolCell } from './BoolCell.vue'
45
+ export { default as SeverityTag } from './SeverityTag.vue'
@@ -18,12 +18,14 @@ import { useNavigation } from '../../composables/useNavigation'
18
18
  import { useApp } from '../../composables/useApp'
19
19
  import { useAuth } from '../../composables/useAuth'
20
20
  import { useGuardDialog } from '../../composables/useGuardStore'
21
+ import { useBreadcrumb } from '../../composables/useBreadcrumb'
21
22
  import Button from 'primevue/button'
23
+ import Breadcrumb from 'primevue/breadcrumb'
22
24
  import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
23
25
  import qdadmLogo from '../../assets/logo.svg'
24
26
  import { version as qdadmVersion } from '../../../package.json'
25
27
 
26
- const features = inject('qdadmFeatures', { poweredBy: true })
28
+ const features = inject('qdadmFeatures', { poweredBy: true, breadcrumb: true })
27
29
 
28
30
  // Guard dialog from shared store (registered by useBareForm/useForm when a form is active)
29
31
  const guardDialog = useGuardDialog()
@@ -122,6 +124,16 @@ function handleLogout() {
122
124
  // Check if slot content is provided
123
125
  const slots = useSlots()
124
126
  const hasSlotContent = computed(() => !!slots.default)
127
+
128
+ // Breadcrumb (auto-generated from route)
129
+ const { breadcrumbItems } = useBreadcrumb()
130
+ // Show breadcrumb if enabled, has items, and not on home page
131
+ const showBreadcrumb = computed(() => {
132
+ if (!features.breadcrumb || breadcrumbItems.value.length === 0) return false
133
+ // Don't show on home page (just "Dashboard" with no parents)
134
+ if (route.name === 'dashboard') return false
135
+ return true
136
+ })
125
137
  </script>
126
138
 
127
139
  <template>
@@ -191,6 +203,20 @@ const hasSlotContent = computed(() => !!slots.default)
191
203
 
192
204
  <!-- Main content -->
193
205
  <main class="main-content">
206
+ <!-- Breadcrumb (auto-generated, can be disabled via features.breadcrumb: false) -->
207
+ <Breadcrumb v-if="showBreadcrumb" :model="breadcrumbItems" class="layout-breadcrumb">
208
+ <template #item="{ item }">
209
+ <RouterLink v-if="item.to" :to="item.to" class="breadcrumb-link">
210
+ <i v-if="item.icon" :class="item.icon"></i>
211
+ <span>{{ item.label }}</span>
212
+ </RouterLink>
213
+ <span v-else class="breadcrumb-current">
214
+ <i v-if="item.icon" :class="item.icon"></i>
215
+ <span>{{ item.label }}</span>
216
+ </span>
217
+ </template>
218
+ </Breadcrumb>
219
+
194
220
  <div class="page-content">
195
221
  <!-- Use slot if provided, otherwise RouterView for nested routes -->
196
222
  <slot v-if="hasSlotContent" />
@@ -390,6 +416,8 @@ const hasSlotContent = computed(() => !!slots.default)
390
416
  overflow-y: auto;
391
417
  }
392
418
 
419
+ /* Breadcrumb styles are in main.css */
420
+
393
421
  /* Dark mode support */
394
422
  .dark-mode .sidebar {
395
423
  background: var(--p-surface-900);
@@ -3,19 +3,39 @@
3
3
  * PageHeader - Reusable page header with breadcrumb, title and actions
4
4
  *
5
5
  * Props:
6
- * - title: Page title (required)
6
+ * - title: Page title (simple string) OR
7
+ * - titleParts: { action, entityName, entityLabel } for decorated rendering
7
8
  * - breadcrumb: Array of { label, to?, icon? } - optional breadcrumb items
8
9
  *
10
+ * Title rendering:
11
+ * - Simple: "Edit Agent" → <h1>Edit Agent</h1>
12
+ * - Decorated: { action: 'Edit', entityName: 'Agent', entityLabel: 'David' }
13
+ * → <h1>Edit Agent: <span class="entity-label">David</span></h1>
14
+ *
9
15
  * Slots:
10
16
  * - subtitle: Optional content next to title
11
17
  * - actions: Action buttons on the right
18
+ *
19
+ * Note: When features.breadcrumb is enabled in AppLayout, the layout handles
20
+ * breadcrumb rendering globally. PageHeader only renders its own breadcrumb
21
+ * when the global feature is disabled.
12
22
  */
23
+ import { computed, inject } from 'vue'
13
24
  import Breadcrumb from './Breadcrumb.vue'
14
25
 
15
- defineProps({
26
+ const features = inject('qdadmFeatures', { breadcrumb: false })
27
+
28
+ // Auto-injected title from useForm (if available)
29
+ const injectedTitleParts = inject('qdadmPageTitleParts', null)
30
+
31
+ const props = defineProps({
16
32
  title: {
17
33
  type: String,
18
- required: true
34
+ default: null
35
+ },
36
+ titleParts: {
37
+ type: Object,
38
+ default: null
19
39
  },
20
40
  subtitle: {
21
41
  type: String,
@@ -26,16 +46,42 @@ defineProps({
26
46
  default: null
27
47
  }
28
48
  })
49
+
50
+ // Use props first, then injected, then fallback
51
+ const effectiveTitleParts = computed(() => {
52
+ return props.titleParts || injectedTitleParts?.value || null
53
+ })
54
+
55
+ // Compute title display
56
+ const hasDecoratedTitle = computed(() => {
57
+ return effectiveTitleParts.value?.entityLabel
58
+ })
59
+
60
+ const titleBase = computed(() => {
61
+ if (effectiveTitleParts.value) {
62
+ return `${effectiveTitleParts.value.action} ${effectiveTitleParts.value.entityName}`
63
+ }
64
+ return props.title
65
+ })
66
+
67
+ // Only show PageHeader breadcrumb if global breadcrumb feature is disabled
68
+ const showBreadcrumb = computed(() => {
69
+ return props.breadcrumb?.length && !features.breadcrumb
70
+ })
29
71
  </script>
30
72
 
31
73
  <template>
32
74
  <div class="page-header">
33
75
  <div class="page-header-content">
34
- <Breadcrumb v-if="breadcrumb?.length" :items="breadcrumb" />
76
+ <Breadcrumb v-if="showBreadcrumb" :items="breadcrumb" />
35
77
  <div class="page-header-row">
36
78
  <div class="page-header-left">
37
79
  <div>
38
- <h1 class="page-title">{{ title }}</h1>
80
+ <h1 class="page-title">
81
+ <template v-if="hasDecoratedTitle"><span class="entity-label">{{ effectiveTitleParts.entityLabel }}</span></template>
82
+ <span v-if="effectiveTitleParts" class="action-badge">{{ effectiveTitleParts.action }} {{ effectiveTitleParts.entityName }}</span>
83
+ <template v-if="!effectiveTitleParts">{{ title }}</template>
84
+ </h1>
39
85
  <p v-if="subtitle" class="page-subtitle">{{ subtitle }}</p>
40
86
  </div>
41
87
  <slot name="subtitle" ></slot>
@@ -72,4 +118,24 @@ defineProps({
72
118
  font-size: 0.875rem;
73
119
  color: var(--p-text-secondary);
74
120
  }
121
+
122
+ /* Action badge (Edit User / Create Role) - small grey badge */
123
+ .action-badge {
124
+ display: inline-block;
125
+ font-size: 0.5em;
126
+ font-weight: 600;
127
+ background: var(--p-surface-200);
128
+ color: var(--p-surface-600);
129
+ padding: 0.25em 0.6em;
130
+ border-radius: 4px;
131
+ margin-left: 0.5em;
132
+ vertical-align: middle;
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.03em;
135
+ }
136
+
137
+ /* Entity label in title - main focus */
138
+ .entity-label {
139
+ font-weight: 600;
140
+ }
75
141
  </style>
@@ -21,8 +21,9 @@ import Button from 'primevue/button'
21
21
  import { useBreadcrumb } from '../../composables/useBreadcrumb'
22
22
 
23
23
  const props = defineProps({
24
- // Header
25
- title: { type: String, required: true },
24
+ // Header - use title OR titleParts (for decorated entity label)
25
+ title: { type: String, default: null },
26
+ titleParts: { type: Object, default: null }, // { action, entityName, entityLabel }
26
27
  subtitle: { type: String, default: null },
27
28
  headerActions: { type: Array, default: () => [] },
28
29
  breadcrumb: { type: Array, default: null }, // Override auto breadcrumb
@@ -52,7 +53,7 @@ function resolveLabel(label) {
52
53
 
53
54
  <template>
54
55
  <div>
55
- <PageHeader :title="title" :breadcrumb="props.breadcrumb || breadcrumbItems">
56
+ <PageHeader :title="title" :title-parts="titleParts" :breadcrumb="props.breadcrumb || breadcrumbItems">
56
57
  <template #subtitle>
57
58
  <slot name="subtitle">
58
59
  <span v-if="subtitle" class="page-subtitle">{{ subtitle }}</span>
@@ -17,3 +17,4 @@ export { useAuth } from './useAuth'
17
17
  export { useNavigation } from './useNavigation'
18
18
  export { useStatus } from './useStatus'
19
19
  export { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
20
+ export { useManager } from './useManager'
@@ -1,4 +1,4 @@
1
- import { ref, computed, provide, onUnmounted } from 'vue'
1
+ import { ref, computed, provide, inject, onUnmounted } from 'vue'
2
2
  import { useRoute, useRouter } from 'vue-router'
3
3
  import { useToast } from 'primevue/usetoast'
4
4
  import { useDirtyState } from './useDirtyState'
@@ -20,6 +20,9 @@ import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
20
20
  * } = useBareForm({
21
21
  * getState: () => ({ form: form.value }),
22
22
  * routePrefix: 'agents', // for cancel() navigation
23
+ * entityName: 'Agent', // for auto-title (optional, or use 'entity' for auto-lookup)
24
+ * labelField: 'name', // field to use as entity label (optional)
25
+ * entity: 'agents', // EntityManager name for auto metadata (optional)
23
26
  * guard: true, // enable unsaved changes modal
24
27
  * onGuardSave: () => save() // optional save callback for guard modal
25
28
  * })
@@ -32,20 +35,25 @@ import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
32
35
  * - Common computed (isEdit, entityId)
33
36
  * - Navigation helpers (cancel)
34
37
  * - Access to router, route, toast
38
+ * - Auto page title via provide (for PageHeader)
35
39
  *
36
40
  * @param {Object} options
37
41
  * @param {Function} options.getState - Function returning current form state for comparison
38
42
  * @param {string} options.routePrefix - Route name for cancel navigation (default: '')
43
+ * @param {string} options.entityName - Display name for entity (default: from manager or derived from routePrefix)
44
+ * @param {string|Function} options.labelField - Field name or function to extract entity label (default: from manager or 'name')
45
+ * @param {string|Ref|Object} options.entity - EntityManager name (string) for auto metadata, OR entity data (Ref/Object) for breadcrumb
39
46
  * @param {boolean} options.guard - Enable unsaved changes guard (default: true)
40
47
  * @param {Function} options.onGuardSave - Callback for save button in guard modal
41
48
  * @param {Function} options.getId - Custom function to extract entity ID from route (optional)
42
- * @param {Ref|Object} options.entity - Entity data for dynamic breadcrumb labels (optional)
43
49
  * @param {Function} options.breadcrumbLabel - Callback (entity) => string for custom breadcrumb label (optional)
44
50
  */
45
51
  export function useBareForm(options = {}) {
46
52
  const {
47
53
  getState,
48
54
  routePrefix = '',
55
+ entityName = null,
56
+ labelField = null,
49
57
  guard = true,
50
58
  onGuardSave = null,
51
59
  getId = null,
@@ -57,6 +65,19 @@ export function useBareForm(options = {}) {
57
65
  throw new Error('useBareForm requires a getState function')
58
66
  }
59
67
 
68
+ // Try to get EntityManager metadata if entity is a string
69
+ let manager = null
70
+ if (typeof entity === 'string') {
71
+ const orchestrator = inject('qdadmOrchestrator', null)
72
+ if (orchestrator) {
73
+ try {
74
+ manager = orchestrator.get(entity)
75
+ } catch {
76
+ // Manager not found, continue without it
77
+ }
78
+ }
79
+ }
80
+
60
81
  // Router, route, toast - common dependencies
61
82
  const router = useRouter()
62
83
  const route = useRoute()
@@ -88,8 +109,53 @@ export function useBareForm(options = {}) {
88
109
  provide('isFieldDirty', isFieldDirty)
89
110
  provide('dirtyFields', dirtyFields)
90
111
 
112
+ // Resolve entityName: explicit > manager > derived from routePrefix
113
+ const derivedEntityName = routePrefix
114
+ ? routePrefix.charAt(0).toUpperCase() + routePrefix.slice(1).replace(/s$/, '')
115
+ : null
116
+ const effectiveEntityName = entityName || manager?.label || derivedEntityName
117
+
118
+ // Resolve labelField: explicit > manager > default 'name'
119
+ const effectiveLabelField = labelField || manager?.labelField || 'name'
120
+
121
+ // Provide entity context for child components (e.g., SeverityTag auto-discovery)
122
+ if (routePrefix) {
123
+ const entityNameForProvider = routePrefix.endsWith('s') ? routePrefix : routePrefix + 's'
124
+ provide('mainEntity', entityNameForProvider)
125
+ } else if (typeof entity === 'string') {
126
+ provide('mainEntity', entity)
127
+ }
128
+
129
+ // Auto page title parts for PageHeader
130
+ const getEntityLabel = () => {
131
+ const state = getState()
132
+ const formData = state.form || state
133
+ if (!formData) return null
134
+ // Use manager.getEntityLabel if available, otherwise use effectiveLabelField
135
+ if (manager) {
136
+ return manager.getEntityLabel(formData)
137
+ }
138
+ if (typeof effectiveLabelField === 'function') {
139
+ return effectiveLabelField(formData)
140
+ }
141
+ return formData[effectiveLabelField] || null
142
+ }
143
+
144
+ const pageTitleParts = computed(() => ({
145
+ action: isEdit.value ? 'Edit' : 'Create',
146
+ entityName: effectiveEntityName,
147
+ entityLabel: isEdit.value ? getEntityLabel() : null
148
+ }))
149
+
150
+ // Provide title parts for automatic PageHeader consumption
151
+ if (effectiveEntityName) {
152
+ provide('qdadmPageTitleParts', pageTitleParts)
153
+ }
154
+
91
155
  // Breadcrumb (auto-generated from route path, with optional entity for dynamic labels)
92
- const { breadcrumbItems } = useBreadcrumb({ entity, getEntityLabel: breadcrumbLabel })
156
+ // Only pass entity to breadcrumb if it's actual entity data (not a string manager name)
157
+ const breadcrumbEntity = typeof entity === 'string' ? null : entity
158
+ const { breadcrumbItems } = useBreadcrumb({ entity: breadcrumbEntity, getEntityLabel: breadcrumbLabel })
93
159
 
94
160
  // Unsaved changes guard
95
161
  let guardDialog = null
@@ -117,6 +183,9 @@ export function useBareForm(options = {}) {
117
183
  route,
118
184
  toast,
119
185
 
186
+ // Manager (if resolved from entity string)
187
+ manager,
188
+
120
189
  // State
121
190
  loading,
122
191
  saving,
@@ -138,6 +207,9 @@ export function useBareForm(options = {}) {
138
207
  breadcrumb: breadcrumbItems,
139
208
 
140
209
  // Guard dialog (for UnsavedChangesDialog component)
141
- guardDialog
210
+ guardDialog,
211
+
212
+ // Title helpers
213
+ pageTitleParts
142
214
  }
143
215
  }