qdadm 0.16.0 → 0.18.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/README.md +153 -1
- package/package.json +15 -2
- package/src/components/forms/FormField.vue +64 -6
- package/src/components/forms/FormPage.vue +276 -0
- package/src/components/index.js +11 -0
- package/src/components/layout/BaseLayout.vue +183 -0
- package/src/components/layout/DashboardLayout.vue +100 -0
- package/src/components/layout/FormLayout.vue +261 -0
- package/src/components/layout/ListLayout.vue +334 -0
- package/src/components/layout/Zone.vue +165 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +140 -0
- package/src/components/layout/defaults/DefaultFooter.vue +56 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +53 -0
- package/src/components/layout/defaults/DefaultHeader.vue +69 -0
- package/src/components/layout/defaults/DefaultMenu.vue +197 -0
- package/src/components/layout/defaults/DefaultPagination.vue +79 -0
- package/src/components/layout/defaults/DefaultTable.vue +130 -0
- package/src/components/layout/defaults/DefaultToaster.vue +16 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +96 -0
- package/src/components/layout/defaults/index.js +17 -0
- package/src/composables/index.js +6 -6
- package/src/composables/useForm.js +135 -0
- package/src/composables/useFormPageBuilder.js +1154 -0
- package/src/composables/useHooks.js +53 -0
- package/src/composables/useLayoutResolver.js +260 -0
- package/src/composables/useListPageBuilder.js +336 -52
- package/src/composables/useNavigation.js +38 -2
- package/src/composables/useSignals.js +49 -0
- package/src/composables/useZoneRegistry.js +162 -0
- package/src/core/bundles.js +406 -0
- package/src/core/decorator.js +322 -0
- package/src/core/extension.js +386 -0
- package/src/core/index.js +28 -0
- package/src/entity/EntityManager.js +359 -16
- package/src/entity/auth/AuthAdapter.js +184 -0
- package/src/entity/auth/PermissiveAdapter.js +64 -0
- package/src/entity/auth/RoleHierarchy.js +153 -0
- package/src/entity/auth/SecurityChecker.js +167 -0
- package/src/entity/auth/index.js +18 -0
- package/src/entity/index.js +3 -0
- package/src/entity/storage/MockApiStorage.js +349 -0
- package/src/entity/storage/SdkStorage.js +478 -0
- package/src/entity/storage/index.js +2 -0
- package/src/hooks/HookRegistry.js +411 -0
- package/src/hooks/index.js +12 -0
- package/src/index.js +13 -0
- package/src/kernel/Kernel.js +206 -5
- package/src/kernel/SignalBus.js +180 -0
- package/src/kernel/index.js +7 -0
- package/src/module/moduleRegistry.js +155 -28
- package/src/orchestrator/Orchestrator.js +73 -1
- package/src/zones/ZoneRegistry.js +828 -0
- package/src/zones/index.js +16 -0
- package/src/zones/zones.js +189 -0
- package/src/composables/useEntityTitle.js +0 -121
- package/src/composables/useManager.js +0 -20
- package/src/composables/usePageBuilder.js +0 -334
- package/src/composables/useStatus.js +0 -146
- package/src/composables/useSubEditor.js +0 -165
- package/src/composables/useTabSync.js +0 -110
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockApiStorage - In-memory storage with localStorage persistence
|
|
3
|
+
*
|
|
4
|
+
* Combines in-memory Map performance with localStorage persistence.
|
|
5
|
+
* Designed for demos that need:
|
|
6
|
+
* - Fast in-memory operations
|
|
7
|
+
* - Data persistence across page reloads
|
|
8
|
+
* - Optional initial data seeding
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```js
|
|
12
|
+
* const storage = new MockApiStorage({
|
|
13
|
+
* entityName: 'users',
|
|
14
|
+
* idField: 'id',
|
|
15
|
+
* initialData: [{ id: '1', name: 'Alice' }]
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* localStorage key pattern: mockapi_${entityName}_data
|
|
20
|
+
*/
|
|
21
|
+
export class MockApiStorage {
|
|
22
|
+
/**
|
|
23
|
+
* In-memory storage with persistence doesn't benefit from EntityManager cache
|
|
24
|
+
*/
|
|
25
|
+
supportsCaching = false
|
|
26
|
+
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
const {
|
|
29
|
+
entityName,
|
|
30
|
+
idField = 'id',
|
|
31
|
+
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
|
|
32
|
+
initialData = null
|
|
33
|
+
} = options
|
|
34
|
+
|
|
35
|
+
if (!entityName) {
|
|
36
|
+
throw new Error('MockApiStorage requires entityName option')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.entityName = entityName
|
|
40
|
+
this.idField = idField
|
|
41
|
+
this.generateId = generateId
|
|
42
|
+
this._storageKey = `mockapi_${entityName}_data`
|
|
43
|
+
this._data = new Map()
|
|
44
|
+
|
|
45
|
+
// Load from localStorage or seed with initialData
|
|
46
|
+
this._loadFromStorage(initialData)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load data from localStorage, seed with initialData if empty
|
|
51
|
+
* @param {Array|null} initialData - Data to seed if storage is empty
|
|
52
|
+
*/
|
|
53
|
+
_loadFromStorage(initialData) {
|
|
54
|
+
try {
|
|
55
|
+
const stored = localStorage.getItem(this._storageKey)
|
|
56
|
+
if (stored) {
|
|
57
|
+
const items = JSON.parse(stored)
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
const id = item[this.idField]
|
|
60
|
+
if (id !== undefined && id !== null) {
|
|
61
|
+
this._data.set(String(id), { ...item })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (initialData && Array.isArray(initialData) && initialData.length > 0) {
|
|
65
|
+
// Seed with initial data
|
|
66
|
+
for (const item of initialData) {
|
|
67
|
+
const id = item[this.idField]
|
|
68
|
+
if (id !== undefined && id !== null) {
|
|
69
|
+
this._data.set(String(id), { ...item })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this._persistToStorage()
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// localStorage unavailable or corrupted, start fresh
|
|
76
|
+
if (initialData && Array.isArray(initialData)) {
|
|
77
|
+
for (const item of initialData) {
|
|
78
|
+
const id = item[this.idField]
|
|
79
|
+
if (id !== undefined && id !== null) {
|
|
80
|
+
this._data.set(String(id), { ...item })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Persist current in-memory data to localStorage
|
|
89
|
+
*/
|
|
90
|
+
_persistToStorage() {
|
|
91
|
+
try {
|
|
92
|
+
const items = Array.from(this._data.values())
|
|
93
|
+
localStorage.setItem(this._storageKey, JSON.stringify(items))
|
|
94
|
+
} catch {
|
|
95
|
+
// localStorage unavailable or quota exceeded, continue without persistence
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get all items as array
|
|
101
|
+
* @returns {Array}
|
|
102
|
+
*/
|
|
103
|
+
_getAll() {
|
|
104
|
+
return Array.from(this._data.values())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List entities with pagination/filtering
|
|
109
|
+
* @param {object} params - Query parameters
|
|
110
|
+
* @param {number} [params.page=1] - Page number (1-based)
|
|
111
|
+
* @param {number} [params.page_size=20] - Items per page
|
|
112
|
+
* @param {string} [params.sort_by] - Field to sort by
|
|
113
|
+
* @param {string} [params.sort_order='asc'] - Sort order ('asc' or 'desc')
|
|
114
|
+
* @param {object} [params.filters] - Field filters { field: value }
|
|
115
|
+
* @param {string} [params.search] - Search query (substring match on string fields)
|
|
116
|
+
* @returns {Promise<{ items: Array, total: number }>}
|
|
117
|
+
*/
|
|
118
|
+
async list(params = {}) {
|
|
119
|
+
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', filters = {}, search } = params
|
|
120
|
+
|
|
121
|
+
let items = this._getAll()
|
|
122
|
+
|
|
123
|
+
// Apply filters (substring match for strings, exact match for others)
|
|
124
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
125
|
+
if (value === null || value === undefined || value === '') continue
|
|
126
|
+
items = items.filter(item => {
|
|
127
|
+
const itemValue = item[key]
|
|
128
|
+
if (typeof value === 'string' && typeof itemValue === 'string') {
|
|
129
|
+
return itemValue.toLowerCase().includes(value.toLowerCase())
|
|
130
|
+
}
|
|
131
|
+
return itemValue === value
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Apply search (substring match on all string fields)
|
|
136
|
+
if (search && search.trim()) {
|
|
137
|
+
const query = search.toLowerCase().trim()
|
|
138
|
+
items = items.filter(item => {
|
|
139
|
+
for (const value of Object.values(item)) {
|
|
140
|
+
if (typeof value === 'string' && value.toLowerCase().includes(query)) {
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const total = items.length
|
|
149
|
+
|
|
150
|
+
// Apply sorting
|
|
151
|
+
if (sort_by) {
|
|
152
|
+
items.sort((a, b) => {
|
|
153
|
+
const aVal = a[sort_by]
|
|
154
|
+
const bVal = b[sort_by]
|
|
155
|
+
if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
|
|
156
|
+
if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
|
|
157
|
+
return 0
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Apply pagination
|
|
162
|
+
const start = (page - 1) * page_size
|
|
163
|
+
items = items.slice(start, start + page_size)
|
|
164
|
+
|
|
165
|
+
return { items, total }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get a single entity by ID
|
|
170
|
+
* @param {string|number} id
|
|
171
|
+
* @returns {Promise<object>}
|
|
172
|
+
*/
|
|
173
|
+
async get(id) {
|
|
174
|
+
const item = this._data.get(String(id))
|
|
175
|
+
if (!item) {
|
|
176
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
177
|
+
error.status = 404
|
|
178
|
+
throw error
|
|
179
|
+
}
|
|
180
|
+
return { ...item }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get multiple entities by IDs (batch fetch)
|
|
185
|
+
* @param {Array<string|number>} ids
|
|
186
|
+
* @returns {Promise<Array<object>>}
|
|
187
|
+
*/
|
|
188
|
+
async getMany(ids) {
|
|
189
|
+
if (!ids || ids.length === 0) return []
|
|
190
|
+
const results = []
|
|
191
|
+
for (const id of ids) {
|
|
192
|
+
const item = this._data.get(String(id))
|
|
193
|
+
if (item) {
|
|
194
|
+
results.push({ ...item })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return results
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get distinct values for a field
|
|
202
|
+
* @param {string} field - Field name to get distinct values from
|
|
203
|
+
* @returns {Promise<Array>} Sorted array of unique values
|
|
204
|
+
*/
|
|
205
|
+
async distinct(field) {
|
|
206
|
+
const values = new Set()
|
|
207
|
+
for (const item of this._data.values()) {
|
|
208
|
+
if (item[field] !== undefined && item[field] !== null) {
|
|
209
|
+
values.add(item[field])
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return Array.from(values).sort()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get distinct values with counts for a field
|
|
217
|
+
* @param {string} field - Field name to get distinct values from
|
|
218
|
+
* @returns {Promise<Array<{ value: any, count: number }>>} Array of value/count pairs, sorted by value
|
|
219
|
+
*/
|
|
220
|
+
async distinctWithCount(field) {
|
|
221
|
+
const counts = new Map()
|
|
222
|
+
for (const item of this._data.values()) {
|
|
223
|
+
const value = item[field]
|
|
224
|
+
if (value !== undefined && value !== null) {
|
|
225
|
+
counts.set(value, (counts.get(value) || 0) + 1)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Array.from(counts.entries())
|
|
229
|
+
.map(([value, count]) => ({ value, count }))
|
|
230
|
+
.sort((a, b) => {
|
|
231
|
+
if (a.value < b.value) return -1
|
|
232
|
+
if (a.value > b.value) return 1
|
|
233
|
+
return 0
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a new entity
|
|
239
|
+
* @param {object} data
|
|
240
|
+
* @returns {Promise<object>}
|
|
241
|
+
*/
|
|
242
|
+
async create(data) {
|
|
243
|
+
const id = data[this.idField] || this.generateId()
|
|
244
|
+
const newItem = {
|
|
245
|
+
...data,
|
|
246
|
+
[this.idField]: id,
|
|
247
|
+
created_at: data.created_at || new Date().toISOString()
|
|
248
|
+
}
|
|
249
|
+
this._data.set(String(id), newItem)
|
|
250
|
+
this._persistToStorage()
|
|
251
|
+
return { ...newItem }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Update an entity (PUT - full replacement)
|
|
256
|
+
* @param {string|number} id
|
|
257
|
+
* @param {object} data
|
|
258
|
+
* @returns {Promise<object>}
|
|
259
|
+
*/
|
|
260
|
+
async update(id, data) {
|
|
261
|
+
if (!this._data.has(String(id))) {
|
|
262
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
263
|
+
error.status = 404
|
|
264
|
+
throw error
|
|
265
|
+
}
|
|
266
|
+
const updated = {
|
|
267
|
+
...data,
|
|
268
|
+
[this.idField]: id,
|
|
269
|
+
updated_at: new Date().toISOString()
|
|
270
|
+
}
|
|
271
|
+
this._data.set(String(id), updated)
|
|
272
|
+
this._persistToStorage()
|
|
273
|
+
return { ...updated }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Partially update an entity (PATCH)
|
|
278
|
+
* @param {string|number} id
|
|
279
|
+
* @param {object} data
|
|
280
|
+
* @returns {Promise<object>}
|
|
281
|
+
*/
|
|
282
|
+
async patch(id, data) {
|
|
283
|
+
const existing = this._data.get(String(id))
|
|
284
|
+
if (!existing) {
|
|
285
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
286
|
+
error.status = 404
|
|
287
|
+
throw error
|
|
288
|
+
}
|
|
289
|
+
const updated = {
|
|
290
|
+
...existing,
|
|
291
|
+
...data,
|
|
292
|
+
updated_at: new Date().toISOString()
|
|
293
|
+
}
|
|
294
|
+
this._data.set(String(id), updated)
|
|
295
|
+
this._persistToStorage()
|
|
296
|
+
return { ...updated }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Delete an entity
|
|
301
|
+
* @param {string|number} id
|
|
302
|
+
* @returns {Promise<void>}
|
|
303
|
+
*/
|
|
304
|
+
async delete(id) {
|
|
305
|
+
if (!this._data.has(String(id))) {
|
|
306
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
307
|
+
error.status = 404
|
|
308
|
+
throw error
|
|
309
|
+
}
|
|
310
|
+
this._data.delete(String(id))
|
|
311
|
+
this._persistToStorage()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Clear all items
|
|
316
|
+
* @returns {Promise<void>}
|
|
317
|
+
*/
|
|
318
|
+
async clear() {
|
|
319
|
+
this._data.clear()
|
|
320
|
+
try {
|
|
321
|
+
localStorage.removeItem(this._storageKey)
|
|
322
|
+
} catch {
|
|
323
|
+
// localStorage unavailable
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get current item count
|
|
329
|
+
* @returns {number}
|
|
330
|
+
*/
|
|
331
|
+
get size() {
|
|
332
|
+
return this._data.size
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get the localStorage key used by this storage
|
|
337
|
+
* @returns {string}
|
|
338
|
+
*/
|
|
339
|
+
get storageKey() {
|
|
340
|
+
return this._storageKey
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Factory function to create a MockApiStorage
|
|
346
|
+
*/
|
|
347
|
+
export function createMockApiStorage(options) {
|
|
348
|
+
return new MockApiStorage(options)
|
|
349
|
+
}
|