qdadm 0.16.0 → 0.17.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.
Files changed (58) hide show
  1. package/README.md +153 -1
  2. package/package.json +15 -2
  3. package/src/components/forms/FormField.vue +64 -6
  4. package/src/components/forms/FormPage.vue +276 -0
  5. package/src/components/index.js +11 -0
  6. package/src/components/layout/BaseLayout.vue +183 -0
  7. package/src/components/layout/DashboardLayout.vue +100 -0
  8. package/src/components/layout/FormLayout.vue +261 -0
  9. package/src/components/layout/ListLayout.vue +334 -0
  10. package/src/components/layout/Zone.vue +165 -0
  11. package/src/components/layout/defaults/DefaultBreadcrumb.vue +140 -0
  12. package/src/components/layout/defaults/DefaultFooter.vue +56 -0
  13. package/src/components/layout/defaults/DefaultFormActions.vue +53 -0
  14. package/src/components/layout/defaults/DefaultHeader.vue +69 -0
  15. package/src/components/layout/defaults/DefaultMenu.vue +197 -0
  16. package/src/components/layout/defaults/DefaultPagination.vue +79 -0
  17. package/src/components/layout/defaults/DefaultTable.vue +130 -0
  18. package/src/components/layout/defaults/DefaultToaster.vue +16 -0
  19. package/src/components/layout/defaults/DefaultUserInfo.vue +96 -0
  20. package/src/components/layout/defaults/index.js +17 -0
  21. package/src/composables/index.js +6 -6
  22. package/src/composables/useForm.js +135 -0
  23. package/src/composables/useFormPageBuilder.js +1154 -0
  24. package/src/composables/useHooks.js +53 -0
  25. package/src/composables/useLayoutResolver.js +260 -0
  26. package/src/composables/useListPageBuilder.js +336 -52
  27. package/src/composables/useNavigation.js +38 -2
  28. package/src/composables/useSignals.js +49 -0
  29. package/src/composables/useZoneRegistry.js +162 -0
  30. package/src/core/bundles.js +406 -0
  31. package/src/core/decorator.js +322 -0
  32. package/src/core/extension.js +386 -0
  33. package/src/core/index.js +28 -0
  34. package/src/entity/EntityManager.js +314 -16
  35. package/src/entity/auth/AuthAdapter.js +125 -0
  36. package/src/entity/auth/PermissiveAdapter.js +64 -0
  37. package/src/entity/auth/index.js +11 -0
  38. package/src/entity/index.js +3 -0
  39. package/src/entity/storage/MockApiStorage.js +349 -0
  40. package/src/entity/storage/SdkStorage.js +478 -0
  41. package/src/entity/storage/index.js +2 -0
  42. package/src/hooks/HookRegistry.js +411 -0
  43. package/src/hooks/index.js +12 -0
  44. package/src/index.js +9 -0
  45. package/src/kernel/Kernel.js +136 -4
  46. package/src/kernel/SignalBus.js +180 -0
  47. package/src/kernel/index.js +7 -0
  48. package/src/module/moduleRegistry.js +124 -6
  49. package/src/orchestrator/Orchestrator.js +73 -1
  50. package/src/zones/ZoneRegistry.js +821 -0
  51. package/src/zones/index.js +16 -0
  52. package/src/zones/zones.js +189 -0
  53. package/src/composables/useEntityTitle.js +0 -121
  54. package/src/composables/useManager.js +0 -20
  55. package/src/composables/usePageBuilder.js +0 -334
  56. package/src/composables/useStatus.js +0 -146
  57. package/src/composables/useSubEditor.js +0 -165
  58. 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
+ }