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,322 @@
1
+ /**
2
+ * EntityManager Decorator Pattern Utilities
3
+ *
4
+ * Decorators wrap an EntityManager to add cross-cutting concerns
5
+ * (auditing, caching, soft-delete) without modifying the base class.
6
+ *
7
+ * Key concepts:
8
+ * - Decorators receive a manager and return an enhanced manager
9
+ * - Decorators compose: first wraps closest to base, last wraps outermost
10
+ * - Each decorator intercepts CRUD operations, adds behavior, delegates to wrapped manager
11
+ *
12
+ * @example
13
+ * // Single decorator
14
+ * const auditedBooks = createDecoratedManager(booksManager, [
15
+ * withAuditLog(console.log)
16
+ * ])
17
+ *
18
+ * @example
19
+ * // Stacked decorators (order matters: audit wraps cache wraps base)
20
+ * const enhancedBooks = createDecoratedManager(booksManager, [
21
+ * withCache({ ttl: 60000 }),
22
+ * withAuditLog(logger.info)
23
+ * ])
24
+ */
25
+
26
+ /**
27
+ * Apply a chain of decorators to an EntityManager
28
+ *
29
+ * Decorators are applied in order: first decorator wraps the base manager,
30
+ * second wraps the first, etc. This means the last decorator in the array
31
+ * is the outermost layer (executed first on method calls).
32
+ *
33
+ * @param {EntityManager} manager - Base EntityManager to decorate
34
+ * @param {Function[]} decorators - Array of decorator functions
35
+ * @returns {EntityManager} - Decorated manager
36
+ *
37
+ * @example
38
+ * // Decorator function signature
39
+ * const myDecorator = (options) => (manager) => {
40
+ * return {
41
+ * ...manager,
42
+ * async create(data) {
43
+ * // Pre-processing
44
+ * const result = await manager.create(data)
45
+ * // Post-processing
46
+ * return result
47
+ * }
48
+ * }
49
+ * }
50
+ */
51
+ export function createDecoratedManager(manager, decorators = []) {
52
+ if (!manager) {
53
+ throw new Error('[createDecoratedManager] Manager is required')
54
+ }
55
+ if (!Array.isArray(decorators)) {
56
+ throw new Error('[createDecoratedManager] Decorators must be an array')
57
+ }
58
+
59
+ // Apply decorators in sequence (first wraps base, last is outermost)
60
+ return decorators.reduce((decorated, decorator) => {
61
+ if (typeof decorator !== 'function') {
62
+ throw new Error('[createDecoratedManager] Each decorator must be a function')
63
+ }
64
+ return decorator(decorated)
65
+ }, manager)
66
+ }
67
+
68
+ /**
69
+ * Audit log decorator factory
70
+ *
71
+ * Logs CRUD operations with timestamps. Useful for tracking changes,
72
+ * debugging, or audit trails.
73
+ *
74
+ * @param {Function} logger - Logging function (receives action, details)
75
+ * @param {object} [options] - Options
76
+ * @param {boolean} [options.includeData=false] - Include record data in logs
77
+ * @returns {Function} Decorator function
78
+ *
79
+ * @example
80
+ * const auditedManager = createDecoratedManager(manager, [
81
+ * withAuditLog(console.log)
82
+ * ])
83
+ *
84
+ * @example
85
+ * // With custom logger
86
+ * const auditedManager = createDecoratedManager(manager, [
87
+ * withAuditLog((action, details) => {
88
+ * auditService.log({ action, ...details, timestamp: new Date() })
89
+ * }, { includeData: true })
90
+ * ])
91
+ */
92
+ export function withAuditLog(logger, options = {}) {
93
+ if (typeof logger !== 'function') {
94
+ throw new Error('[withAuditLog] Logger must be a function')
95
+ }
96
+
97
+ const { includeData = false } = options
98
+
99
+ return (manager) => {
100
+ const entityName = manager.name
101
+
102
+ const logAction = (action, details) => {
103
+ logger(action, {
104
+ entity: entityName,
105
+ timestamp: new Date().toISOString(),
106
+ ...details
107
+ })
108
+ }
109
+
110
+ return {
111
+ // Preserve all manager properties and methods
112
+ ...manager,
113
+ // Ensure name is accessible (might be a getter)
114
+ get name() { return manager.name },
115
+
116
+ async create(data) {
117
+ const result = await manager.create(data)
118
+ const logDetails = { id: result?.[manager.idField] }
119
+ if (includeData) logDetails.data = data
120
+ logAction('create', logDetails)
121
+ return result
122
+ },
123
+
124
+ async update(id, data) {
125
+ const result = await manager.update(id, data)
126
+ const logDetails = { id }
127
+ if (includeData) logDetails.data = data
128
+ logAction('update', logDetails)
129
+ return result
130
+ },
131
+
132
+ async patch(id, data) {
133
+ const result = await manager.patch(id, data)
134
+ const logDetails = { id }
135
+ if (includeData) logDetails.data = data
136
+ logAction('patch', logDetails)
137
+ return result
138
+ },
139
+
140
+ async delete(id) {
141
+ await manager.delete(id)
142
+ logAction('delete', { id })
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Soft delete decorator factory
150
+ *
151
+ * Converts delete() to update with deleted_at timestamp.
152
+ * Useful for audit trails, undo functionality, or legal compliance.
153
+ *
154
+ * @param {object} [options] - Options
155
+ * @param {string} [options.field='deleted_at'] - Field name for deletion timestamp
156
+ * @param {Function} [options.timestamp] - Custom timestamp function (default: ISO string)
157
+ * @returns {Function} Decorator function
158
+ *
159
+ * @example
160
+ * const softDeleteManager = createDecoratedManager(manager, [
161
+ * withSoftDelete()
162
+ * ])
163
+ *
164
+ * // Instead of deleting, sets deleted_at
165
+ * await softDeleteManager.delete(1)
166
+ * // Record: { id: 1, ..., deleted_at: '2024-01-01T00:00:00.000Z' }
167
+ *
168
+ * @example
169
+ * // Custom field and timestamp
170
+ * const softDeleteManager = createDecoratedManager(manager, [
171
+ * withSoftDelete({
172
+ * field: 'removed_at',
173
+ * timestamp: () => Date.now()
174
+ * })
175
+ * ])
176
+ */
177
+ export function withSoftDelete(options = {}) {
178
+ const {
179
+ field = 'deleted_at',
180
+ timestamp = () => new Date().toISOString()
181
+ } = options
182
+
183
+ return (manager) => ({
184
+ ...manager,
185
+ get name() { return manager.name },
186
+
187
+ async delete(id) {
188
+ // Convert delete to update with soft-delete field
189
+ return manager.patch(id, { [field]: timestamp() })
190
+ }
191
+ })
192
+ }
193
+
194
+ /**
195
+ * Timestamp decorator factory
196
+ *
197
+ * Automatically adds created_at and updated_at timestamps to records.
198
+ *
199
+ * @param {object} [options] - Options
200
+ * @param {string} [options.createdAtField='created_at'] - Field for creation timestamp
201
+ * @param {string} [options.updatedAtField='updated_at'] - Field for update timestamp
202
+ * @param {Function} [options.timestamp] - Custom timestamp function
203
+ * @returns {Function} Decorator function
204
+ *
205
+ * @example
206
+ * const timestampedManager = createDecoratedManager(manager, [
207
+ * withTimestamps()
208
+ * ])
209
+ *
210
+ * await timestampedManager.create({ title: 'Test' })
211
+ * // Record: { title: 'Test', created_at: '...', updated_at: '...' }
212
+ *
213
+ * await timestampedManager.update(1, { title: 'Updated' })
214
+ * // Record: { title: 'Updated', updated_at: '...' }
215
+ */
216
+ export function withTimestamps(options = {}) {
217
+ const {
218
+ createdAtField = 'created_at',
219
+ updatedAtField = 'updated_at',
220
+ timestamp = () => new Date().toISOString()
221
+ } = options
222
+
223
+ return (manager) => ({
224
+ ...manager,
225
+ get name() { return manager.name },
226
+
227
+ async create(data) {
228
+ const now = timestamp()
229
+ return manager.create({
230
+ ...data,
231
+ [createdAtField]: now,
232
+ [updatedAtField]: now
233
+ })
234
+ },
235
+
236
+ async update(id, data) {
237
+ return manager.update(id, {
238
+ ...data,
239
+ [updatedAtField]: timestamp()
240
+ })
241
+ },
242
+
243
+ async patch(id, data) {
244
+ return manager.patch(id, {
245
+ ...data,
246
+ [updatedAtField]: timestamp()
247
+ })
248
+ }
249
+ })
250
+ }
251
+
252
+ /**
253
+ * Validation decorator factory
254
+ *
255
+ * Validates data before create/update operations.
256
+ * Throws ValidationError if validation fails.
257
+ *
258
+ * @param {Function} validator - Validation function (data, context) => errors | null
259
+ * @param {object} [options] - Options
260
+ * @param {boolean} [options.onUpdate=true] - Validate on update
261
+ * @param {boolean} [options.onPatch=true] - Validate on patch
262
+ * @returns {Function} Decorator function
263
+ *
264
+ * @example
265
+ * const validatedManager = createDecoratedManager(manager, [
266
+ * withValidation((data) => {
267
+ * const errors = {}
268
+ * if (!data.title) errors.title = 'Title is required'
269
+ * if (data.price < 0) errors.price = 'Price must be positive'
270
+ * return Object.keys(errors).length ? errors : null
271
+ * })
272
+ * ])
273
+ *
274
+ * await validatedManager.create({ price: -5 })
275
+ * // Throws: ValidationError { errors: { title: '...', price: '...' } }
276
+ */
277
+ export function withValidation(validator, options = {}) {
278
+ if (typeof validator !== 'function') {
279
+ throw new Error('[withValidation] Validator must be a function')
280
+ }
281
+
282
+ const {
283
+ onUpdate = true,
284
+ onPatch = true
285
+ } = options
286
+
287
+ return (manager) => {
288
+ const validate = (data, context) => {
289
+ const errors = validator(data, context)
290
+ if (errors && Object.keys(errors).length > 0) {
291
+ const error = new Error('Validation failed')
292
+ error.name = 'ValidationError'
293
+ error.errors = errors
294
+ throw error
295
+ }
296
+ }
297
+
298
+ return {
299
+ ...manager,
300
+ get name() { return manager.name },
301
+
302
+ async create(data) {
303
+ validate(data, { action: 'create', manager })
304
+ return manager.create(data)
305
+ },
306
+
307
+ async update(id, data) {
308
+ if (onUpdate) {
309
+ validate(data, { action: 'update', id, manager })
310
+ }
311
+ return manager.update(id, data)
312
+ },
313
+
314
+ async patch(id, data) {
315
+ if (onPatch) {
316
+ validate(data, { action: 'patch', id, manager })
317
+ }
318
+ return manager.patch(id, data)
319
+ }
320
+ }
321
+ }
322
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * extendModule - Module extension helper for qdadm
3
+ *
4
+ * Provides a clean API for one module to extend another module's
5
+ * configuration and behavior through the hook system.
6
+ *
7
+ * Extension types:
8
+ * - columns: adds columns via `{target}:list:alter` hook
9
+ * - fields: adds fields via `{target}:form:alter` hook
10
+ * - filters: adds filters via `{target}:filter:alter` hook
11
+ * - blocks: injects blocks via zone registry
12
+ *
13
+ * @example
14
+ * // Simple config object approach
15
+ * const cleanup = extendModule('books', {
16
+ * columns: [
17
+ * { field: 'rating', header: 'Rating', width: 100 }
18
+ * ],
19
+ * fields: [
20
+ * { name: 'rating', type: 'number', label: 'Rating' }
21
+ * ],
22
+ * blocks: {
23
+ * 'books:detail:sidebar': [
24
+ * { component: RatingWidget, weight: 20 }
25
+ * ]
26
+ * }
27
+ * }, { hooks, zones })
28
+ *
29
+ * // Later: cleanup() // Removes all registered extensions
30
+ *
31
+ * @example
32
+ * // Fluent API approach
33
+ * const cleanup = extendModule('books')
34
+ * .addColumn({ field: 'rating', header: 'Rating' })
35
+ * .addField({ name: 'rating', type: 'number' })
36
+ * .addBlock('books:detail:sidebar', { component: RatingWidget })
37
+ * .register({ hooks, zones })
38
+ */
39
+
40
+ /**
41
+ * Default priority for extension hooks
42
+ */
43
+ const DEFAULT_PRIORITY = 50
44
+
45
+ /**
46
+ * Extension builder for fluent API
47
+ *
48
+ * @private
49
+ */
50
+ class ExtensionBuilder {
51
+ /**
52
+ * @param {string} target - Target module name to extend
53
+ */
54
+ constructor(target) {
55
+ if (!target || typeof target !== 'string') {
56
+ throw new Error('[extendModule] Target module name must be a non-empty string')
57
+ }
58
+ this._target = target
59
+ this._columns = []
60
+ this._fields = []
61
+ this._filters = []
62
+ this._blocks = new Map()
63
+ }
64
+
65
+ /**
66
+ * Add a column to the target module's list view
67
+ *
68
+ * @param {object} column - Column configuration
69
+ * @param {string} column.field - Field name
70
+ * @param {string} [column.header] - Column header label
71
+ * @param {number} [column.width] - Column width
72
+ * @param {Function} [column.body] - Custom body template function
73
+ * @returns {this} For chaining
74
+ */
75
+ addColumn(column) {
76
+ if (!column || !column.field) {
77
+ throw new Error('[extendModule] Column must have a field property')
78
+ }
79
+ this._columns.push(column)
80
+ return this
81
+ }
82
+
83
+ /**
84
+ * Add multiple columns to the target module's list view
85
+ *
86
+ * @param {object[]} columns - Array of column configurations
87
+ * @returns {this} For chaining
88
+ */
89
+ addColumns(columns) {
90
+ if (!Array.isArray(columns)) {
91
+ throw new Error('[extendModule] addColumns expects an array')
92
+ }
93
+ for (const column of columns) {
94
+ this.addColumn(column)
95
+ }
96
+ return this
97
+ }
98
+
99
+ /**
100
+ * Add a field to the target module's form view
101
+ *
102
+ * @param {object} field - Field configuration
103
+ * @param {string} field.name - Field name
104
+ * @param {string} [field.type='text'] - Field type
105
+ * @param {string} [field.label] - Field label
106
+ * @returns {this} For chaining
107
+ */
108
+ addField(field) {
109
+ if (!field || !field.name) {
110
+ throw new Error('[extendModule] Field must have a name property')
111
+ }
112
+ this._fields.push(field)
113
+ return this
114
+ }
115
+
116
+ /**
117
+ * Add multiple fields to the target module's form view
118
+ *
119
+ * @param {object[]} fields - Array of field configurations
120
+ * @returns {this} For chaining
121
+ */
122
+ addFields(fields) {
123
+ if (!Array.isArray(fields)) {
124
+ throw new Error('[extendModule] addFields expects an array')
125
+ }
126
+ for (const field of fields) {
127
+ this.addField(field)
128
+ }
129
+ return this
130
+ }
131
+
132
+ /**
133
+ * Add a filter to the target module's filter bar
134
+ *
135
+ * @param {object} filter - Filter configuration
136
+ * @param {string} filter.name - Filter name
137
+ * @param {string} [filter.type='text'] - Filter type
138
+ * @param {string} [filter.label] - Filter label
139
+ * @returns {this} For chaining
140
+ */
141
+ addFilter(filter) {
142
+ if (!filter || !filter.name) {
143
+ throw new Error('[extendModule] Filter must have a name property')
144
+ }
145
+ this._filters.push(filter)
146
+ return this
147
+ }
148
+
149
+ /**
150
+ * Add multiple filters to the target module's filter bar
151
+ *
152
+ * @param {object[]} filters - Array of filter configurations
153
+ * @returns {this} For chaining
154
+ */
155
+ addFilters(filters) {
156
+ if (!Array.isArray(filters)) {
157
+ throw new Error('[extendModule] addFilters expects an array')
158
+ }
159
+ for (const filter of filters) {
160
+ this.addFilter(filter)
161
+ }
162
+ return this
163
+ }
164
+
165
+ /**
166
+ * Add a block to a zone in the target module
167
+ *
168
+ * @param {string} zoneName - Zone name (e.g., 'books:detail:sidebar')
169
+ * @param {object} block - Block configuration
170
+ * @param {import('vue').Component} block.component - Vue component
171
+ * @param {number} [block.weight=50] - Block weight for ordering
172
+ * @param {object} [block.props={}] - Props to pass to component
173
+ * @param {string} [block.id] - Unique block ID
174
+ * @returns {this} For chaining
175
+ */
176
+ addBlock(zoneName, block) {
177
+ if (!zoneName || typeof zoneName !== 'string') {
178
+ throw new Error('[extendModule] Zone name must be a non-empty string')
179
+ }
180
+ if (!block || !block.component) {
181
+ throw new Error('[extendModule] Block must have a component property')
182
+ }
183
+ if (!this._blocks.has(zoneName)) {
184
+ this._blocks.set(zoneName, [])
185
+ }
186
+ this._blocks.get(zoneName).push(block)
187
+ return this
188
+ }
189
+
190
+ /**
191
+ * Add multiple blocks to a zone
192
+ *
193
+ * @param {string} zoneName - Zone name
194
+ * @param {object[]} blocks - Array of block configurations
195
+ * @returns {this} For chaining
196
+ */
197
+ addBlocks(zoneName, blocks) {
198
+ if (!Array.isArray(blocks)) {
199
+ throw new Error('[extendModule] addBlocks expects an array')
200
+ }
201
+ for (const block of blocks) {
202
+ this.addBlock(zoneName, block)
203
+ }
204
+ return this
205
+ }
206
+
207
+ /**
208
+ * Register all extensions with the kernel
209
+ *
210
+ * @param {object} context - Registration context
211
+ * @param {object} context.hooks - HookRegistry instance
212
+ * @param {object} [context.zones] - ZoneRegistry instance (required for blocks)
213
+ * @param {number} [context.priority=50] - Hook priority
214
+ * @returns {Function} Cleanup function to remove all extensions
215
+ */
216
+ register(context) {
217
+ if (!context || !context.hooks) {
218
+ throw new Error('[extendModule] register() requires { hooks } context')
219
+ }
220
+
221
+ const { hooks, zones, priority = DEFAULT_PRIORITY } = context
222
+ const cleanupFns = []
223
+
224
+ // Register column alter hook
225
+ if (this._columns.length > 0) {
226
+ const hookName = `${this._target}:list:alter`
227
+ const columns = [...this._columns]
228
+ const unbind = hooks.register(hookName, (config) => {
229
+ if (!config.columns) {
230
+ config.columns = []
231
+ }
232
+ config.columns.push(...columns)
233
+ return config
234
+ }, { priority })
235
+ cleanupFns.push(unbind)
236
+ }
237
+
238
+ // Register field alter hook
239
+ if (this._fields.length > 0) {
240
+ const hookName = `${this._target}:form:alter`
241
+ const fields = [...this._fields]
242
+ const unbind = hooks.register(hookName, (config) => {
243
+ if (!config.fields) {
244
+ config.fields = []
245
+ }
246
+ config.fields.push(...fields)
247
+ return config
248
+ }, { priority })
249
+ cleanupFns.push(unbind)
250
+ }
251
+
252
+ // Register filter alter hook
253
+ if (this._filters.length > 0) {
254
+ const hookName = `${this._target}:filter:alter`
255
+ const filters = [...this._filters]
256
+ const unbind = hooks.register(hookName, (config) => {
257
+ if (!config.filters) {
258
+ config.filters = []
259
+ }
260
+ config.filters.push(...filters)
261
+ return config
262
+ }, { priority })
263
+ cleanupFns.push(unbind)
264
+ }
265
+
266
+ // Register zone blocks
267
+ if (this._blocks.size > 0) {
268
+ if (!zones) {
269
+ throw new Error('[extendModule] register() requires { zones } context when blocks are defined')
270
+ }
271
+ for (const [zoneName, blocks] of this._blocks) {
272
+ for (const block of blocks) {
273
+ zones.registerBlock(zoneName, block)
274
+ }
275
+ }
276
+ // Zone blocks cleanup: store zone/id pairs for removal
277
+ const blockIds = []
278
+ for (const [zoneName, blocks] of this._blocks) {
279
+ for (const block of blocks) {
280
+ if (block.id) {
281
+ blockIds.push({ zoneName, id: block.id })
282
+ }
283
+ }
284
+ }
285
+ if (blockIds.length > 0) {
286
+ cleanupFns.push(() => {
287
+ for (const { zoneName, id } of blockIds) {
288
+ zones.removeBlock(zoneName, id)
289
+ }
290
+ })
291
+ }
292
+ }
293
+
294
+ // Return cleanup function
295
+ return () => {
296
+ for (const cleanup of cleanupFns) {
297
+ cleanup()
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Get collected extensions as a config object
304
+ *
305
+ * Useful for debugging or serialization.
306
+ *
307
+ * @returns {object} Extensions config
308
+ */
309
+ toConfig() {
310
+ const config = {
311
+ target: this._target
312
+ }
313
+ if (this._columns.length > 0) {
314
+ config.columns = [...this._columns]
315
+ }
316
+ if (this._fields.length > 0) {
317
+ config.fields = [...this._fields]
318
+ }
319
+ if (this._filters.length > 0) {
320
+ config.filters = [...this._filters]
321
+ }
322
+ if (this._blocks.size > 0) {
323
+ config.blocks = {}
324
+ for (const [zoneName, blocks] of this._blocks) {
325
+ config.blocks[zoneName] = [...blocks]
326
+ }
327
+ }
328
+ return config
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Create an extension for a target module
334
+ *
335
+ * Can be used in two ways:
336
+ * 1. Fluent API: extendModule('target').addColumn(...).register({ hooks })
337
+ * 2. Config object: extendModule('target', { columns: [...] }, { hooks })
338
+ *
339
+ * @param {string} target - Target module name to extend
340
+ * @param {object} [extensions] - Optional extensions config object
341
+ * @param {object[]} [extensions.columns] - Columns to add to list view
342
+ * @param {object[]} [extensions.fields] - Fields to add to form view
343
+ * @param {object[]} [extensions.filters] - Filters to add to filter bar
344
+ * @param {Object.<string, object[]>} [extensions.blocks] - Blocks to add to zones
345
+ * @param {object} [context] - Registration context (required if extensions provided)
346
+ * @param {object} context.hooks - HookRegistry instance
347
+ * @param {object} [context.zones] - ZoneRegistry instance
348
+ * @param {number} [context.priority] - Hook priority
349
+ * @returns {ExtensionBuilder|Function} Builder for fluent API, or cleanup function if extensions provided
350
+ */
351
+ export function extendModule(target, extensions, context) {
352
+ const builder = new ExtensionBuilder(target)
353
+
354
+ // If no extensions provided, return builder for fluent API
355
+ if (!extensions) {
356
+ return builder
357
+ }
358
+
359
+ // Config object approach: apply extensions and register immediately
360
+ if (extensions.columns) {
361
+ builder.addColumns(extensions.columns)
362
+ }
363
+ if (extensions.fields) {
364
+ builder.addFields(extensions.fields)
365
+ }
366
+ if (extensions.filters) {
367
+ builder.addFilters(extensions.filters)
368
+ }
369
+ if (extensions.blocks) {
370
+ for (const [zoneName, blocks] of Object.entries(extensions.blocks)) {
371
+ builder.addBlocks(zoneName, blocks)
372
+ }
373
+ }
374
+
375
+ // Context required for config object approach
376
+ if (!context) {
377
+ throw new Error('[extendModule] Context { hooks } is required when extensions are provided')
378
+ }
379
+
380
+ return builder.register(context)
381
+ }
382
+
383
+ /**
384
+ * ExtensionBuilder class export for instanceof checks
385
+ */
386
+ export { ExtensionBuilder }