@open-mercato/ui 0.6.6-develop.5523.1.e223ca1915 → 0.6.6-develop.5531.1.ab1959dfae

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.
@@ -31,6 +31,14 @@ function useInjectionWidgets(spotId, options) {
31
31
  const [error, setError] = React.useState(null);
32
32
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion());
33
33
  const loadedRef = React.useRef(false);
34
+ const contextRef = React.useRef(options?.context);
35
+ const triggerOnLoadRef = React.useRef(options?.triggerOnLoad);
36
+ const onEventRef = React.useRef(options?.onEvent);
37
+ React.useEffect(() => {
38
+ contextRef.current = options?.context;
39
+ triggerOnLoadRef.current = options?.triggerOnLoad;
40
+ onEventRef.current = options?.onEvent;
41
+ });
34
42
  React.useEffect(() => {
35
43
  return subscribeToInjectionRegistryChanges(() => {
36
44
  setRegistryVersion(getInjectionRegistryVersion());
@@ -58,14 +66,14 @@ function useInjectionWidgets(spotId, options) {
58
66
  placement: w.placement
59
67
  }));
60
68
  setWidgets(widgetList);
61
- if (!loadedRef.current && options?.triggerOnLoad) {
69
+ if (!loadedRef.current && triggerOnLoadRef.current) {
62
70
  loadedRef.current = true;
63
71
  for (const widget of widgetList) {
64
72
  if (widget.module.eventHandlers?.onLoad) {
65
73
  try {
66
- const widgetContext = injectSharedStateIntoContext(options.context, widget.moduleId);
74
+ const widgetContext = injectSharedStateIntoContext(contextRef.current, widget.moduleId);
67
75
  await widget.module.eventHandlers.onLoad(widgetContext);
68
- options.onEvent?.("onLoad", widget.widgetId);
76
+ onEventRef.current?.("onLoad", widget.widgetId);
69
77
  } catch (err) {
70
78
  console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err);
71
79
  }
@@ -84,7 +92,7 @@ function useInjectionWidgets(spotId, options) {
84
92
  return () => {
85
93
  mounted = false;
86
94
  };
87
- }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent, registryVersion]);
95
+ }, [spotId, registryVersion]);
88
96
  return { widgets, loading, error };
89
97
  }
90
98
  function InjectionSpot({
@@ -97,15 +105,24 @@ function InjectionSpot({
97
105
  widgetsOverride
98
106
  }) {
99
107
  const useSpotId = widgetsOverride ? null : spotId;
108
+ const onEventRef = React.useRef(onEvent);
109
+ React.useEffect(() => {
110
+ onEventRef.current = onEvent;
111
+ });
112
+ const hasOnEvent = Boolean(onEvent);
113
+ const stableOnEvent = React.useMemo(
114
+ () => hasOnEvent ? (event, id) => onEventRef.current?.(event, id) : void 0,
115
+ [hasOnEvent]
116
+ );
100
117
  const { widgets, loading, error } = useInjectionWidgets(useSpotId, {
101
118
  context,
102
119
  triggerOnLoad: !widgetsOverride,
103
- onEvent: onEvent ? (event, id) => onEvent(event, id) : void 0
120
+ onEvent: stableOnEvent
104
121
  });
105
122
  const effectiveWidgets = widgetsOverride ?? widgets;
106
123
  const effectiveLoading = widgetsOverride ? false : loading;
107
124
  const effectiveError = widgetsOverride ? null : error;
108
- if (effectiveLoading) {
125
+ if (effectiveLoading && effectiveWidgets.length === 0) {
109
126
  return null;
110
127
  }
111
128
  if (effectiveError) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/injection/InjectionSpot.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type {\n InjectionSpotId,\n InjectionWidgetModule,\n WidgetInjectionEventHandlers,\n WidgetBeforeDeleteResult,\n WidgetBeforeSaveResult,\n FieldChangeResult,\n NavigateGuardResult,\n} from '@open-mercato/shared/modules/widgets/injection'\nimport {\n getInjectionRegistryVersion,\n loadInjectionWidgetsForSpot,\n subscribeToInjectionRegistryChanges,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { getWidgetSharedState } from './WidgetSharedState'\n\nexport type InjectionSpotProps<TContext = unknown, TData = unknown> = {\n spotId: InjectionSpotId\n context: TContext\n data?: TData\n onDataChange?: (data: TData) => void\n disabled?: boolean\n onEvent?: (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n widgetId: string,\n ) => void\n widgetsOverride?: LoadedWidget[]\n}\n\n/**\n * Transformer events use pipeline dispatch: output of widget N becomes input of widget N+1.\n */\nconst TRANSFORMER_EVENTS = new Set<string>([\n 'transformFormData',\n 'transformDisplayData',\n 'transformValidation',\n])\n\ntype LoadedWidget = {\n widgetId: string\n module: InjectionWidgetModule<any, any>\n moduleId: string\n key: string\n placement?: LoadedInjectionWidget['placement']\n}\n\nexport type LoadedInjectionSpotWidget = LoadedWidget\n\nfunction injectSharedStateIntoContext<TContext>(context: TContext, moduleId: string): TContext {\n const sharedState = getWidgetSharedState(moduleId)\n if (typeof context === 'object' && context !== null && !Array.isArray(context)) {\n return {\n ...(context as Record<string, unknown>),\n sharedState,\n } as TContext\n }\n return {\n value: context,\n sharedState,\n } as TContext\n}\n\nexport function useInjectionWidgets<TContext = unknown>(\n spotId: InjectionSpotId | null | undefined,\n options?: {\n context?: TContext\n triggerOnLoad?: boolean\n onEvent?: (event: 'onLoad', widgetId: string) => void\n }\n) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n const loadedRef = React.useRef(false)\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (!spotId) {\n setWidgets([])\n setLoading(false)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n const widgetList: LoadedWidget[] = loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n setWidgets(widgetList)\n \n // Trigger onLoad for all widgets\n if (!loadedRef.current && options?.triggerOnLoad) {\n loadedRef.current = true\n for (const widget of widgetList) {\n if (widget.module.eventHandlers?.onLoad) {\n try {\n const widgetContext = injectSharedStateIntoContext(options.context as TContext, widget.moduleId)\n await widget.module.eventHandlers.onLoad(widgetContext)\n options.onEvent?.('onLoad', widget.widgetId)\n } catch (err) {\n console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n if (mounted) setLoading(false)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent, registryVersion])\n\n return { widgets, loading, error }\n}\n\nexport function InjectionSpot<TContext = unknown, TData = unknown>({\n spotId,\n context,\n data,\n onDataChange,\n disabled,\n onEvent,\n widgetsOverride,\n}: InjectionSpotProps<TContext, TData>) {\n const useSpotId = widgetsOverride ? null : spotId\n const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {\n context,\n triggerOnLoad: !widgetsOverride,\n onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,\n })\n const effectiveWidgets = widgetsOverride ?? widgets\n const effectiveLoading = widgetsOverride ? false : loading\n const effectiveError = widgetsOverride ? null : error\n\n if (effectiveLoading) {\n return null\n }\n\n if (effectiveError) {\n console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)\n return null\n }\n\n if (effectiveWidgets.length === 0) {\n return null\n }\n\n return (\n <>\n {effectiveWidgets.map((widget) => {\n const { Widget } = widget.module\n return (\n <Widget\n key={widget.widgetId}\n context={injectSharedStateIntoContext(context, widget.moduleId)}\n data={data}\n onDataChange={onDataChange}\n disabled={disabled}\n />\n )\n })}\n </>\n )\n}\n\n/**\n * Hook to trigger injection widget events imperatively\n */\nexport function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (prefetchedWidgets && prefetchedWidgets.length) {\n setWidgets(prefetchedWidgets)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n )\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets, registryVersion])\n\n const triggerEvent = React.useCallback(\n async (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n data: TData,\n context: TContext,\n meta?: {\n error?: unknown\n fieldId?: string\n fieldValue?: unknown\n originalData?: TData\n target?: unknown\n visible?: boolean\n appEvent?: unknown\n }\n ): Promise<{\n ok: boolean\n message?: string\n fieldErrors?: Record<string, string>\n requestHeaders?: Record<string, string>\n details?: unknown\n data?: TData\n applyToForm?: boolean\n fieldChange?: {\n value?: unknown\n sideEffects?: Record<string, unknown>\n messages?: Array<{ text: string; severity: 'info' | 'warning' | 'error' }>\n }\n }> => {\n const normalizeBeforeSave = (\n result: WidgetBeforeSaveResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const normalizeBeforeDelete = (\n result: WidgetBeforeDeleteResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n // --- Transformer events: pipeline dispatch ---\n // Output of widget N becomes input of widget N+1\n if (TRANSFORMER_EVENTS.has(event)) {\n let pipelineData = data\n let applyToForm = false\n for (const widget of widgets) {\n const handler = widget.module.eventHandlers?.[event]\n if (!handler) continue\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n let handlerResult: unknown\n if (event === 'transformValidation') {\n handlerResult = await (handler as any)(pipelineData, meta?.originalData ?? data, widgetContext)\n } else {\n handlerResult = await (handler as any)(pipelineData, widgetContext)\n }\n if (\n event === 'transformFormData' &&\n handlerResult !== null &&\n typeof handlerResult === 'object' &&\n 'applyToForm' in handlerResult &&\n (handlerResult as { applyToForm: unknown }).applyToForm === true &&\n 'data' in handlerResult\n ) {\n pipelineData = (handlerResult as { data: TData }).data\n applyToForm = true\n } else {\n pipelineData = handlerResult as TData\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n }\n }\n return { ok: true, data: pipelineData, applyToForm }\n }\n\n // --- Action events: sequential dispatch ---\n const mergedRequestHeaders: Record<string, string> = {}\n let hasRequestHeaders = false\n let fieldValue = meta?.fieldValue\n let fieldSideEffects: Record<string, unknown> | undefined\n let fieldMessages: Array<{ text: string; severity: 'info' | 'warning' | 'error' }> | undefined\n\n for (const widget of widgets) {\n const eventHandlers = widget.module.eventHandlers\n // Check operation filter \u2014 skip widget if current operation is filtered out\n const operationFilter = eventHandlers?.filter?.operations\n if (operationFilter) {\n const currentOperation = (context as Record<string, unknown>)?.operation as string | undefined\n if (currentOperation && !operationFilter.includes(currentOperation as 'create' | 'update' | 'delete')) {\n continue\n }\n }\n let handler = eventHandlers?.[event]\n // Delete-to-save fallback chain\n if (!handler && event === 'onBeforeDelete') handler = eventHandlers?.onBeforeSave as typeof handler\n if (!handler && event === 'onDelete') handler = eventHandlers?.onSave as typeof handler\n if (!handler && event === 'onAfterDelete') handler = eventHandlers?.onAfterSave as typeof handler\n if (handler) {\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n const result =\n event === 'onDeleteError'\n ? await (handler as any)(data, widgetContext, meta?.error)\n : event === 'onFieldChange'\n ? await (handler as any)(meta?.fieldId, fieldValue, data, widgetContext)\n : event === 'onBeforeNavigate'\n ? await (handler as any)(meta?.target, widgetContext)\n : event === 'onVisibilityChange'\n ? await (handler as any)(meta?.visible, widgetContext)\n : event === 'onAppEvent'\n ? await (handler as any)(meta?.appEvent, widgetContext)\n : await (handler as any)(data, widgetContext)\n if (event === 'onBeforeSave') {\n const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeDelete') {\n const normalized = normalizeBeforeDelete(result as WidgetBeforeDeleteResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeNavigate') {\n const navResult = result as NavigateGuardResult | undefined\n if (navResult && navResult.ok === false) {\n return { ok: false, message: navResult.message }\n }\n }\n if (event === 'onFieldChange') {\n const changeResult = result as FieldChangeResult | void\n if (changeResult?.value !== undefined) {\n fieldValue = changeResult.value\n }\n if (changeResult?.sideEffects && typeof changeResult.sideEffects === 'object') {\n fieldSideEffects = { ...(fieldSideEffects ?? {}), ...changeResult.sideEffects }\n }\n if (changeResult?.message?.text) {\n fieldMessages = [...(fieldMessages ?? []), changeResult.message]\n }\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n if (event === 'onBeforeSave' || event === 'onBeforeDelete' || event === 'onBeforeNavigate') {\n const message =\n err instanceof Error\n ? err.message || 'Validation blocked'\n : typeof err === 'string'\n ? err\n : undefined\n return { ok: false, message }\n }\n }\n }\n }\n if ((event === 'onBeforeSave' || event === 'onBeforeDelete') && hasRequestHeaders) {\n return { ok: true, requestHeaders: mergedRequestHeaders }\n }\n if (event === 'onFieldChange') {\n return {\n ok: true,\n fieldChange: {\n value: fieldValue,\n sideEffects: fieldSideEffects,\n messages: fieldMessages,\n },\n }\n }\n return { ok: true }\n },\n [widgets]\n )\n\n return { triggerEvent, widgets }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type {\n InjectionSpotId,\n InjectionWidgetModule,\n WidgetInjectionEventHandlers,\n WidgetBeforeDeleteResult,\n WidgetBeforeSaveResult,\n FieldChangeResult,\n NavigateGuardResult,\n} from '@open-mercato/shared/modules/widgets/injection'\nimport {\n getInjectionRegistryVersion,\n loadInjectionWidgetsForSpot,\n subscribeToInjectionRegistryChanges,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { getWidgetSharedState } from './WidgetSharedState'\n\nexport type InjectionSpotProps<TContext = unknown, TData = unknown> = {\n spotId: InjectionSpotId\n context: TContext\n data?: TData\n onDataChange?: (data: TData) => void\n disabled?: boolean\n onEvent?: (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n widgetId: string,\n ) => void\n widgetsOverride?: LoadedWidget[]\n}\n\n/**\n * Transformer events use pipeline dispatch: output of widget N becomes input of widget N+1.\n */\nconst TRANSFORMER_EVENTS = new Set<string>([\n 'transformFormData',\n 'transformDisplayData',\n 'transformValidation',\n])\n\ntype LoadedWidget = {\n widgetId: string\n module: InjectionWidgetModule<any, any>\n moduleId: string\n key: string\n placement?: LoadedInjectionWidget['placement']\n}\n\nexport type LoadedInjectionSpotWidget = LoadedWidget\n\nfunction injectSharedStateIntoContext<TContext>(context: TContext, moduleId: string): TContext {\n const sharedState = getWidgetSharedState(moduleId)\n if (typeof context === 'object' && context !== null && !Array.isArray(context)) {\n return {\n ...(context as Record<string, unknown>),\n sharedState,\n } as TContext\n }\n return {\n value: context,\n sharedState,\n } as TContext\n}\n\nexport function useInjectionWidgets<TContext = unknown>(\n spotId: InjectionSpotId | null | undefined,\n options?: {\n context?: TContext\n triggerOnLoad?: boolean\n onEvent?: (event: 'onLoad', widgetId: string) => void\n }\n) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n const loadedRef = React.useRef(false)\n const contextRef = React.useRef(options?.context)\n const triggerOnLoadRef = React.useRef(options?.triggerOnLoad)\n const onEventRef = React.useRef(options?.onEvent)\n\n React.useEffect(() => {\n contextRef.current = options?.context\n triggerOnLoadRef.current = options?.triggerOnLoad\n onEventRef.current = options?.onEvent\n })\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (!spotId) {\n setWidgets([])\n setLoading(false)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n const widgetList: LoadedWidget[] = loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n setWidgets(widgetList)\n\n // Trigger onLoad for all widgets\n if (!loadedRef.current && triggerOnLoadRef.current) {\n loadedRef.current = true\n for (const widget of widgetList) {\n if (widget.module.eventHandlers?.onLoad) {\n try {\n const widgetContext = injectSharedStateIntoContext(contextRef.current as TContext, widget.moduleId)\n await widget.module.eventHandlers.onLoad(widgetContext)\n onEventRef.current?.('onLoad', widget.widgetId)\n } catch (err) {\n console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n if (mounted) setLoading(false)\n }\n }\n load()\n return () => {\n mounted = false\n }\n // context/triggerOnLoad/onEvent are read from refs so only a real registry-version bump reloads the spot\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [spotId, registryVersion])\n\n return { widgets, loading, error }\n}\n\nexport function InjectionSpot<TContext = unknown, TData = unknown>({\n spotId,\n context,\n data,\n onDataChange,\n disabled,\n onEvent,\n widgetsOverride,\n}: InjectionSpotProps<TContext, TData>) {\n const useSpotId = widgetsOverride ? null : spotId\n const onEventRef = React.useRef(onEvent)\n React.useEffect(() => {\n onEventRef.current = onEvent\n })\n const hasOnEvent = Boolean(onEvent)\n const stableOnEvent = React.useMemo(\n () => (hasOnEvent ? (event: 'onLoad', id: string) => onEventRef.current?.(event, id) : undefined),\n [hasOnEvent],\n )\n const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {\n context,\n triggerOnLoad: !widgetsOverride,\n onEvent: stableOnEvent,\n })\n const effectiveWidgets = widgetsOverride ?? widgets\n const effectiveLoading = widgetsOverride ? false : loading\n const effectiveError = widgetsOverride ? null : error\n\n if (effectiveLoading && effectiveWidgets.length === 0) {\n return null\n }\n\n if (effectiveError) {\n console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)\n return null\n }\n\n if (effectiveWidgets.length === 0) {\n return null\n }\n\n return (\n <>\n {effectiveWidgets.map((widget) => {\n const { Widget } = widget.module\n return (\n <Widget\n key={widget.widgetId}\n context={injectSharedStateIntoContext(context, widget.moduleId)}\n data={data}\n onDataChange={onDataChange}\n disabled={disabled}\n />\n )\n })}\n </>\n )\n}\n\n/**\n * Hook to trigger injection widget events imperatively\n */\nexport function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (prefetchedWidgets && prefetchedWidgets.length) {\n setWidgets(prefetchedWidgets)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n )\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets, registryVersion])\n\n const triggerEvent = React.useCallback(\n async (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n data: TData,\n context: TContext,\n meta?: {\n error?: unknown\n fieldId?: string\n fieldValue?: unknown\n originalData?: TData\n target?: unknown\n visible?: boolean\n appEvent?: unknown\n }\n ): Promise<{\n ok: boolean\n message?: string\n fieldErrors?: Record<string, string>\n requestHeaders?: Record<string, string>\n details?: unknown\n data?: TData\n applyToForm?: boolean\n fieldChange?: {\n value?: unknown\n sideEffects?: Record<string, unknown>\n messages?: Array<{ text: string; severity: 'info' | 'warning' | 'error' }>\n }\n }> => {\n const normalizeBeforeSave = (\n result: WidgetBeforeSaveResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const normalizeBeforeDelete = (\n result: WidgetBeforeDeleteResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n // --- Transformer events: pipeline dispatch ---\n // Output of widget N becomes input of widget N+1\n if (TRANSFORMER_EVENTS.has(event)) {\n let pipelineData = data\n let applyToForm = false\n for (const widget of widgets) {\n const handler = widget.module.eventHandlers?.[event]\n if (!handler) continue\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n let handlerResult: unknown\n if (event === 'transformValidation') {\n handlerResult = await (handler as any)(pipelineData, meta?.originalData ?? data, widgetContext)\n } else {\n handlerResult = await (handler as any)(pipelineData, widgetContext)\n }\n if (\n event === 'transformFormData' &&\n handlerResult !== null &&\n typeof handlerResult === 'object' &&\n 'applyToForm' in handlerResult &&\n (handlerResult as { applyToForm: unknown }).applyToForm === true &&\n 'data' in handlerResult\n ) {\n pipelineData = (handlerResult as { data: TData }).data\n applyToForm = true\n } else {\n pipelineData = handlerResult as TData\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n }\n }\n return { ok: true, data: pipelineData, applyToForm }\n }\n\n // --- Action events: sequential dispatch ---\n const mergedRequestHeaders: Record<string, string> = {}\n let hasRequestHeaders = false\n let fieldValue = meta?.fieldValue\n let fieldSideEffects: Record<string, unknown> | undefined\n let fieldMessages: Array<{ text: string; severity: 'info' | 'warning' | 'error' }> | undefined\n\n for (const widget of widgets) {\n const eventHandlers = widget.module.eventHandlers\n // Check operation filter \u2014 skip widget if current operation is filtered out\n const operationFilter = eventHandlers?.filter?.operations\n if (operationFilter) {\n const currentOperation = (context as Record<string, unknown>)?.operation as string | undefined\n if (currentOperation && !operationFilter.includes(currentOperation as 'create' | 'update' | 'delete')) {\n continue\n }\n }\n let handler = eventHandlers?.[event]\n // Delete-to-save fallback chain\n if (!handler && event === 'onBeforeDelete') handler = eventHandlers?.onBeforeSave as typeof handler\n if (!handler && event === 'onDelete') handler = eventHandlers?.onSave as typeof handler\n if (!handler && event === 'onAfterDelete') handler = eventHandlers?.onAfterSave as typeof handler\n if (handler) {\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n const result =\n event === 'onDeleteError'\n ? await (handler as any)(data, widgetContext, meta?.error)\n : event === 'onFieldChange'\n ? await (handler as any)(meta?.fieldId, fieldValue, data, widgetContext)\n : event === 'onBeforeNavigate'\n ? await (handler as any)(meta?.target, widgetContext)\n : event === 'onVisibilityChange'\n ? await (handler as any)(meta?.visible, widgetContext)\n : event === 'onAppEvent'\n ? await (handler as any)(meta?.appEvent, widgetContext)\n : await (handler as any)(data, widgetContext)\n if (event === 'onBeforeSave') {\n const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeDelete') {\n const normalized = normalizeBeforeDelete(result as WidgetBeforeDeleteResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeNavigate') {\n const navResult = result as NavigateGuardResult | undefined\n if (navResult && navResult.ok === false) {\n return { ok: false, message: navResult.message }\n }\n }\n if (event === 'onFieldChange') {\n const changeResult = result as FieldChangeResult | void\n if (changeResult?.value !== undefined) {\n fieldValue = changeResult.value\n }\n if (changeResult?.sideEffects && typeof changeResult.sideEffects === 'object') {\n fieldSideEffects = { ...(fieldSideEffects ?? {}), ...changeResult.sideEffects }\n }\n if (changeResult?.message?.text) {\n fieldMessages = [...(fieldMessages ?? []), changeResult.message]\n }\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n if (event === 'onBeforeSave' || event === 'onBeforeDelete' || event === 'onBeforeNavigate') {\n const message =\n err instanceof Error\n ? err.message || 'Validation blocked'\n : typeof err === 'string'\n ? err\n : undefined\n return { ok: false, message }\n }\n }\n }\n }\n if ((event === 'onBeforeSave' || event === 'onBeforeDelete') && hasRequestHeaders) {\n return { ok: true, requestHeaders: mergedRequestHeaders }\n }\n if (event === 'onFieldChange') {\n return {\n ok: true,\n fieldChange: {\n value: fieldValue,\n sideEffects: fieldSideEffects,\n messages: fieldMessages,\n },\n }\n }\n return { ok: true }\n },\n [widgets]\n )\n\n return { triggerEvent, widgets }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.6-develop.5523.1.e223ca1915",
3
+ "version": "0.6.6-develop.5531.1.ab1959dfae",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -154,13 +154,13 @@
154
154
  "remark-gfm": "^4.0.1"
155
155
  },
156
156
  "peerDependencies": {
157
- "@open-mercato/shared": "0.6.6-develop.5523.1.e223ca1915",
157
+ "@open-mercato/shared": "0.6.6-develop.5531.1.ab1959dfae",
158
158
  "react": ">=18.0.0",
159
159
  "react-dom": ">=18.0.0",
160
160
  "react-is": ">=18.0.0"
161
161
  },
162
162
  "devDependencies": {
163
- "@open-mercato/shared": "0.6.6-develop.5523.1.e223ca1915",
163
+ "@open-mercato/shared": "0.6.6-develop.5531.1.ab1959dfae",
164
164
  "@testing-library/dom": "^10.4.1",
165
165
  "@testing-library/jest-dom": "^6.9.1",
166
166
  "@testing-library/react": "^16.3.1",
@@ -76,6 +76,15 @@ export function useInjectionWidgets<TContext = unknown>(
76
76
  const [error, setError] = React.useState<string | null>(null)
77
77
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())
78
78
  const loadedRef = React.useRef(false)
79
+ const contextRef = React.useRef(options?.context)
80
+ const triggerOnLoadRef = React.useRef(options?.triggerOnLoad)
81
+ const onEventRef = React.useRef(options?.onEvent)
82
+
83
+ React.useEffect(() => {
84
+ contextRef.current = options?.context
85
+ triggerOnLoadRef.current = options?.triggerOnLoad
86
+ onEventRef.current = options?.onEvent
87
+ })
79
88
 
80
89
  React.useEffect(() => {
81
90
  return subscribeToInjectionRegistryChanges(() => {
@@ -105,16 +114,16 @@ export function useInjectionWidgets<TContext = unknown>(
105
114
  placement: w.placement,
106
115
  }))
107
116
  setWidgets(widgetList)
108
-
117
+
109
118
  // Trigger onLoad for all widgets
110
- if (!loadedRef.current && options?.triggerOnLoad) {
119
+ if (!loadedRef.current && triggerOnLoadRef.current) {
111
120
  loadedRef.current = true
112
121
  for (const widget of widgetList) {
113
122
  if (widget.module.eventHandlers?.onLoad) {
114
123
  try {
115
- const widgetContext = injectSharedStateIntoContext(options.context as TContext, widget.moduleId)
124
+ const widgetContext = injectSharedStateIntoContext(contextRef.current as TContext, widget.moduleId)
116
125
  await widget.module.eventHandlers.onLoad(widgetContext)
117
- options.onEvent?.('onLoad', widget.widgetId)
126
+ onEventRef.current?.('onLoad', widget.widgetId)
118
127
  } catch (err) {
119
128
  console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)
120
129
  }
@@ -133,7 +142,9 @@ export function useInjectionWidgets<TContext = unknown>(
133
142
  return () => {
134
143
  mounted = false
135
144
  }
136
- }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent, registryVersion])
145
+ // context/triggerOnLoad/onEvent are read from refs so only a real registry-version bump reloads the spot
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ }, [spotId, registryVersion])
137
148
 
138
149
  return { widgets, loading, error }
139
150
  }
@@ -148,16 +159,25 @@ export function InjectionSpot<TContext = unknown, TData = unknown>({
148
159
  widgetsOverride,
149
160
  }: InjectionSpotProps<TContext, TData>) {
150
161
  const useSpotId = widgetsOverride ? null : spotId
162
+ const onEventRef = React.useRef(onEvent)
163
+ React.useEffect(() => {
164
+ onEventRef.current = onEvent
165
+ })
166
+ const hasOnEvent = Boolean(onEvent)
167
+ const stableOnEvent = React.useMemo(
168
+ () => (hasOnEvent ? (event: 'onLoad', id: string) => onEventRef.current?.(event, id) : undefined),
169
+ [hasOnEvent],
170
+ )
151
171
  const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {
152
172
  context,
153
173
  triggerOnLoad: !widgetsOverride,
154
- onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,
174
+ onEvent: stableOnEvent,
155
175
  })
156
176
  const effectiveWidgets = widgetsOverride ?? widgets
157
177
  const effectiveLoading = widgetsOverride ? false : loading
158
178
  const effectiveError = widgetsOverride ? null : error
159
179
 
160
- if (effectiveLoading) {
180
+ if (effectiveLoading && effectiveWidgets.length === 0) {
161
181
  return null
162
182
  }
163
183
 
@@ -0,0 +1,140 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as React from 'react'
4
+ import { act, render, waitFor } from '@testing-library/react'
5
+
6
+ const mockState: { registryVersion: number; listener: null | (() => void) } = {
7
+ registryVersion: 0,
8
+ listener: null,
9
+ }
10
+ const mockLoadSpy = jest.fn()
11
+
12
+ jest.mock('@open-mercato/shared/modules/widgets/injection-loader', () => ({
13
+ getInjectionRegistryVersion: () => mockState.registryVersion,
14
+ subscribeToInjectionRegistryChanges: (listener: () => void) => {
15
+ mockState.listener = listener
16
+ return () => {
17
+ mockState.listener = null
18
+ }
19
+ },
20
+ loadInjectionWidgetsForSpot: (...args: unknown[]) => mockLoadSpy(...args),
21
+ }))
22
+
23
+ jest.mock('../WidgetSharedState', () => ({
24
+ getWidgetSharedState: () => ({}),
25
+ }))
26
+
27
+ import { InjectionSpot } from '../InjectionSpot'
28
+
29
+ const SPOT_ID = 'data-table:test.products:toolbar'
30
+ const widgetMountSpy = jest.fn()
31
+ const onLoadSpy = jest.fn()
32
+
33
+ function makeWidget(id: string) {
34
+ return {
35
+ metadata: { id },
36
+ moduleId: 'test_module',
37
+ key: id,
38
+ placement: undefined,
39
+ eventHandlers: { onLoad: onLoadSpy },
40
+ Widget: ({ context }: { context: unknown }) => {
41
+ React.useEffect(() => {
42
+ widgetMountSpy()
43
+ }, [])
44
+ const label = (context as { label?: string } | null)?.label ?? ''
45
+ return <div data-testid="injected-widget">{label}</div>
46
+ },
47
+ }
48
+ }
49
+
50
+ function Harness({ tick, onEvent }: { tick: number; onEvent?: (event: 'onLoad', id: string) => void }) {
51
+ // Fresh context identity on every render — mirrors hosts that feed a useMemo/closure
52
+ // context whose identity changes on routine interactions (e.g. row-selection toggles).
53
+ const context = { label: 'ctx', tick }
54
+ return <InjectionSpot spotId={SPOT_ID} context={context} onEvent={onEvent} />
55
+ }
56
+
57
+ describe('InjectionSpot — context identity changes do not remount injected widgets', () => {
58
+ beforeEach(() => {
59
+ mockState.registryVersion = 0
60
+ mockState.listener = null
61
+ mockLoadSpy.mockReset()
62
+ mockLoadSpy.mockResolvedValue([makeWidget('test_module:hello')])
63
+ widgetMountSpy.mockClear()
64
+ onLoadSpy.mockClear()
65
+ })
66
+
67
+ it('renders the injected widget and loads the spot exactly once', async () => {
68
+ const { findByTestId } = render(<Harness tick={0} />)
69
+ await findByTestId('injected-widget')
70
+ expect(mockLoadSpy).toHaveBeenCalledTimes(1)
71
+ expect(widgetMountSpy).toHaveBeenCalledTimes(1)
72
+ expect(onLoadSpy).toHaveBeenCalledTimes(1)
73
+ })
74
+
75
+ it('does NOT reload or remount when only the context identity changes', async () => {
76
+ const { rerender, findByTestId } = render(<Harness tick={0} />)
77
+ await findByTestId('injected-widget')
78
+ expect(mockLoadSpy).toHaveBeenCalledTimes(1)
79
+ expect(widgetMountSpy).toHaveBeenCalledTimes(1)
80
+
81
+ for (let next = 1; next <= 3; next += 1) {
82
+ rerender(<Harness tick={next} />)
83
+ // flush effects for this render
84
+ await act(async () => {})
85
+ }
86
+
87
+ expect(mockLoadSpy).toHaveBeenCalledTimes(1)
88
+ expect(widgetMountSpy).toHaveBeenCalledTimes(1)
89
+ expect(onLoadSpy).toHaveBeenCalledTimes(1)
90
+ })
91
+
92
+ it('does NOT reload when a fresh onEvent closure is passed on every render', async () => {
93
+ const { rerender, findByTestId } = render(<Harness tick={0} onEvent={() => {}} />)
94
+ await findByTestId('injected-widget')
95
+ expect(mockLoadSpy).toHaveBeenCalledTimes(1)
96
+
97
+ rerender(<Harness tick={1} onEvent={() => {}} />)
98
+ await act(async () => {})
99
+
100
+ expect(mockLoadSpy).toHaveBeenCalledTimes(1)
101
+ expect(widgetMountSpy).toHaveBeenCalledTimes(1)
102
+ })
103
+
104
+ it('DOES reload when the injection registry version actually bumps', async () => {
105
+ const { findByTestId } = render(<Harness tick={0} />)
106
+ await findByTestId('injected-widget')
107
+ expect(mockLoadSpy).toHaveBeenCalledTimes(1)
108
+
109
+ await act(async () => {
110
+ mockState.registryVersion += 1
111
+ mockState.listener?.()
112
+ })
113
+
114
+ await waitFor(() => expect(mockLoadSpy).toHaveBeenCalledTimes(2))
115
+ })
116
+
117
+ it('keeps the previously-rendered widget mounted during a registry reload', async () => {
118
+ const { findByTestId } = render(<Harness tick={0} />)
119
+ await findByTestId('injected-widget')
120
+
121
+ let resolveReload: (value: ReturnType<typeof makeWidget>[]) => void = () => {}
122
+ mockLoadSpy.mockImplementationOnce(
123
+ () => new Promise((resolve) => { resolveReload = resolve }),
124
+ )
125
+
126
+ await act(async () => {
127
+ mockState.registryVersion += 1
128
+ mockState.listener?.()
129
+ })
130
+
131
+ // Reload is in-flight (loading === true) but the existing widget stays mounted.
132
+ expect(await findByTestId('injected-widget')).toBeTruthy()
133
+
134
+ await act(async () => {
135
+ resolveReload([makeWidget('test_module:hello')])
136
+ })
137
+
138
+ await findByTestId('injected-widget')
139
+ })
140
+ })