qdadm 0.46.1 → 0.48.0
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/package.json +3 -2
- package/src/components/forms/FormField.vue +2 -2
- package/src/components/forms/FormPage.vue +2 -2
- package/src/components/layout/PageNav.vue +5 -3
- package/src/composables/index.js +2 -1
- package/src/composables/{useFormPageBuilder.js → useEntityItemFormPage.js} +18 -38
- package/src/composables/useEntityItemPage.js +201 -0
- package/src/composables/useNavContext.js +15 -2
- package/src/entity/EntityManager.js +7 -8
- package/src/entity/storage/MockApiStorage.js +4 -10
- package/src/security/pages/RoleForm.vue +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdadm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@quazardous/quarkernel": "^2.1.0"
|
|
42
|
+
"@quazardous/quarkernel": "^2.1.0",
|
|
43
|
+
"pluralize": "^8.0.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"pinia": "^2.0.0",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* <InputText v-model="form.username" />
|
|
8
8
|
* </FormField>
|
|
9
9
|
*
|
|
10
|
-
* The parent form (
|
|
10
|
+
* The parent form (useEntityItemFormPage) provides:
|
|
11
11
|
* - isFieldDirty: function to check if field is dirty
|
|
12
12
|
* - getFieldError: function to get field error message
|
|
13
13
|
* - handleFieldBlur: function to trigger validation on blur
|
|
@@ -44,7 +44,7 @@ const props = defineProps({
|
|
|
44
44
|
}
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
// Inject from parent form (provided by
|
|
47
|
+
// Inject from parent form (provided by useEntityItemFormPage)
|
|
48
48
|
const isFieldDirty = inject('isFieldDirty', () => false)
|
|
49
49
|
const getFieldError = inject('getFieldError', () => null)
|
|
50
50
|
const handleFieldBlur = inject('handleFieldBlur', () => {})
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* - FormActions footer
|
|
10
10
|
* - UnsavedChangesDialog integration
|
|
11
11
|
*
|
|
12
|
-
* Props come from
|
|
12
|
+
* Props come from useEntityItemFormPage composable:
|
|
13
13
|
*
|
|
14
14
|
* ```vue
|
|
15
|
-
* const form =
|
|
15
|
+
* const form = useEntityItemFormPage({ entity: 'books' })
|
|
16
16
|
* form.generateFields()
|
|
17
17
|
* form.addSaveAction()
|
|
18
18
|
*
|
|
@@ -110,7 +110,8 @@ const breadcrumbItems = computed(() => {
|
|
|
110
110
|
const parentLabel = parentData.value
|
|
111
111
|
? parentManager.getEntityLabel(parentData.value)
|
|
112
112
|
: '...'
|
|
113
|
-
const
|
|
113
|
+
const defaultSuffix = parentManager.readOnly ? '-show' : '-edit'
|
|
114
|
+
const parentRouteName = itemRoute || `${parentManager.routePrefix}${defaultSuffix}`
|
|
114
115
|
|
|
115
116
|
items.push({
|
|
116
117
|
label: parentLabel,
|
|
@@ -157,10 +158,11 @@ const allNavlinks = computed(() => {
|
|
|
157
158
|
|
|
158
159
|
if (!parentManager) return navlinks.value
|
|
159
160
|
|
|
160
|
-
const
|
|
161
|
+
const defaultSuffix = parentManager.readOnly ? '-show' : '-edit'
|
|
162
|
+
const parentRouteName = itemRoute || `${parentManager.routePrefix}${defaultSuffix}`
|
|
161
163
|
const isOnParentRoute = route.name === parentRouteName
|
|
162
164
|
|
|
163
|
-
// Details link to parent
|
|
165
|
+
// Details link to parent item page
|
|
164
166
|
const detailsLink = {
|
|
165
167
|
label: 'Details',
|
|
166
168
|
to: { name: parentRouteName, params: { id: parentId } },
|
package/src/composables/index.js
CHANGED
|
@@ -7,13 +7,14 @@ export { useBreadcrumb } from './useBreadcrumb'
|
|
|
7
7
|
export { useSemanticBreadcrumb, computeSemanticBreadcrumb } from './useSemanticBreadcrumb'
|
|
8
8
|
export { useDirtyState } from './useDirtyState'
|
|
9
9
|
export { useForm } from './useForm'
|
|
10
|
-
export { useFormPageBuilder } from './
|
|
10
|
+
export { useEntityItemFormPage, useEntityItemFormPage as useFormPageBuilder } from './useEntityItemFormPage'
|
|
11
11
|
export * from './useJsonSyntax'
|
|
12
12
|
export { useListPageBuilder, PAGE_SIZE_OPTIONS } from './useListPageBuilder'
|
|
13
13
|
export { usePageTitle } from './usePageTitle'
|
|
14
14
|
export { useApp } from './useApp'
|
|
15
15
|
export { useAuth } from './useAuth'
|
|
16
16
|
export { useCurrentEntity } from './useCurrentEntity'
|
|
17
|
+
export { useEntityItemPage } from './useEntityItemPage'
|
|
17
18
|
export { useNavContext } from './useNavContext'
|
|
18
19
|
export { useNavigation } from './useNavigation'
|
|
19
20
|
export { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* useEntityItemFormPage - Unified procedural builder for CRUD form pages
|
|
3
3
|
*
|
|
4
4
|
* Provides a declarative/procedural API to build form pages with:
|
|
5
5
|
* - Mode detection (create vs edit from route params)
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ## Basic Usage
|
|
14
14
|
*
|
|
15
15
|
* ```js
|
|
16
|
-
* const form =
|
|
16
|
+
* const form = useEntityItemFormPage({ entity: 'books' })
|
|
17
17
|
* form.addSaveAction()
|
|
18
18
|
* form.addDeleteAction()
|
|
19
19
|
*
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* ## Auto-generated Fields
|
|
28
28
|
*
|
|
29
29
|
* ```js
|
|
30
|
-
* const form =
|
|
30
|
+
* const form = useEntityItemFormPage({ entity: 'books' })
|
|
31
31
|
* form.generateFields() // Auto-generate from manager.fields
|
|
32
32
|
* form.excludeField('internal_id') // Exclude specific fields
|
|
33
33
|
* form.addField('custom', { type: 'text', label: 'Custom' }) // Manual override
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* ## Validation
|
|
41
41
|
*
|
|
42
42
|
* ```js
|
|
43
|
-
* const form =
|
|
43
|
+
* const form = useEntityItemFormPage({ entity: 'books' })
|
|
44
44
|
* form.generateFields()
|
|
45
45
|
*
|
|
46
46
|
* // Add custom validator
|
|
@@ -77,19 +77,18 @@
|
|
|
77
77
|
* - Unsaved changes guard modal
|
|
78
78
|
* - Permission-aware save/delete actions via EntityManager.canUpdate/canDelete
|
|
79
79
|
*/
|
|
80
|
-
import { ref, computed, watch, onMounted,
|
|
80
|
+
import { ref, computed, watch, onMounted, onUnmounted, provide } from 'vue'
|
|
81
81
|
import { useRouter, useRoute } from 'vue-router'
|
|
82
82
|
import { useToast } from 'primevue/usetoast'
|
|
83
83
|
import { useConfirm } from 'primevue/useconfirm'
|
|
84
84
|
import { useDirtyState } from './useDirtyState'
|
|
85
85
|
import { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
86
86
|
import { useBreadcrumb } from './useBreadcrumb'
|
|
87
|
-
import {
|
|
87
|
+
import { useEntityItemPage } from './useEntityItemPage'
|
|
88
88
|
import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
|
|
89
89
|
import { deepClone } from '../utils/transformers'
|
|
90
|
-
import { onUnmounted } from 'vue'
|
|
91
90
|
|
|
92
|
-
export function
|
|
91
|
+
export function useEntityItemFormPage(config = {}) {
|
|
93
92
|
const {
|
|
94
93
|
entity,
|
|
95
94
|
// Mode detection
|
|
@@ -118,41 +117,22 @@ export function useFormPageBuilder(config = {}) {
|
|
|
118
117
|
const toast = useToast()
|
|
119
118
|
const confirm = useConfirm()
|
|
120
119
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'3. Missing entityFactory in Kernel options'
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
const manager = orchestrator.get(entity)
|
|
133
|
-
|
|
134
|
-
// Provide entity context for child components (e.g., SeverityTag auto-discovery)
|
|
135
|
-
provide('mainEntity', entity)
|
|
120
|
+
// Use useEntityItemPage for common infrastructure
|
|
121
|
+
// (orchestrator, manager, entityId, provide('mainEntity'), breadcrumb)
|
|
122
|
+
const itemPage = useEntityItemPage({
|
|
123
|
+
entity,
|
|
124
|
+
loadOnMount: false, // Form controls its own loading
|
|
125
|
+
breadcrumb: false, // Form calls setBreadcrumbEntity manually after transform
|
|
126
|
+
getId
|
|
127
|
+
})
|
|
136
128
|
|
|
137
|
-
|
|
138
|
-
const { setCurrentEntity } = useCurrentEntity()
|
|
129
|
+
const { manager, orchestrator, entityId, setBreadcrumbEntity } = itemPage
|
|
139
130
|
|
|
140
131
|
// Read config from manager with option overrides
|
|
141
132
|
const entityName = config.entityName ?? manager.label
|
|
142
133
|
const routePrefix = config.routePrefix ?? manager.routePrefix
|
|
143
134
|
const initialData = config.initialData ?? manager.getInitialData()
|
|
144
135
|
|
|
145
|
-
// ============ MODE DETECTION ============
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Extract entity ID from route params
|
|
149
|
-
* Supports: /books/:id/edit, /books/:id, /books/new
|
|
150
|
-
*/
|
|
151
|
-
const entityId = computed(() => {
|
|
152
|
-
if (getId) return getId()
|
|
153
|
-
return route.params.id || route.params.key || null
|
|
154
|
-
})
|
|
155
|
-
|
|
156
136
|
/**
|
|
157
137
|
* Detect form mode: 'create' or 'edit'
|
|
158
138
|
* Based on route name or presence of ID
|
|
@@ -263,7 +243,7 @@ export function useFormPageBuilder(config = {}) {
|
|
|
263
243
|
takeSnapshot()
|
|
264
244
|
|
|
265
245
|
// Share with navigation context for breadcrumb
|
|
266
|
-
|
|
246
|
+
setBreadcrumbEntity(transformed)
|
|
267
247
|
|
|
268
248
|
if (onLoadSuccess) {
|
|
269
249
|
await onLoadSuccess(transformed)
|
|
@@ -520,7 +500,7 @@ export function useFormPageBuilder(config = {}) {
|
|
|
520
500
|
const updatedConfig = { ...config, options }
|
|
521
501
|
fieldsMap.value.set(name, updatedConfig)
|
|
522
502
|
} catch (error) {
|
|
523
|
-
console.warn(`[
|
|
503
|
+
console.warn(`[useEntityItemFormPage] Failed to resolve options for field '${name}':`, error)
|
|
524
504
|
}
|
|
525
505
|
}
|
|
526
506
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEntityItemPage - Base composable for single entity item pages
|
|
3
|
+
*
|
|
4
|
+
* Provides common functionality for pages that display a single entity:
|
|
5
|
+
* - Entity loading by ID from route params
|
|
6
|
+
* - Loading/error state management
|
|
7
|
+
* - Breadcrumb integration (auto-calls setBreadcrumbEntity)
|
|
8
|
+
* - Manager access for child composables
|
|
9
|
+
*
|
|
10
|
+
* Used as base for:
|
|
11
|
+
* - Show pages (read-only detail pages)
|
|
12
|
+
* - useEntityItemFormPage (create/edit forms)
|
|
13
|
+
*
|
|
14
|
+
* ## Basic Usage (standalone)
|
|
15
|
+
*
|
|
16
|
+
* ```js
|
|
17
|
+
* const { data, loading, error, reload } = useEntityItemPage({ entity: 'posts' })
|
|
18
|
+
*
|
|
19
|
+
* // data.value contains the loaded entity
|
|
20
|
+
* // loading.value is true while fetching
|
|
21
|
+
* // error.value contains error message if failed
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Usage in parent composables
|
|
25
|
+
*
|
|
26
|
+
* ```js
|
|
27
|
+
* function useEntityItemFormPage(config) {
|
|
28
|
+
* const itemPage = useEntityItemPage({
|
|
29
|
+
* entity: config.entity,
|
|
30
|
+
* loadOnMount: false, // Form controls its own loading
|
|
31
|
+
* transformLoad: config.transformLoad
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* // Use itemPage.load(), itemPage.data, etc.
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
import { ref, computed, onMounted, inject, provide } from 'vue'
|
|
39
|
+
import { useRoute } from 'vue-router'
|
|
40
|
+
import { useCurrentEntity } from './useCurrentEntity.js'
|
|
41
|
+
|
|
42
|
+
export function useEntityItemPage(config = {}) {
|
|
43
|
+
const {
|
|
44
|
+
entity,
|
|
45
|
+
// Loading options
|
|
46
|
+
loadOnMount = true,
|
|
47
|
+
breadcrumb = true,
|
|
48
|
+
// ID extraction
|
|
49
|
+
getId = null,
|
|
50
|
+
idParam = 'id',
|
|
51
|
+
// Transform hook
|
|
52
|
+
transformLoad = (data) => data,
|
|
53
|
+
// Callbacks
|
|
54
|
+
onLoadSuccess = null,
|
|
55
|
+
onLoadError = null
|
|
56
|
+
} = config
|
|
57
|
+
|
|
58
|
+
const route = useRoute()
|
|
59
|
+
|
|
60
|
+
// Get EntityManager via orchestrator
|
|
61
|
+
const orchestrator = inject('qdadmOrchestrator')
|
|
62
|
+
if (!orchestrator) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'[qdadm] Orchestrator not provided.\n' +
|
|
65
|
+
'Possible causes:\n' +
|
|
66
|
+
'1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
|
|
67
|
+
'2. Component used outside of qdadm app context\n' +
|
|
68
|
+
'3. Missing entityFactory in Kernel options'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
const manager = orchestrator.get(entity)
|
|
72
|
+
|
|
73
|
+
// Provide entity context for child components
|
|
74
|
+
provide('mainEntity', entity)
|
|
75
|
+
|
|
76
|
+
// Breadcrumb integration
|
|
77
|
+
const { setBreadcrumbEntity } = useCurrentEntity()
|
|
78
|
+
|
|
79
|
+
// ============ STATE ============
|
|
80
|
+
|
|
81
|
+
const data = ref(null)
|
|
82
|
+
const loading = ref(false)
|
|
83
|
+
const error = ref(null)
|
|
84
|
+
|
|
85
|
+
// ============ ID EXTRACTION ============
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract entity ID from route params
|
|
89
|
+
* Supports custom getId function or param name
|
|
90
|
+
*/
|
|
91
|
+
const entityId = computed(() => {
|
|
92
|
+
if (getId) return getId()
|
|
93
|
+
return route.params[idParam] || route.params.id || route.params.key || null
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ============ LOADING ============
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load entity by ID
|
|
100
|
+
* @param {string|number} [id] - Optional ID override (defaults to route param)
|
|
101
|
+
* @returns {Promise<object|null>} Loaded entity data or null on error
|
|
102
|
+
*/
|
|
103
|
+
async function load(id = entityId.value) {
|
|
104
|
+
if (!id) {
|
|
105
|
+
error.value = 'No entity ID provided'
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
loading.value = true
|
|
110
|
+
error.value = null
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const responseData = await manager.get(id)
|
|
114
|
+
|
|
115
|
+
if (!responseData) {
|
|
116
|
+
error.value = `${manager.label || entity} not found`
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const transformed = transformLoad(responseData)
|
|
121
|
+
data.value = transformed
|
|
122
|
+
|
|
123
|
+
// Share with navigation context for breadcrumb
|
|
124
|
+
if (breadcrumb) {
|
|
125
|
+
setBreadcrumbEntity(transformed)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (onLoadSuccess) {
|
|
129
|
+
await onLoadSuccess(transformed)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return transformed
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(`[useEntityItemPage] Failed to load ${entity}:`, err)
|
|
135
|
+
error.value = err.response?.data?.detail || err.message || `Failed to load ${manager.label || entity}`
|
|
136
|
+
|
|
137
|
+
if (onLoadError) {
|
|
138
|
+
await onLoadError(err)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
} finally {
|
|
143
|
+
loading.value = false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Reload current entity
|
|
149
|
+
*/
|
|
150
|
+
async function reload() {
|
|
151
|
+
return load(entityId.value)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============ COMPUTED ============
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Entity label (uses manager.getEntityLabel)
|
|
158
|
+
*/
|
|
159
|
+
const entityLabel = computed(() => {
|
|
160
|
+
if (!data.value) return null
|
|
161
|
+
return manager.getEntityLabel(data.value)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if entity is loaded
|
|
166
|
+
*/
|
|
167
|
+
const isLoaded = computed(() => data.value !== null)
|
|
168
|
+
|
|
169
|
+
// ============ LIFECYCLE ============
|
|
170
|
+
|
|
171
|
+
if (loadOnMount) {
|
|
172
|
+
onMounted(() => {
|
|
173
|
+
if (entityId.value) {
|
|
174
|
+
load()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============ RETURN ============
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
// State
|
|
183
|
+
data,
|
|
184
|
+
loading,
|
|
185
|
+
error,
|
|
186
|
+
|
|
187
|
+
// Computed
|
|
188
|
+
entityId,
|
|
189
|
+
entityLabel,
|
|
190
|
+
isLoaded,
|
|
191
|
+
|
|
192
|
+
// Actions
|
|
193
|
+
load,
|
|
194
|
+
reload,
|
|
195
|
+
|
|
196
|
+
// References (for parent composables)
|
|
197
|
+
manager,
|
|
198
|
+
orchestrator,
|
|
199
|
+
setBreadcrumbEntity
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -46,6 +46,19 @@ export function useNavContext(options = {}) {
|
|
|
46
46
|
return router.getRoutes().some(r => r.name === name)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Get default item route for an entity manager
|
|
51
|
+
* - Read-only entities: use -show suffix
|
|
52
|
+
* - Editable entities: use -edit suffix
|
|
53
|
+
* @param {EntityManager} manager
|
|
54
|
+
* @returns {string} Route name
|
|
55
|
+
*/
|
|
56
|
+
function getDefaultItemRoute(manager) {
|
|
57
|
+
if (!manager) return null
|
|
58
|
+
const suffix = manager.readOnly ? '-show' : '-edit'
|
|
59
|
+
return `${manager.routePrefix}${suffix}`
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
/**
|
|
50
63
|
* Convert semantic breadcrumb to navigation chain format
|
|
51
64
|
* Maps semantic kinds to chain types for compatibility
|
|
@@ -71,7 +84,7 @@ export function useNavContext(options = {}) {
|
|
|
71
84
|
entity: item.entity,
|
|
72
85
|
manager,
|
|
73
86
|
id: item.id,
|
|
74
|
-
routeName: manager
|
|
87
|
+
routeName: getDefaultItemRoute(manager)
|
|
75
88
|
})
|
|
76
89
|
} else if (item.kind === 'entity-create') {
|
|
77
90
|
// Create page - treat as special list
|
|
@@ -246,7 +259,7 @@ export function useNavContext(options = {}) {
|
|
|
246
259
|
const parentManager = getManager(parentEntity)
|
|
247
260
|
if (!parentManager) return []
|
|
248
261
|
|
|
249
|
-
const parentRouteName = itemRoute ||
|
|
262
|
+
const parentRouteName = itemRoute || getDefaultItemRoute(parentManager)
|
|
250
263
|
const isOnParent = route.name === parentRouteName
|
|
251
264
|
|
|
252
265
|
// Details link
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PermissiveAuthAdapter } from './auth/PermissiveAdapter.js'
|
|
2
2
|
import { AuthActions } from './auth/EntityAuthAdapter.js'
|
|
3
3
|
import { QueryExecutor } from '../query/QueryExecutor.js'
|
|
4
|
+
import pluralize from 'pluralize'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* EntityManager - Base class for entity CRUD operations
|
|
@@ -307,36 +308,34 @@ export class EntityManager {
|
|
|
307
308
|
|
|
308
309
|
/**
|
|
309
310
|
* Get entity label (singular)
|
|
310
|
-
* Default: capitalize name (e.g., 'users' → 'User')
|
|
311
|
+
* Default: capitalize name (e.g., 'users' → 'User', 'countries' → 'Country')
|
|
311
312
|
*/
|
|
312
313
|
get label() {
|
|
313
314
|
if (this._label) return this._label
|
|
314
315
|
if (!this.name) return 'Item'
|
|
315
|
-
|
|
316
|
-
const singular = this.name.endsWith('s') ? this.name.slice(0, -1) : this.name
|
|
316
|
+
const singular = pluralize.singular(this.name)
|
|
317
317
|
return singular.charAt(0).toUpperCase() + singular.slice(1)
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
321
|
* Get entity label (plural)
|
|
322
|
-
* Default: capitalize name
|
|
322
|
+
* Default: capitalize name (e.g., 'user' → 'Users', 'country' → 'Countries')
|
|
323
323
|
*/
|
|
324
324
|
get labelPlural() {
|
|
325
325
|
if (this._labelPlural) return this._labelPlural
|
|
326
326
|
if (!this.name) return 'Items'
|
|
327
|
-
|
|
328
|
-
const plural = this.name.endsWith('s') ? this.name : this.name + 's'
|
|
327
|
+
const plural = pluralize.plural(this.name)
|
|
329
328
|
return plural.charAt(0).toUpperCase() + plural.slice(1)
|
|
330
329
|
}
|
|
331
330
|
|
|
332
331
|
/**
|
|
333
332
|
* Get route prefix for this entity
|
|
334
|
-
* Default: singular form of name (e.g., 'books' → 'book')
|
|
333
|
+
* Default: singular form of name (e.g., 'books' → 'book', 'countries' → 'country')
|
|
335
334
|
*/
|
|
336
335
|
get routePrefix() {
|
|
337
336
|
if (this._routePrefix) return this._routePrefix
|
|
338
337
|
if (!this.name) return 'item'
|
|
339
|
-
return
|
|
338
|
+
return pluralize.singular(this.name)
|
|
340
339
|
}
|
|
341
340
|
|
|
342
341
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IStorage } from './IStorage.js'
|
|
2
|
+
import { QueryExecutor } from '../../query/index.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* MockApiStorage - In-memory storage with localStorage persistence
|
|
@@ -191,16 +192,9 @@ export class MockApiStorage extends IStorage {
|
|
|
191
192
|
|
|
192
193
|
let items = this._getAll()
|
|
193
194
|
|
|
194
|
-
// Apply filters
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
items = items.filter(item => {
|
|
198
|
-
const itemValue = item[key]
|
|
199
|
-
if (typeof value === 'string' && typeof itemValue === 'string') {
|
|
200
|
-
return itemValue.toLowerCase().includes(value.toLowerCase())
|
|
201
|
-
}
|
|
202
|
-
return itemValue === value
|
|
203
|
-
})
|
|
195
|
+
// Apply filters using QueryExecutor (supports MongoDB-like operators)
|
|
196
|
+
if (Object.keys(filters).length > 0) {
|
|
197
|
+
items = QueryExecutor.execute(items, filters).items
|
|
204
198
|
}
|
|
205
199
|
|
|
206
200
|
// Apply search (substring match on all string fields)
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { ref, computed, inject } from 'vue'
|
|
7
|
-
import {
|
|
7
|
+
import { useEntityItemFormPage, FormPage, useOrchestrator, PermissionEditor } from '../../index.js'
|
|
8
8
|
import InputText from 'primevue/inputtext'
|
|
9
9
|
import AutoComplete from 'primevue/autocomplete'
|
|
10
10
|
import Chip from 'primevue/chip'
|
|
11
11
|
|
|
12
12
|
// ============ FORM BUILDER ============
|
|
13
|
-
const form =
|
|
13
|
+
const form = useEntityItemFormPage({ entity: 'roles' })
|
|
14
14
|
|
|
15
15
|
// ============ HELPERS ============
|
|
16
16
|
const { getManager } = useOrchestrator()
|