@open-mercato/shared 0.4.5-develop-03023b2707 → 0.4.5-develop-0c30cb4b11

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 (36) hide show
  1. package/dist/lib/bootstrap/factory.js +4 -0
  2. package/dist/lib/bootstrap/factory.js.map +2 -2
  3. package/dist/lib/crud/enricher-registry.js +47 -0
  4. package/dist/lib/crud/enricher-registry.js.map +7 -0
  5. package/dist/lib/crud/enricher-runner.js +242 -0
  6. package/dist/lib/crud/enricher-runner.js.map +7 -0
  7. package/dist/lib/crud/factory.js +53 -1
  8. package/dist/lib/crud/factory.js.map +2 -2
  9. package/dist/lib/crud/response-enricher.js +1 -0
  10. package/dist/lib/crud/response-enricher.js.map +7 -0
  11. package/dist/lib/version.js +1 -1
  12. package/dist/lib/version.js.map +1 -1
  13. package/dist/modules/events/factory.js +5 -0
  14. package/dist/modules/events/factory.js.map +2 -2
  15. package/dist/modules/registry.js.map +1 -1
  16. package/dist/modules/widgets/injection-loader.js +100 -40
  17. package/dist/modules/widgets/injection-loader.js.map +2 -2
  18. package/dist/modules/widgets/injection-position.js +48 -0
  19. package/dist/modules/widgets/injection-position.js.map +7 -0
  20. package/dist/modules/widgets/injection-progress.js +1 -0
  21. package/dist/modules/widgets/injection-progress.js.map +7 -0
  22. package/package.json +1 -1
  23. package/src/lib/bootstrap/factory.ts +6 -0
  24. package/src/lib/bootstrap/types.ts +6 -0
  25. package/src/lib/crud/enricher-registry.ts +68 -0
  26. package/src/lib/crud/enricher-runner.ts +329 -0
  27. package/src/lib/crud/factory.ts +79 -1
  28. package/src/lib/crud/response-enricher.ts +110 -0
  29. package/src/modules/events/factory.ts +9 -0
  30. package/src/modules/events/types.ts +2 -0
  31. package/src/modules/registry.ts +2 -2
  32. package/src/modules/widgets/__tests__/injection-position.test.ts +33 -0
  33. package/src/modules/widgets/injection-loader.ts +140 -50
  34. package/src/modules/widgets/injection-position.ts +59 -0
  35. package/src/modules/widgets/injection-progress.ts +35 -0
  36. package/src/modules/widgets/injection.ts +280 -3
@@ -1,5 +1,7 @@
1
1
  import type { ModuleInjectionWidgetEntry } from '../registry'
2
2
  import type {
3
+ InjectionAnyWidgetModule,
4
+ InjectionDataWidgetModule,
3
5
  InjectionWidgetMetadata,
4
6
  InjectionWidgetModule,
5
7
  InjectionSpotId,
@@ -9,6 +11,8 @@ import type {
9
11
  } from './injection'
10
12
 
11
13
  type LoadedWidgetModule = InjectionWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }
14
+ type LoadedDataWidgetModule = InjectionDataWidgetModule & { metadata: InjectionWidgetMetadata }
15
+
12
16
  export type LoadedInjectionWidget = LoadedWidgetModule & {
13
17
  moduleId: string
14
18
  key: string
@@ -22,6 +26,19 @@ export type LoadedInjectionWidget = LoadedWidgetModule & {
22
26
  }
23
27
  }
24
28
 
29
+ export type LoadedInjectionDataWidget = LoadedDataWidgetModule & {
30
+ moduleId: string
31
+ key: string
32
+ placement?: {
33
+ groupId?: string
34
+ groupLabel?: string
35
+ groupDescription?: string
36
+ column?: 1 | 2
37
+ kind?: 'tab' | 'group' | 'stack'
38
+ [k: string]: unknown
39
+ }
40
+ }
41
+
25
42
  type WidgetEntry = ModuleInjectionWidgetEntry & { moduleId: string }
26
43
 
27
44
  // Registration pattern for publishable packages
@@ -142,7 +159,6 @@ async function loadWidgetEntries(): Promise<WidgetEntry[]> {
142
159
  }))
143
160
  )
144
161
  widgetEntriesPromise = promise.catch((err) => {
145
- // Clear cache on error so next call can retry after registration
146
162
  if (widgetEntriesPromise === promise) {
147
163
  widgetEntriesPromise = null
148
164
  }
@@ -190,7 +206,6 @@ async function loadInjectionTable(): Promise<Map<InjectionSpotId, TableEntry[]>>
190
206
  return table
191
207
  })
192
208
  injectionTablePromise = promise.catch((err) => {
193
- // Clear cache on error so next call can retry after registration
194
209
  if (injectionTablePromise === promise) {
195
210
  injectionTablePromise = null
196
211
  }
@@ -200,49 +215,115 @@ async function loadInjectionTable(): Promise<Map<InjectionSpotId, TableEntry[]>>
200
215
  return injectionTablePromise
201
216
  }
202
217
 
203
- const widgetCache = new Map<string, Promise<LoadedWidgetModule>>()
218
+ const widgetCache = new Map<string, Promise<InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }>>()
219
+
220
+ function isDataWidgetModule(widget: Record<string, unknown>): widget is LoadedDataWidgetModule {
221
+ const keys = [
222
+ 'columns',
223
+ 'rowActions',
224
+ 'bulkActions',
225
+ 'filters',
226
+ 'fields',
227
+ 'steps',
228
+ 'badge',
229
+ 'menuItems',
230
+ ]
231
+ return keys.some((key) => key in widget)
232
+ }
204
233
 
205
- function ensureValidWidgetModule(mod: any, key: string, moduleId: string): LoadedWidgetModule {
234
+ function ensureValidInjectionModule(mod: unknown, key: string, moduleId: string): (InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }) {
206
235
  if (!mod || typeof mod !== 'object') {
207
236
  throw new Error(`Invalid injection widget module "${key}" from "${moduleId}" (expected object export)`)
208
237
  }
209
- const widget = (mod.default ?? mod) as InjectionWidgetModule<any, any>
238
+ const widget = (mod as { default?: InjectionAnyWidgetModule<any, any> }).default ?? (mod as InjectionAnyWidgetModule<any, any>)
210
239
  if (!widget || typeof widget !== 'object') {
211
- throw new Error(`Invalid injection widget export "${key}" from "${moduleId}" (missing default export)`)
240
+ throw new Error(`Invalid injection widget export "${key}" from "${moduleId}" (missing default export)`)
212
241
  }
213
- if (!widget.metadata || typeof widget.metadata !== 'object') {
242
+ if (!('metadata' in widget) || !widget.metadata || typeof widget.metadata !== 'object') {
214
243
  throw new Error(`Injection widget "${key}" from "${moduleId}" is missing metadata`)
215
244
  }
216
- const { metadata } = widget
245
+ const metadata = widget.metadata
217
246
  if (typeof metadata.id !== 'string' || metadata.id.length === 0) {
218
247
  throw new Error(`Injection widget "${key}" from "${moduleId}" metadata.id must be a non-empty string`)
219
248
  }
220
- if (typeof metadata.title !== 'string' || metadata.title.length === 0) {
221
- throw new Error(`Injection widget "${metadata.id}" from "${moduleId}" must have a title`)
222
- }
223
- return {
249
+ const normalized = {
224
250
  ...widget,
225
251
  metadata,
226
252
  }
253
+
254
+ if ('Widget' in normalized && typeof normalized.Widget === 'function') {
255
+ if (typeof metadata.title !== 'string' || metadata.title.length === 0) {
256
+ throw new Error(`Injection widget "${metadata.id}" from "${moduleId}" must have a title`)
257
+ }
258
+ return normalized
259
+ }
260
+
261
+ if (!isDataWidgetModule(normalized as Record<string, unknown>)) {
262
+ throw new Error(
263
+ `Injection widget "${metadata.id}" from "${moduleId}" must export either Widget component or a declarative data payload`
264
+ )
265
+ }
266
+
267
+ return normalized
227
268
  }
228
269
 
229
- async function loadEntry(entry: WidgetEntry): Promise<LoadedWidgetModule> {
270
+ function isLoadedInjectionWidget(
271
+ module: InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }
272
+ ): module is LoadedWidgetModule {
273
+ return 'Widget' in module && typeof module.Widget === 'function'
274
+ }
275
+
276
+ function isLoadedInjectionDataWidget(
277
+ module: InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }
278
+ ): module is LoadedDataWidgetModule {
279
+ return !isLoadedInjectionWidget(module)
280
+ }
281
+
282
+ async function loadEntry(entry: WidgetEntry): Promise<InjectionAnyWidgetModule<any, any> & { metadata: InjectionWidgetMetadata }> {
230
283
  if (!widgetCache.has(entry.key)) {
231
- const promise = entry.loader()
232
- .then((mod) => ensureValidWidgetModule(mod, entry.key, entry.moduleId))
284
+ const promise = entry.loader().then((mod) => ensureValidInjectionModule(mod, entry.key, entry.moduleId))
233
285
  widgetCache.set(entry.key, promise)
234
286
  }
235
287
  return widgetCache.get(entry.key)!
236
288
  }
237
289
 
290
+ async function getResolvedEntriesForSpot(spotId: InjectionSpotId): Promise<TableEntry[]> {
291
+ const table = await loadInjectionTable()
292
+ const exactEntries = table.get(spotId) ?? []
293
+ const wildcardEntries: TableEntry[] = []
294
+
295
+ for (const [candidateSpotId, candidateEntries] of table.entries()) {
296
+ if (candidateSpotId === spotId) continue
297
+ if (!candidateSpotId.includes('*')) continue
298
+ const pattern = new RegExp(`^${candidateSpotId.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`)
299
+ if (!pattern.test(spotId)) continue
300
+ wildcardEntries.push(...candidateEntries)
301
+ }
302
+
303
+ const dedupedEntries = new Map<string, TableEntry>()
304
+ for (const entry of [...exactEntries, ...wildcardEntries]) {
305
+ const cacheKey = `${entry.moduleId}:${entry.widgetId}`
306
+ const previous = dedupedEntries.get(cacheKey)
307
+ if (!previous || (entry.priority ?? 0) > (previous.priority ?? 0)) {
308
+ dedupedEntries.set(cacheKey, entry)
309
+ }
310
+ }
311
+
312
+ return Array.from(dedupedEntries.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
313
+ }
314
+
238
315
  export async function loadAllInjectionWidgets(): Promise<LoadedInjectionWidget[]> {
239
316
  const widgetEntries = await loadWidgetEntries()
240
- const loaded = await Promise.all(widgetEntries.map(async (entry) => {
241
- const widget = await loadEntry(entry)
242
- return { ...widget, moduleId: entry.moduleId, key: entry.key }
243
- }))
244
- const byId = new Map<string, LoadedWidgetModule & { moduleId: string; key: string }>()
317
+ const loaded = await Promise.all(
318
+ widgetEntries.map(async (entry) => {
319
+ const module = await loadEntry(entry)
320
+ if (!isLoadedInjectionWidget(module)) return null
321
+ return { ...module, moduleId: entry.moduleId, key: entry.key }
322
+ })
323
+ )
324
+ const byId = new Map<string, LoadedInjectionWidget>()
245
325
  for (const widget of loaded) {
326
+ if (!widget) continue
246
327
  if (!byId.has(widget.metadata.id)) {
247
328
  byId.set(widget.metadata.id, widget)
248
329
  }
@@ -253,42 +334,51 @@ export async function loadAllInjectionWidgets(): Promise<LoadedInjectionWidget[]
253
334
  export async function loadInjectionWidgetById(widgetId: string): Promise<LoadedInjectionWidget | null> {
254
335
  const widgetEntries = await loadWidgetEntries()
255
336
  for (const entry of widgetEntries) {
256
- const widget = await loadEntry(entry)
257
- if (widget.metadata.id === widgetId) {
258
- return { ...widget, moduleId: entry.moduleId, key: entry.key }
337
+ const module = await loadEntry(entry)
338
+ if (!isLoadedInjectionWidget(module)) continue
339
+ if (module.metadata.id === widgetId) {
340
+ return { ...module, moduleId: entry.moduleId, key: entry.key }
341
+ }
342
+ }
343
+ return null
344
+ }
345
+
346
+ export async function loadInjectionDataWidgetById(widgetId: string): Promise<LoadedInjectionDataWidget | null> {
347
+ const widgetEntries = await loadWidgetEntries()
348
+ for (const entry of widgetEntries) {
349
+ const module = await loadEntry(entry)
350
+ if (!isLoadedInjectionDataWidget(module)) continue
351
+ if (module.metadata.id === widgetId) {
352
+ return { ...module, moduleId: entry.moduleId, key: entry.key }
259
353
  }
260
354
  }
261
355
  return null
262
356
  }
263
357
 
264
358
  export async function loadInjectionWidgetsForSpot(spotId: InjectionSpotId): Promise<LoadedInjectionWidget[]> {
265
- const table = await loadInjectionTable()
266
- const exactEntries = table.get(spotId) ?? []
267
- const wildcardEntries: TableEntry[] = []
268
- for (const [candidateSpotId, candidateEntries] of table.entries()) {
269
- if (candidateSpotId === spotId) continue
270
- if (!candidateSpotId.includes('*')) continue
271
- const pattern = new RegExp(`^${candidateSpotId.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`)
272
- if (!pattern.test(spotId)) continue
273
- wildcardEntries.push(...candidateEntries)
359
+ const entries = await getResolvedEntriesForSpot(spotId)
360
+ const widgets: LoadedInjectionWidget[] = []
361
+ for (const { widgetId, placement, priority } of entries) {
362
+ const widget = await loadInjectionWidgetById(widgetId)
363
+ if (!widget) continue
364
+ const combinedPlacement = placement
365
+ ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }
366
+ : { priority: typeof priority === 'number' ? priority : 0 }
367
+ widgets.push({ ...widget, placement: combinedPlacement })
274
368
  }
275
- const dedupedEntries = new Map<string, TableEntry>()
276
- for (const entry of [...exactEntries, ...wildcardEntries]) {
277
- const key = `${entry.moduleId}:${entry.widgetId}`
278
- const previous = dedupedEntries.get(key)
279
- if (!previous || (entry.priority ?? 0) > (previous.priority ?? 0)) {
280
- dedupedEntries.set(key, entry)
281
- }
369
+ return widgets
370
+ }
371
+
372
+ export async function loadInjectionDataWidgetsForSpot(spotId: InjectionSpotId): Promise<LoadedInjectionDataWidget[]> {
373
+ const entries = await getResolvedEntriesForSpot(spotId)
374
+ const widgets: LoadedInjectionDataWidget[] = []
375
+ for (const { widgetId, placement, priority } of entries) {
376
+ const widget = await loadInjectionDataWidgetById(widgetId)
377
+ if (!widget) continue
378
+ const combinedPlacement = placement
379
+ ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }
380
+ : { priority: typeof priority === 'number' ? priority : 0 }
381
+ widgets.push({ ...widget, placement: combinedPlacement })
282
382
  }
283
- const entries = Array.from(dedupedEntries.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
284
- const widgets = await Promise.all(
285
- entries.map(async ({ widgetId, placement, priority }) => {
286
- const widget = await loadInjectionWidgetById(widgetId)
287
- const combinedPlacement = placement
288
- ? { ...placement, priority: typeof priority === 'number' ? priority : 0 }
289
- : { priority: typeof priority === 'number' ? priority : 0 }
290
- return widget ? { ...widget, placement: combinedPlacement } : null
291
- })
292
- )
293
- return widgets.filter((w): w is NonNullable<typeof w> => w !== null)
383
+ return widgets
294
384
  }
@@ -0,0 +1,59 @@
1
+ export enum InjectionPosition {
2
+ Before = 'before',
3
+ After = 'after',
4
+ First = 'first',
5
+ Last = 'last',
6
+ }
7
+
8
+ export type InjectionPlacement = {
9
+ position?: InjectionPosition
10
+ relativeTo?: string
11
+ }
12
+
13
+ export function getInjectionPosition(placement?: InjectionPlacement): InjectionPosition {
14
+ if (!placement?.position) return InjectionPosition.Last
15
+ return placement.position
16
+ }
17
+
18
+ function warnInvalidRelativeTo(relativeTo: string | undefined) {
19
+ if (process.env.NODE_ENV !== 'development') return
20
+ if (!relativeTo) return
21
+ console.warn(`[InjectionPlacement] relativeTo target "${relativeTo}" not found, appending item at the end.`)
22
+ }
23
+
24
+ export function insertByInjectionPlacement<T>(
25
+ items: T[],
26
+ item: T,
27
+ placement: InjectionPlacement | undefined,
28
+ getItemId: (value: T) => string,
29
+ ): T[] {
30
+ const current = [...items]
31
+ const position = getInjectionPosition(placement)
32
+
33
+ if (position === InjectionPosition.First) {
34
+ current.unshift(item)
35
+ return current
36
+ }
37
+
38
+ if (position === InjectionPosition.Last) {
39
+ current.push(item)
40
+ return current
41
+ }
42
+
43
+ const relativeTo = placement?.relativeTo
44
+ if (!relativeTo) {
45
+ current.push(item)
46
+ return current
47
+ }
48
+
49
+ const targetIndex = current.findIndex((value) => getItemId(value) === relativeTo)
50
+ if (targetIndex < 0) {
51
+ warnInvalidRelativeTo(relativeTo)
52
+ current.push(item)
53
+ return current
54
+ }
55
+
56
+ const insertIndex = position === InjectionPosition.Before ? targetIndex : targetIndex + 1
57
+ current.splice(insertIndex, 0, item)
58
+ return current
59
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Async Operation Progress Types
3
+ *
4
+ * Standard contract for real-time progress tracking of long-running operations
5
+ * (data sync imports, bulk exports, webhook replay). These events are delivered
6
+ * via the DOM Event Bridge (clientBroadcast: true) and consumed by the
7
+ * `useOperationProgress` hook.
8
+ */
9
+
10
+ /**
11
+ * Structured progress payload emitted by server-side workers.
12
+ * Workers emit these as standard events with `clientBroadcast: true`.
13
+ */
14
+ export interface OperationProgressEvent {
15
+ /** Unique operation ID (e.g., syncRunId) */
16
+ operationId: string
17
+ /** Operation type identifier (e.g., 'sync.import', 'bulk.export') */
18
+ operationType: string
19
+ /** Current status of the operation */
20
+ status: 'running' | 'completed' | 'failed' | 'cancelled'
21
+ /** Progress percentage (0-100) */
22
+ progress: number
23
+ /** Number of items processed so far */
24
+ processedCount: number
25
+ /** Total number of items to process */
26
+ totalCount: number
27
+ /** Human-readable current step name */
28
+ currentStep?: string
29
+ /** Number of errors encountered */
30
+ errors: number
31
+ /** Timestamp when the operation started */
32
+ startedAt: number
33
+ /** Additional operation-specific metadata */
34
+ metadata?: Record<string, unknown>
35
+ }
@@ -1,9 +1,52 @@
1
- import type { ComponentType } from 'react'
1
+ import type { ComponentType, LazyExoticComponent, ReactNode } from 'react'
2
+ import type { InjectionPlacement } from './injection-position'
2
3
 
3
4
  /**
4
- * Widget injection event handlers for lifecycle management
5
+ * Result returned by `onFieldChange` handlers.
6
+ */
7
+ export type FieldChangeResult = {
8
+ /** Override the field value */
9
+ value?: unknown
10
+ /** Set other fields as side effects */
11
+ sideEffects?: Record<string, unknown>
12
+ /** Display a message to the user near the field */
13
+ message?: { text: string; severity: 'info' | 'warning' | 'error' }
14
+ }
15
+
16
+ /**
17
+ * Result returned by `onBeforeNavigate` handlers.
18
+ */
19
+ export type NavigateGuardResult = {
20
+ /** Whether navigation should proceed */
21
+ ok: boolean
22
+ /** Reason shown to the user when navigation is blocked */
23
+ message?: string
24
+ }
25
+
26
+ /**
27
+ * Payload delivered by the DOM Event Bridge for server-side app events.
28
+ */
29
+ export type AppEventPayload = {
30
+ /** Event identifier (e.g., 'example.todo.created') */
31
+ id: string
32
+ /** Event-specific payload data */
33
+ payload: Record<string, unknown>
34
+ /** Server timestamp when the event was emitted */
35
+ timestamp: number
36
+ /** Organization the event belongs to */
37
+ organizationId: string
38
+ }
39
+
40
+ /**
41
+ * Widget injection event handlers for lifecycle management.
42
+ *
43
+ * Handlers are classified into two categories:
44
+ * - **Action events**: Fire-and-forget or gate handlers (accumulate requestHeaders, check ok boolean)
45
+ * - **Transformer events**: Pipeline handlers where output of widget N becomes input of widget N+1
5
46
  */
6
47
  export type WidgetInjectionEventHandlers<TContext = unknown, TData = unknown> = {
48
+ // === Existing: Lifecycle Actions ===
49
+
7
50
  /**
8
51
  * Called when the widget is first loaded/mounted
9
52
  */
@@ -47,6 +90,52 @@ export type WidgetInjectionEventHandlers<TContext = unknown, TData = unknown> =
47
90
  * Called when delete action fails.
48
91
  */
49
92
  onDeleteError?: (data: TData, context: TContext, error: unknown) => void | Promise<void>
93
+
94
+ // === New: DOM-Inspired Lifecycle (Phase C) ===
95
+
96
+ /**
97
+ * Called when a form field value changes. Can return side-effects and user messages.
98
+ * Action event — called for each widget independently.
99
+ */
100
+ onFieldChange?: (fieldId: string, value: unknown, data: TData, context: TContext) => Promise<FieldChangeResult | void>
101
+
102
+ /**
103
+ * Called before navigating away from the current page. Can block navigation.
104
+ * Action event — first widget returning `ok: false` stops navigation.
105
+ */
106
+ onBeforeNavigate?: (target: string, context: TContext) => Promise<NavigateGuardResult>
107
+
108
+ /**
109
+ * Called when the widget's visibility changes (e.g., tab switches).
110
+ * Action event — fire-and-forget.
111
+ */
112
+ onVisibilityChange?: (visible: boolean, context: TContext) => Promise<void>
113
+
114
+ /**
115
+ * Called when an app event matching the widget's subscription arrives via the DOM Event Bridge.
116
+ * Action event — fire-and-forget.
117
+ */
118
+ onAppEvent?: (event: AppEventPayload, context: TContext) => Promise<void>
119
+
120
+ // === New: Data Transformation Pipelines (Phase C) ===
121
+
122
+ /**
123
+ * Transform form data before submission. Output of widget N becomes input of widget N+1.
124
+ * Transformer event — pipeline dispatch.
125
+ */
126
+ transformFormData?: (data: TData, context: TContext) => Promise<TData>
127
+
128
+ /**
129
+ * Transform data for display purposes. Output of widget N becomes input of widget N+1.
130
+ * Transformer event — pipeline dispatch.
131
+ */
132
+ transformDisplayData?: (data: TData, context: TContext) => Promise<TData>
133
+
134
+ /**
135
+ * Transform validation errors. Output of widget N becomes input of widget N+1.
136
+ * Transformer event — pipeline dispatch.
137
+ */
138
+ transformValidation?: (errors: Record<string, string>, data: TData, context: TContext) => Promise<Record<string, string>>
50
139
  }
51
140
 
52
141
  /**
@@ -54,7 +143,7 @@ export type WidgetInjectionEventHandlers<TContext = unknown, TData = unknown> =
54
143
  */
55
144
  export type InjectionWidgetMetadata = {
56
145
  id: string
57
- title: string
146
+ title?: string
58
147
  description?: string
59
148
  features?: string[]
60
149
  priority?: number
@@ -123,6 +212,194 @@ export type InjectionWidgetModule<TContext = unknown, TData = unknown> = {
123
212
  eventHandlers?: WidgetInjectionEventHandlers<TContext, TData>
124
213
  }
125
214
 
215
+ export type InjectionColumnDefinition = {
216
+ id: string
217
+ header: string
218
+ accessorKey: string
219
+ cell?: (props: { getValue: () => unknown }) => ReactNode
220
+ size?: number
221
+ sortable?: boolean
222
+ placement?: InjectionPlacement
223
+ }
224
+
225
+ export type InjectionRowActionDefinition = {
226
+ id: string
227
+ label: string
228
+ icon?: string
229
+ onSelect: (row: unknown, context: unknown) => void
230
+ placement?: InjectionPlacement
231
+ }
232
+
233
+ export type InjectionBulkActionDefinition = {
234
+ id: string
235
+ label: string
236
+ icon?: string
237
+ onExecute: (selectedRows: unknown[], context: unknown) => Promise<void>
238
+ }
239
+
240
+ export type InjectionFilterDefinition = {
241
+ id: string
242
+ label: string
243
+ type: 'select' | 'text' | 'date-range' | 'boolean'
244
+ options?: { value: string; label: string }[]
245
+ strategy: 'server' | 'client'
246
+ queryParam?: string
247
+ enrichedField?: string
248
+ }
249
+
250
+ export type FieldVisibilityCondition<TContext = unknown> = (
251
+ values: Record<string, unknown>,
252
+ context: TContext,
253
+ ) => boolean
254
+
255
+ export type CustomFieldProps<TContext = unknown> = {
256
+ value: unknown
257
+ onChange: (value: unknown) => void
258
+ context: TContext
259
+ disabled?: boolean
260
+ }
261
+
262
+ export type FieldContext = {
263
+ organizationId?: string | null
264
+ tenantId?: string | null
265
+ userId?: string | null
266
+ record?: Record<string, unknown>
267
+ }
268
+
269
+ export type InjectionFieldDefinition = {
270
+ id: string
271
+ label: string
272
+ type: 'text' | 'select' | 'number' | 'date' | 'boolean' | 'textarea' | 'custom'
273
+ options?: { value: string; label: string }[]
274
+ optionsLoader?: (context: FieldContext) => Promise<{ value: string; label: string }[]>
275
+ optionsCacheTtl?: number
276
+ customComponent?: LazyExoticComponent<ComponentType<CustomFieldProps>>
277
+ group: string
278
+ placement?: InjectionPlacement
279
+ readOnly?: boolean
280
+ visibleWhen?: FieldVisibilityCondition
281
+ }
282
+
283
+ export type WizardStepProps<TContext = unknown> = {
284
+ data: Record<string, unknown>
285
+ setData: (next: Record<string, unknown>) => void
286
+ context: TContext
287
+ }
288
+
289
+ export type InjectionContext = {
290
+ organizationId?: string | null
291
+ tenantId?: string | null
292
+ userId?: string | null
293
+ path?: string
294
+ [k: string]: unknown
295
+ }
296
+
297
+ export type InjectionWizardStep = {
298
+ id: string
299
+ label: string
300
+ fields?: InjectionFieldDefinition[]
301
+ customComponent?: LazyExoticComponent<ComponentType<WizardStepProps>>
302
+ validate?: (
303
+ data: Record<string, unknown>,
304
+ context: InjectionContext,
305
+ ) => Promise<{ ok: boolean; message?: string }>
306
+ }
307
+
308
+ export type InjectionWizardWidget = {
309
+ metadata: InjectionWidgetMetadata
310
+ kind: 'wizard'
311
+ steps: InjectionWizardStep[]
312
+ onComplete?: (stepData: Record<string, unknown>, context: InjectionContext) => Promise<void>
313
+ eventHandlers?: WidgetInjectionEventHandlers<InjectionContext, Record<string, unknown>>
314
+ }
315
+
316
+ export type StatusBadgeResult = {
317
+ status: 'healthy' | 'warning' | 'error' | 'unknown'
318
+ tooltip?: string
319
+ count?: number
320
+ }
321
+
322
+ export type StatusBadgeContext = {
323
+ organizationId: string
324
+ tenantId: string
325
+ userId: string
326
+ }
327
+
328
+ export type InjectionStatusBadgeWidget = {
329
+ metadata: InjectionWidgetMetadata
330
+ kind: 'status-badge'
331
+ badge: {
332
+ label: string
333
+ statusLoader: (context: StatusBadgeContext) => Promise<StatusBadgeResult>
334
+ href?: string
335
+ pollInterval?: number
336
+ }
337
+ }
338
+
339
+ export type InjectionMenuItem = {
340
+ id: string
341
+ label: string
342
+ labelKey?: string
343
+ icon?: string
344
+ href?: string
345
+ onClick?: () => void
346
+ separator?: boolean
347
+ placement?: InjectionPlacement
348
+ features?: string[]
349
+ roles?: string[]
350
+ badge?: string | number
351
+ children?: Omit<InjectionMenuItem, 'children'>[]
352
+ groupId?: string
353
+ groupLabel?: string
354
+ groupLabelKey?: string
355
+ groupOrder?: number
356
+ }
357
+
358
+ export type InjectionMenuItemWidget = {
359
+ metadata: InjectionWidgetMetadata
360
+ menuItems: InjectionMenuItem[]
361
+ }
362
+
363
+ export type InjectionColumnWidget = {
364
+ metadata: InjectionWidgetMetadata
365
+ columns: InjectionColumnDefinition[]
366
+ }
367
+
368
+ export type InjectionRowActionWidget = {
369
+ metadata: InjectionWidgetMetadata
370
+ rowActions: InjectionRowActionDefinition[]
371
+ }
372
+
373
+ export type InjectionBulkActionWidget = {
374
+ metadata: InjectionWidgetMetadata
375
+ bulkActions: InjectionBulkActionDefinition[]
376
+ }
377
+
378
+ export type InjectionFilterWidget = {
379
+ metadata: InjectionWidgetMetadata
380
+ filters: InjectionFilterDefinition[]
381
+ }
382
+
383
+ export type InjectionFieldWidget = {
384
+ metadata: InjectionWidgetMetadata
385
+ fields: InjectionFieldDefinition[]
386
+ eventHandlers?: WidgetInjectionEventHandlers<InjectionContext, Record<string, unknown>>
387
+ }
388
+
389
+ export type InjectionDataWidgetModule =
390
+ | InjectionColumnWidget
391
+ | InjectionRowActionWidget
392
+ | InjectionBulkActionWidget
393
+ | InjectionFilterWidget
394
+ | InjectionFieldWidget
395
+ | InjectionWizardWidget
396
+ | InjectionStatusBadgeWidget
397
+ | InjectionMenuItemWidget
398
+
399
+ export type InjectionAnyWidgetModule<TContext = unknown, TData = unknown> =
400
+ | InjectionWidgetModule<TContext, TData>
401
+ | InjectionDataWidgetModule
402
+
126
403
  /**
127
404
  * Injection spot identifier - uniquely identifies where widgets can be injected
128
405
  */