@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.
- package/dist/lib/bootstrap/factory.js +4 -0
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/crud/enricher-registry.js +47 -0
- package/dist/lib/crud/enricher-registry.js.map +7 -0
- package/dist/lib/crud/enricher-runner.js +242 -0
- package/dist/lib/crud/enricher-runner.js.map +7 -0
- package/dist/lib/crud/factory.js +53 -1
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/response-enricher.js +1 -0
- package/dist/lib/crud/response-enricher.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +5 -0
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/registry.js.map +1 -1
- package/dist/modules/widgets/injection-loader.js +100 -40
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/dist/modules/widgets/injection-position.js +48 -0
- package/dist/modules/widgets/injection-position.js.map +7 -0
- package/dist/modules/widgets/injection-progress.js +1 -0
- package/dist/modules/widgets/injection-progress.js.map +7 -0
- package/package.json +1 -1
- package/src/lib/bootstrap/factory.ts +6 -0
- package/src/lib/bootstrap/types.ts +6 -0
- package/src/lib/crud/enricher-registry.ts +68 -0
- package/src/lib/crud/enricher-runner.ts +329 -0
- package/src/lib/crud/factory.ts +79 -1
- package/src/lib/crud/response-enricher.ts +110 -0
- package/src/modules/events/factory.ts +9 -0
- package/src/modules/events/types.ts +2 -0
- package/src/modules/registry.ts +2 -2
- package/src/modules/widgets/__tests__/injection-position.test.ts +33 -0
- package/src/modules/widgets/injection-loader.ts +140 -50
- package/src/modules/widgets/injection-position.ts +59 -0
- package/src/modules/widgets/injection-progress.ts +35 -0
- 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<
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
257
|
-
if (
|
|
258
|
-
|
|
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
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
*/
|