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 +26 -117
- package/package.json +1 -2
- package/src/components/SeverityTag.vue +71 -0
- package/src/components/editors/ScopeEditor.vue +1 -1
- package/src/components/index.js +1 -0
- package/src/components/layout/AppLayout.vue +29 -1
- package/src/components/layout/PageHeader.vue +71 -5
- package/src/components/layout/PageLayout.vue +4 -3
- package/src/composables/index.js +1 -0
- package/src/composables/useBareForm.js +76 -4
- package/src/composables/useBreadcrumb.js +23 -14
- package/src/composables/useForm.js +48 -2
- package/src/composables/useListPageBuilder.js +51 -74
- package/src/composables/useManager.js +20 -0
- package/src/entity/EntityManager.js +391 -9
- package/src/entity/storage/ApiStorage.js +5 -0
- package/src/entity/storage/LocalStorage.js +5 -0
- package/src/kernel/Kernel.js +25 -8
- package/src/plugin.js +3 -1
- package/src/styles/main.css +44 -0
- package/src/styles/theme/index.css +1 -1
- package/CHANGELOG.md +0 -270
package/README.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# qdadm
|
|
2
2
|
|
|
3
|
-
Vue 3
|
|
3
|
+
**Vue 3 admin framework. PrimeVue. Zero boilerplate.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Full documentation: [../../README.md](../../README.md)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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'
|
|
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
|
-
##
|
|
43
|
+
## Exports
|
|
60
44
|
|
|
61
45
|
```js
|
|
62
|
-
|
|
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
|
-
|
|
49
|
+
// Composables
|
|
50
|
+
import { useForm, useBareForm, useListPageBuilder } from 'qdadm/composables'
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
58
|
+
// Utilities
|
|
59
|
+
import { formatDate, truncate } from 'qdadm/utils'
|
|
113
60
|
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
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: '
|
|
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: {
|
package/src/components/index.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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">
|
|
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,
|
|
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>
|
package/src/composables/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|