@open-mercato/ui 0.6.7-develop.6584.1.bc9d39be70 → 0.6.7-develop.6585.1.3c76ca3d22

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.
@@ -6,6 +6,8 @@ import {
6
6
  loadInjectionWidgetsForSpot,
7
7
  subscribeToInjectionRegistryChanges
8
8
  } from "@open-mercato/shared/modules/widgets/injection-loader";
9
+ import { hasAllFeatures } from "@open-mercato/shared/security/features";
10
+ import { useBackendChrome } from "../BackendChromeProvider.js";
9
11
  import { getWidgetSharedState } from "./WidgetSharedState.js";
10
12
  import { createLogger } from "@open-mercato/shared/lib/logger";
11
13
  const logger = createLogger("ui").child({ component: "InjectionSpot" });
@@ -14,6 +16,33 @@ const TRANSFORMER_EVENTS = /* @__PURE__ */ new Set([
14
16
  "transformDisplayData",
15
17
  "transformValidation"
16
18
  ]);
19
+ function toLoadedWidget(widget) {
20
+ return {
21
+ widgetId: widget.metadata.id,
22
+ module: widget,
23
+ moduleId: widget.moduleId,
24
+ key: widget.key,
25
+ placement: widget.placement
26
+ };
27
+ }
28
+ function hasGrantedWidgetFeatures(widget, grantedFeatures, hasBackendChromePayload) {
29
+ if (!hasBackendChromePayload) return true;
30
+ const features = widget.module.metadata.features ?? [];
31
+ return features.length === 0 || hasAllFeatures(grantedFeatures, features);
32
+ }
33
+ function filterWidgetsByGrantedFeatures(widgets, grantedFeatures, hasBackendChromePayload) {
34
+ return widgets.filter((widget) => hasGrantedWidgetFeatures(widget, grantedFeatures, hasBackendChromePayload));
35
+ }
36
+ function areSameLoadedWidgets(current, next) {
37
+ if (current.length !== next.length) return false;
38
+ return current.every((widget, index) => {
39
+ const nextWidget = next[index];
40
+ return nextWidget !== void 0 && widget.widgetId === nextWidget.widgetId && widget.moduleId === nextWidget.moduleId && widget.key === nextWidget.key && widget.module === nextWidget.module;
41
+ });
42
+ }
43
+ function setWidgetsIfChanged(setWidgets, next) {
44
+ setWidgets((current) => areSameLoadedWidgets(current, next) ? current : next);
45
+ }
17
46
  function injectSharedStateIntoContext(context, moduleId) {
18
47
  const sharedState = getWidgetSharedState(moduleId);
19
48
  if (typeof context === "object" && context !== null && !Array.isArray(context)) {
@@ -32,10 +61,16 @@ function useInjectionWidgets(spotId, options) {
32
61
  const [loading, setLoading] = React.useState(true);
33
62
  const [error, setError] = React.useState(null);
34
63
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion());
35
- const loadedRef = React.useRef(false);
64
+ const loadedWidgetIdsRef = React.useRef(/* @__PURE__ */ new Set());
36
65
  const contextRef = React.useRef(options?.context);
37
66
  const triggerOnLoadRef = React.useRef(options?.triggerOnLoad);
38
67
  const onEventRef = React.useRef(options?.onEvent);
68
+ const { payload, isReady: backendChromeReady } = useBackendChrome();
69
+ const grantedFeatureList = React.useMemo(
70
+ () => payload?.grantedFeatures ?? [],
71
+ [payload?.grantedFeatures]
72
+ );
73
+ const hasBackendChromePayload = payload !== null;
39
74
  React.useEffect(() => {
40
75
  contextRef.current = options?.context;
41
76
  triggerOnLoadRef.current = options?.triggerOnLoad;
@@ -53,6 +88,11 @@ function useInjectionWidgets(spotId, options) {
53
88
  setError(null);
54
89
  return;
55
90
  }
91
+ if (!backendChromeReady) {
92
+ setLoading(true);
93
+ setError(null);
94
+ return;
95
+ }
56
96
  let mounted = true;
57
97
  const load = async () => {
58
98
  try {
@@ -60,17 +100,16 @@ function useInjectionWidgets(spotId, options) {
60
100
  setError(null);
61
101
  const loaded = await loadInjectionWidgetsForSpot(spotId);
62
102
  if (!mounted) return;
63
- const widgetList = loaded.map((w) => ({
64
- widgetId: w.metadata.id,
65
- module: w,
66
- moduleId: w.moduleId,
67
- key: w.key,
68
- placement: w.placement
69
- }));
103
+ const widgetList = filterWidgetsByGrantedFeatures(
104
+ loaded.map(toLoadedWidget),
105
+ grantedFeatureList,
106
+ hasBackendChromePayload
107
+ );
70
108
  setWidgets(widgetList);
71
- if (!loadedRef.current && triggerOnLoadRef.current) {
72
- loadedRef.current = true;
109
+ if (triggerOnLoadRef.current) {
73
110
  for (const widget of widgetList) {
111
+ if (loadedWidgetIdsRef.current.has(widget.widgetId)) continue;
112
+ loadedWidgetIdsRef.current.add(widget.widgetId);
74
113
  if (widget.module.eventHandlers?.onLoad) {
75
114
  try {
76
115
  const widgetContext = injectSharedStateIntoContext(contextRef.current, widget.moduleId);
@@ -94,7 +133,7 @@ function useInjectionWidgets(spotId, options) {
94
133
  return () => {
95
134
  mounted = false;
96
135
  };
97
- }, [spotId, registryVersion]);
136
+ }, [spotId, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload]);
98
137
  return { widgets, loading, error };
99
138
  }
100
139
  function InjectionSpot({
@@ -106,7 +145,8 @@ function InjectionSpot({
106
145
  onEvent,
107
146
  widgetsOverride
108
147
  }) {
109
- const useSpotId = widgetsOverride ? null : spotId;
148
+ const hasWidgetsOverride = widgetsOverride !== void 0;
149
+ const useSpotId = hasWidgetsOverride ? null : spotId;
110
150
  const onEventRef = React.useRef(onEvent);
111
151
  React.useEffect(() => {
112
152
  onEventRef.current = onEvent;
@@ -118,12 +158,26 @@ function InjectionSpot({
118
158
  );
119
159
  const { widgets, loading, error } = useInjectionWidgets(useSpotId, {
120
160
  context,
121
- triggerOnLoad: !widgetsOverride,
161
+ triggerOnLoad: !hasWidgetsOverride,
122
162
  onEvent: stableOnEvent
123
163
  });
124
- const effectiveWidgets = widgetsOverride ?? widgets;
125
- const effectiveLoading = widgetsOverride ? false : loading;
126
- const effectiveError = widgetsOverride ? null : error;
164
+ const { payload: overrideFeaturePayload, isReady: overrideBackendChromeReady } = useBackendChrome();
165
+ const overrideGrantedFeatureList = React.useMemo(
166
+ () => overrideFeaturePayload?.grantedFeatures ?? [],
167
+ [overrideFeaturePayload?.grantedFeatures]
168
+ );
169
+ const overrideHasBackendChromePayload = overrideFeaturePayload !== null;
170
+ const filteredWidgetsOverride = React.useMemo(
171
+ () => widgetsOverride === void 0 ? void 0 : filterWidgetsByGrantedFeatures(
172
+ widgetsOverride,
173
+ overrideGrantedFeatureList,
174
+ overrideHasBackendChromePayload
175
+ ),
176
+ [widgetsOverride, overrideGrantedFeatureList, overrideHasBackendChromePayload]
177
+ );
178
+ const effectiveWidgets = hasWidgetsOverride ? overrideBackendChromeReady ? filteredWidgetsOverride ?? [] : [] : widgets;
179
+ const effectiveLoading = hasWidgetsOverride ? !overrideBackendChromeReady : loading;
180
+ const effectiveError = hasWidgetsOverride ? null : error;
127
181
  if (effectiveLoading && effectiveWidgets.length === 0) {
128
182
  return null;
129
183
  }
@@ -151,14 +205,27 @@ function InjectionSpot({
151
205
  function useInjectionSpotEvents(spotId, prefetchedWidgets) {
152
206
  const [widgets, setWidgets] = React.useState([]);
153
207
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion());
208
+ const { payload, isReady: backendChromeReady } = useBackendChrome();
209
+ const grantedFeatureList = React.useMemo(
210
+ () => payload?.grantedFeatures ?? [],
211
+ [payload?.grantedFeatures]
212
+ );
213
+ const hasBackendChromePayload = payload !== null;
154
214
  React.useEffect(() => {
155
215
  return subscribeToInjectionRegistryChanges(() => {
156
216
  setRegistryVersion(getInjectionRegistryVersion());
157
217
  });
158
218
  }, []);
159
219
  React.useEffect(() => {
160
- if (prefetchedWidgets && prefetchedWidgets.length) {
161
- setWidgets(prefetchedWidgets);
220
+ if (!backendChromeReady) {
221
+ setWidgetsIfChanged(setWidgets, []);
222
+ return;
223
+ }
224
+ if (prefetchedWidgets !== void 0) {
225
+ setWidgetsIfChanged(
226
+ setWidgets,
227
+ filterWidgetsByGrantedFeatures(prefetchedWidgets, grantedFeatureList, hasBackendChromePayload)
228
+ );
162
229
  return;
163
230
  }
164
231
  let mounted = true;
@@ -166,14 +233,13 @@ function useInjectionSpotEvents(spotId, prefetchedWidgets) {
166
233
  try {
167
234
  const loaded = await loadInjectionWidgetsForSpot(spotId);
168
235
  if (!mounted) return;
169
- setWidgets(
170
- loaded.map((w) => ({
171
- widgetId: w.metadata.id,
172
- module: w,
173
- moduleId: w.moduleId,
174
- key: w.key,
175
- placement: w.placement
176
- }))
236
+ setWidgetsIfChanged(
237
+ setWidgets,
238
+ filterWidgetsByGrantedFeatures(
239
+ loaded.map(toLoadedWidget),
240
+ grantedFeatureList,
241
+ hasBackendChromePayload
242
+ )
177
243
  );
178
244
  } catch (err) {
179
245
  logger.error("Failed to load widgets for spot", { hook: "useInjectionSpotEvents", spotId, err });
@@ -183,7 +249,7 @@ function useInjectionSpotEvents(spotId, prefetchedWidgets) {
183
249
  return () => {
184
250
  mounted = false;
185
251
  };
186
- }, [spotId, prefetchedWidgets, registryVersion]);
252
+ }, [spotId, prefetchedWidgets, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload]);
187
253
  const triggerEvent = React.useCallback(
188
254
  async (event, data, context, meta) => {
189
255
  const normalizeBeforeSave = (result) => {
@@ -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'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'InjectionSpot' })\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 logger.error('Error in onLoad for widget', { widgetId: widget.widgetId, err })\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n logger.error('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 logger.error('Error loading widgets for spot', { spotId, err: 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 logger.error('Failed to load widgets for spot', { hook: 'useInjectionSpotEvents', 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 logger.error('Error in event handler for widget', { hook: 'useInjectionSpotEvents', event, widgetId: 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 logger.info('Widget prevented event', { hook: 'useInjectionSpotEvents', widgetId: widget.widgetId, 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 logger.info('Widget prevented event', { hook: 'useInjectionSpotEvents', widgetId: widget.widgetId, 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 logger.error('Error in event handler for widget', { hook: 'useInjectionSpotEvents', event, widgetId: 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 { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport { useBackendChrome } from '../BackendChromeProvider'\nimport { getWidgetSharedState } from './WidgetSharedState'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'InjectionSpot' })\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 toLoadedWidget(widget: LoadedInjectionWidget): LoadedWidget {\n return {\n widgetId: widget.metadata.id,\n module: widget,\n moduleId: widget.moduleId,\n key: widget.key,\n placement: widget.placement,\n }\n}\n\nfunction hasGrantedWidgetFeatures(\n widget: LoadedWidget,\n grantedFeatures: string[],\n hasBackendChromePayload: boolean,\n): boolean {\n if (!hasBackendChromePayload) return true\n const features = widget.module.metadata.features ?? []\n return features.length === 0 || hasAllFeatures(grantedFeatures, features)\n}\n\nfunction filterWidgetsByGrantedFeatures(\n widgets: LoadedWidget[],\n grantedFeatures: string[],\n hasBackendChromePayload: boolean,\n): LoadedWidget[] {\n return widgets.filter((widget) => hasGrantedWidgetFeatures(widget, grantedFeatures, hasBackendChromePayload))\n}\n\nfunction areSameLoadedWidgets(current: LoadedWidget[], next: LoadedWidget[]): boolean {\n if (current.length !== next.length) return false\n return current.every((widget, index) => {\n const nextWidget = next[index]\n return (\n nextWidget !== undefined &&\n widget.widgetId === nextWidget.widgetId &&\n widget.moduleId === nextWidget.moduleId &&\n widget.key === nextWidget.key &&\n widget.module === nextWidget.module\n )\n })\n}\n\nfunction setWidgetsIfChanged(\n setWidgets: React.Dispatch<React.SetStateAction<LoadedWidget[]>>,\n next: LoadedWidget[],\n) {\n setWidgets((current) => (areSameLoadedWidgets(current, next) ? current : next))\n}\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 loadedWidgetIdsRef = React.useRef(new Set<string>())\n const contextRef = React.useRef(options?.context)\n const triggerOnLoadRef = React.useRef(options?.triggerOnLoad)\n const onEventRef = React.useRef(options?.onEvent)\n const { payload, isReady: backendChromeReady } = useBackendChrome()\n const grantedFeatureList = React.useMemo(\n () => payload?.grantedFeatures ?? [],\n [payload?.grantedFeatures],\n )\n const hasBackendChromePayload = payload !== null\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 if (!backendChromeReady) {\n setLoading(true)\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 = filterWidgetsByGrantedFeatures(\n loaded.map(toLoadedWidget),\n grantedFeatureList,\n hasBackendChromePayload,\n )\n setWidgets(widgetList)\n\n // Trigger onLoad for all widgets\n if (triggerOnLoadRef.current) {\n for (const widget of widgetList) {\n if (loadedWidgetIdsRef.current.has(widget.widgetId)) continue\n loadedWidgetIdsRef.current.add(widget.widgetId)\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 logger.error('Error in onLoad for widget', { widgetId: widget.widgetId, err })\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n logger.error('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, backendChromeReady, grantedFeatureList, hasBackendChromePayload])\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 hasWidgetsOverride = widgetsOverride !== undefined\n const useSpotId = hasWidgetsOverride ? 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: !hasWidgetsOverride,\n onEvent: stableOnEvent,\n })\n const { payload: overrideFeaturePayload, isReady: overrideBackendChromeReady } = useBackendChrome()\n const overrideGrantedFeatureList = React.useMemo(\n () => overrideFeaturePayload?.grantedFeatures ?? [],\n [overrideFeaturePayload?.grantedFeatures],\n )\n const overrideHasBackendChromePayload = overrideFeaturePayload !== null\n const filteredWidgetsOverride = React.useMemo(\n () =>\n widgetsOverride === undefined\n ? undefined\n : filterWidgetsByGrantedFeatures(\n widgetsOverride,\n overrideGrantedFeatureList,\n overrideHasBackendChromePayload,\n ),\n [widgetsOverride, overrideGrantedFeatureList, overrideHasBackendChromePayload],\n )\n const effectiveWidgets = hasWidgetsOverride\n ? (overrideBackendChromeReady ? filteredWidgetsOverride ?? [] : [])\n : widgets\n const effectiveLoading = hasWidgetsOverride ? !overrideBackendChromeReady : loading\n const effectiveError = hasWidgetsOverride ? null : error\n\n if (effectiveLoading && effectiveWidgets.length === 0) {\n return null\n }\n\n if (effectiveError) {\n logger.error('Error loading widgets for spot', { spotId, err: 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 const { payload, isReady: backendChromeReady } = useBackendChrome()\n const grantedFeatureList = React.useMemo(\n () => payload?.grantedFeatures ?? [],\n [payload?.grantedFeatures],\n )\n const hasBackendChromePayload = payload !== null\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n if (!backendChromeReady) {\n setWidgetsIfChanged(setWidgets, [])\n return\n }\n if (prefetchedWidgets !== undefined) {\n setWidgetsIfChanged(\n setWidgets,\n filterWidgetsByGrantedFeatures(prefetchedWidgets, grantedFeatureList, hasBackendChromePayload),\n )\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgetsIfChanged(\n setWidgets,\n filterWidgetsByGrantedFeatures(\n loaded.map(toLoadedWidget),\n grantedFeatureList,\n hasBackendChromePayload,\n )\n )\n } catch (err) {\n logger.error('Failed to load widgets for spot', { hook: 'useInjectionSpotEvents', spotId, err })\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload])\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 logger.error('Error in event handler for widget', { hook: 'useInjectionSpotEvents', event, widgetId: 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 logger.info('Widget prevented event', { hook: 'useInjectionSpotEvents', widgetId: widget.widgetId, 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 logger.info('Widget prevented event', { hook: 'useInjectionSpotEvents', widgetId: widget.widgetId, 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 logger.error('Error in event handler for widget', { hook: 'useInjectionSpotEvents', event, widgetId: 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
  }
@@ -5,12 +5,20 @@ import {
5
5
  getInjectionRegistryVersion,
6
6
  subscribeToInjectionRegistryChanges
7
7
  } from "@open-mercato/shared/modules/widgets/injection-loader";
8
+ import { hasAllFeatures } from "@open-mercato/shared/security/features";
9
+ import { useBackendChrome } from "../BackendChromeProvider.js";
8
10
  import { createLogger } from "@open-mercato/shared/lib/logger";
9
11
  const logger = createLogger("ui").child({ component: "useInjectionDataWidgets" });
10
12
  function useInjectionDataWidgets(spotId) {
11
13
  const [widgets, setWidgets] = React.useState([]);
12
14
  const [isLoading, setIsLoading] = React.useState(true);
13
15
  const [error, setError] = React.useState(null);
16
+ const { payload, isReady: backendChromeReady } = useBackendChrome();
17
+ const grantedFeatureList = React.useMemo(
18
+ () => payload?.grantedFeatures ?? [],
19
+ [payload?.grantedFeatures]
20
+ );
21
+ const hasBackendChromePayload = payload !== null;
14
22
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion());
15
23
  React.useEffect(() => {
16
24
  return subscribeToInjectionRegistryChanges(() => {
@@ -18,6 +26,11 @@ function useInjectionDataWidgets(spotId) {
18
26
  });
19
27
  }, []);
20
28
  React.useEffect(() => {
29
+ if (!backendChromeReady) {
30
+ setIsLoading(true);
31
+ setError(null);
32
+ return;
33
+ }
21
34
  let mounted = true;
22
35
  const load = async () => {
23
36
  try {
@@ -25,7 +38,13 @@ function useInjectionDataWidgets(spotId) {
25
38
  setError(null);
26
39
  const loaded = await loadInjectionDataWidgetsForSpot(spotId);
27
40
  if (!mounted) return;
28
- setWidgets(loaded);
41
+ setWidgets(
42
+ loaded.filter((widget) => {
43
+ if (!hasBackendChromePayload) return true;
44
+ const features = widget.metadata.features ?? [];
45
+ return features.length === 0 || hasAllFeatures(grantedFeatureList, features);
46
+ })
47
+ );
29
48
  } catch (loadError) {
30
49
  if (!mounted) return;
31
50
  logger.error("Failed to load widgets for spot", { spotId, err: loadError });
@@ -39,7 +58,7 @@ function useInjectionDataWidgets(spotId) {
39
58
  return () => {
40
59
  mounted = false;
41
60
  };
42
- }, [spotId, registryVersion]);
61
+ }, [spotId, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload]);
43
62
  return { widgets, isLoading, error };
44
63
  }
45
64
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/injection/useInjectionDataWidgets.ts"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport {\n loadInjectionDataWidgetsForSpot,\n getInjectionRegistryVersion,\n subscribeToInjectionRegistryChanges,\n type LoadedInjectionDataWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'useInjectionDataWidgets' })\n\nexport function useInjectionDataWidgets(spotId: InjectionSpotId): {\n widgets: LoadedInjectionDataWidget[]\n isLoading: boolean\n error: string | null\n} {\n const [widgets, setWidgets] = React.useState<LoadedInjectionDataWidget[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n // Re-load when the injection registry is (re-)populated. With the async\n // ClientBootstrap, registration can land after this hook first runs; the\n // version bump triggers a reload so injected menus/columns/fields appear\n // without requiring an unrelated re-render.\n const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())\n\n React.useEffect(() => {\n return subscribeToInjectionRegistryChanges(() => {\n setRegistryVersion(getInjectionRegistryVersion())\n })\n }, [])\n\n React.useEffect(() => {\n let mounted = true\n const load = async () => {\n try {\n setIsLoading(true)\n setError(null)\n const loaded = await loadInjectionDataWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(loaded)\n } catch (loadError) {\n if (!mounted) return\n logger.error('Failed to load widgets for spot', { spotId, err: loadError })\n setError(loadError instanceof Error ? loadError.message : String(loadError))\n setWidgets([])\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n void load()\n return () => {\n mounted = false\n }\n }, [spotId, registryVersion])\n\n return { widgets, isLoading, error }\n}\n"],
5
- "mappings": ";AAEA,YAAY,WAAW;AAEvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,0BAA0B,CAAC;AAEzE,SAAS,wBAAwB,QAItC;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAsC,CAAC,CAAC;AAC5E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAK5D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,MAAM,4BAA4B,CAAC;AAEhG,QAAM,UAAU,MAAM;AACpB,WAAO,oCAAoC,MAAM;AAC/C,yBAAmB,4BAA4B,CAAC;AAAA,IAClD,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AACd,UAAM,OAAO,YAAY;AACvB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,IAAI;AACb,cAAM,SAAS,MAAM,gCAAgC,MAAM;AAC3D,YAAI,CAAC,QAAS;AACd,mBAAW,MAAM;AAAA,MACnB,SAAS,WAAW;AAClB,YAAI,CAAC,QAAS;AACd,eAAO,MAAM,mCAAmC,EAAE,QAAQ,KAAK,UAAU,CAAC;AAC1E,iBAAS,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS,CAAC;AAC3E,mBAAW,CAAC,CAAC;AAAA,MACf,UAAE;AACA,YAAI,QAAS,cAAa,KAAK;AAAA,MACjC;AAAA,IACF;AACA,SAAK,KAAK;AACV,WAAO,MAAM;AACX,gBAAU;AAAA,IACZ;AAAA,EACF,GAAG,CAAC,QAAQ,eAAe,CAAC;AAE5B,SAAO,EAAE,SAAS,WAAW,MAAM;AACrC;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport {\n loadInjectionDataWidgetsForSpot,\n getInjectionRegistryVersion,\n subscribeToInjectionRegistryChanges,\n type LoadedInjectionDataWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport { useBackendChrome } from '../BackendChromeProvider'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'useInjectionDataWidgets' })\n\nexport function useInjectionDataWidgets(spotId: InjectionSpotId): {\n widgets: LoadedInjectionDataWidget[]\n isLoading: boolean\n error: string | null\n} {\n const [widgets, setWidgets] = React.useState<LoadedInjectionDataWidget[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const { payload, isReady: backendChromeReady } = useBackendChrome()\n const grantedFeatureList = React.useMemo(\n () => payload?.grantedFeatures ?? [],\n [payload?.grantedFeatures],\n )\n const hasBackendChromePayload = payload !== null\n // Re-load when the injection registry is (re-)populated. With the async\n // ClientBootstrap, registration can land after this hook first runs; the\n // version bump triggers a reload so injected menus/columns/fields appear\n // without requiring an unrelated re-render.\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 (!backendChromeReady) {\n setIsLoading(true)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setIsLoading(true)\n setError(null)\n const loaded = await loadInjectionDataWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.filter((widget) => {\n if (!hasBackendChromePayload) return true\n const features = widget.metadata.features ?? []\n return features.length === 0 || hasAllFeatures(grantedFeatureList, features)\n })\n )\n } catch (loadError) {\n if (!mounted) return\n logger.error('Failed to load widgets for spot', { spotId, err: loadError })\n setError(loadError instanceof Error ? loadError.message : String(loadError))\n setWidgets([])\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n void load()\n return () => {\n mounted = false\n }\n }, [spotId, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload])\n\n return { widgets, isLoading, error }\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AAEvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,0BAA0B,CAAC;AAEzE,SAAS,wBAAwB,QAItC;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAsC,CAAC,CAAC;AAC5E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,EAAE,SAAS,SAAS,mBAAmB,IAAI,iBAAiB;AAClE,QAAM,qBAAqB,MAAM;AAAA,IAC/B,MAAM,SAAS,mBAAmB,CAAC;AAAA,IACnC,CAAC,SAAS,eAAe;AAAA,EAC3B;AACA,QAAM,0BAA0B,YAAY;AAK5C,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,MAAM,4BAA4B,CAAC;AAEhG,QAAM,UAAU,MAAM;AACpB,WAAO,oCAAoC,MAAM;AAC/C,yBAAmB,4BAA4B,CAAC;AAAA,IAClD,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,oBAAoB;AACvB,mBAAa,IAAI;AACjB,eAAS,IAAI;AACb;AAAA,IACF;AACA,QAAI,UAAU;AACd,UAAM,OAAO,YAAY;AACvB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,IAAI;AACb,cAAM,SAAS,MAAM,gCAAgC,MAAM;AAC3D,YAAI,CAAC,QAAS;AACd;AAAA,UACE,OAAO,OAAO,CAAC,WAAW;AACxB,gBAAI,CAAC,wBAAyB,QAAO;AACrC,kBAAM,WAAW,OAAO,SAAS,YAAY,CAAC;AAC9C,mBAAO,SAAS,WAAW,KAAK,eAAe,oBAAoB,QAAQ;AAAA,UAC7E,CAAC;AAAA,QACH;AAAA,MACF,SAAS,WAAW;AAClB,YAAI,CAAC,QAAS;AACd,eAAO,MAAM,mCAAmC,EAAE,QAAQ,KAAK,UAAU,CAAC;AAC1E,iBAAS,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS,CAAC;AAC3E,mBAAW,CAAC,CAAC;AAAA,MACf,UAAE;AACA,YAAI,QAAS,cAAa,KAAK;AAAA,MACjC;AAAA,IACF;AACA,SAAK,KAAK;AACV,WAAO,MAAM;AACX,gBAAU;AAAA,IACZ;AAAA,EACF,GAAG,CAAC,QAAQ,iBAAiB,oBAAoB,oBAAoB,uBAAuB,CAAC;AAE7F,SAAO,EAAE,SAAS,WAAW,MAAM;AACrC;",
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.7-develop.6584.1.bc9d39be70",
3
+ "version": "0.6.7-develop.6585.1.3c76ca3d22",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,13 +155,13 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.7-develop.6584.1.bc9d39be70",
158
+ "@open-mercato/shared": "0.6.7-develop.6585.1.3c76ca3d22",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
- "@open-mercato/shared": "0.6.7-develop.6584.1.bc9d39be70",
164
+ "@open-mercato/shared": "0.6.7-develop.6585.1.3c76ca3d22",
165
165
  "@testing-library/dom": "^10.4.1",
166
166
  "@testing-library/jest-dom": "^6.9.1",
167
167
  "@testing-library/react": "^16.3.1",
@@ -15,6 +15,8 @@ import {
15
15
  subscribeToInjectionRegistryChanges,
16
16
  type LoadedInjectionWidget,
17
17
  } from '@open-mercato/shared/modules/widgets/injection-loader'
18
+ import { hasAllFeatures } from '@open-mercato/shared/security/features'
19
+ import { useBackendChrome } from '../BackendChromeProvider'
18
20
  import { getWidgetSharedState } from './WidgetSharedState'
19
21
  import { createLogger } from '@open-mercato/shared/lib/logger'
20
22
 
@@ -52,6 +54,55 @@ type LoadedWidget = {
52
54
 
53
55
  export type LoadedInjectionSpotWidget = LoadedWidget
54
56
 
57
+ function toLoadedWidget(widget: LoadedInjectionWidget): LoadedWidget {
58
+ return {
59
+ widgetId: widget.metadata.id,
60
+ module: widget,
61
+ moduleId: widget.moduleId,
62
+ key: widget.key,
63
+ placement: widget.placement,
64
+ }
65
+ }
66
+
67
+ function hasGrantedWidgetFeatures(
68
+ widget: LoadedWidget,
69
+ grantedFeatures: string[],
70
+ hasBackendChromePayload: boolean,
71
+ ): boolean {
72
+ if (!hasBackendChromePayload) return true
73
+ const features = widget.module.metadata.features ?? []
74
+ return features.length === 0 || hasAllFeatures(grantedFeatures, features)
75
+ }
76
+
77
+ function filterWidgetsByGrantedFeatures(
78
+ widgets: LoadedWidget[],
79
+ grantedFeatures: string[],
80
+ hasBackendChromePayload: boolean,
81
+ ): LoadedWidget[] {
82
+ return widgets.filter((widget) => hasGrantedWidgetFeatures(widget, grantedFeatures, hasBackendChromePayload))
83
+ }
84
+
85
+ function areSameLoadedWidgets(current: LoadedWidget[], next: LoadedWidget[]): boolean {
86
+ if (current.length !== next.length) return false
87
+ return current.every((widget, index) => {
88
+ const nextWidget = next[index]
89
+ return (
90
+ nextWidget !== undefined &&
91
+ widget.widgetId === nextWidget.widgetId &&
92
+ widget.moduleId === nextWidget.moduleId &&
93
+ widget.key === nextWidget.key &&
94
+ widget.module === nextWidget.module
95
+ )
96
+ })
97
+ }
98
+
99
+ function setWidgetsIfChanged(
100
+ setWidgets: React.Dispatch<React.SetStateAction<LoadedWidget[]>>,
101
+ next: LoadedWidget[],
102
+ ) {
103
+ setWidgets((current) => (areSameLoadedWidgets(current, next) ? current : next))
104
+ }
105
+
55
106
  function injectSharedStateIntoContext<TContext>(context: TContext, moduleId: string): TContext {
56
107
  const sharedState = getWidgetSharedState(moduleId)
57
108
  if (typeof context === 'object' && context !== null && !Array.isArray(context)) {
@@ -78,10 +129,16 @@ export function useInjectionWidgets<TContext = unknown>(
78
129
  const [loading, setLoading] = React.useState(true)
79
130
  const [error, setError] = React.useState<string | null>(null)
80
131
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())
81
- const loadedRef = React.useRef(false)
132
+ const loadedWidgetIdsRef = React.useRef(new Set<string>())
82
133
  const contextRef = React.useRef(options?.context)
83
134
  const triggerOnLoadRef = React.useRef(options?.triggerOnLoad)
84
135
  const onEventRef = React.useRef(options?.onEvent)
136
+ const { payload, isReady: backendChromeReady } = useBackendChrome()
137
+ const grantedFeatureList = React.useMemo(
138
+ () => payload?.grantedFeatures ?? [],
139
+ [payload?.grantedFeatures],
140
+ )
141
+ const hasBackendChromePayload = payload !== null
85
142
 
86
143
  React.useEffect(() => {
87
144
  contextRef.current = options?.context
@@ -102,6 +159,11 @@ export function useInjectionWidgets<TContext = unknown>(
102
159
  setError(null)
103
160
  return
104
161
  }
162
+ if (!backendChromeReady) {
163
+ setLoading(true)
164
+ setError(null)
165
+ return
166
+ }
105
167
  let mounted = true
106
168
  const load = async () => {
107
169
  try {
@@ -109,19 +171,18 @@ export function useInjectionWidgets<TContext = unknown>(
109
171
  setError(null)
110
172
  const loaded = await loadInjectionWidgetsForSpot(spotId)
111
173
  if (!mounted) return
112
- const widgetList: LoadedWidget[] = loaded.map((w) => ({
113
- widgetId: w.metadata.id,
114
- module: w,
115
- moduleId: w.moduleId,
116
- key: w.key,
117
- placement: w.placement,
118
- }))
174
+ const widgetList = filterWidgetsByGrantedFeatures(
175
+ loaded.map(toLoadedWidget),
176
+ grantedFeatureList,
177
+ hasBackendChromePayload,
178
+ )
119
179
  setWidgets(widgetList)
120
180
 
121
181
  // Trigger onLoad for all widgets
122
- if (!loadedRef.current && triggerOnLoadRef.current) {
123
- loadedRef.current = true
182
+ if (triggerOnLoadRef.current) {
124
183
  for (const widget of widgetList) {
184
+ if (loadedWidgetIdsRef.current.has(widget.widgetId)) continue
185
+ loadedWidgetIdsRef.current.add(widget.widgetId)
125
186
  if (widget.module.eventHandlers?.onLoad) {
126
187
  try {
127
188
  const widgetContext = injectSharedStateIntoContext(contextRef.current as TContext, widget.moduleId)
@@ -147,7 +208,7 @@ export function useInjectionWidgets<TContext = unknown>(
147
208
  }
148
209
  // context/triggerOnLoad/onEvent are read from refs so only a real registry-version bump reloads the spot
149
210
  // eslint-disable-next-line react-hooks/exhaustive-deps
150
- }, [spotId, registryVersion])
211
+ }, [spotId, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload])
151
212
 
152
213
  return { widgets, loading, error }
153
214
  }
@@ -161,7 +222,8 @@ export function InjectionSpot<TContext = unknown, TData = unknown>({
161
222
  onEvent,
162
223
  widgetsOverride,
163
224
  }: InjectionSpotProps<TContext, TData>) {
164
- const useSpotId = widgetsOverride ? null : spotId
225
+ const hasWidgetsOverride = widgetsOverride !== undefined
226
+ const useSpotId = hasWidgetsOverride ? null : spotId
165
227
  const onEventRef = React.useRef(onEvent)
166
228
  React.useEffect(() => {
167
229
  onEventRef.current = onEvent
@@ -173,12 +235,31 @@ export function InjectionSpot<TContext = unknown, TData = unknown>({
173
235
  )
174
236
  const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {
175
237
  context,
176
- triggerOnLoad: !widgetsOverride,
238
+ triggerOnLoad: !hasWidgetsOverride,
177
239
  onEvent: stableOnEvent,
178
240
  })
179
- const effectiveWidgets = widgetsOverride ?? widgets
180
- const effectiveLoading = widgetsOverride ? false : loading
181
- const effectiveError = widgetsOverride ? null : error
241
+ const { payload: overrideFeaturePayload, isReady: overrideBackendChromeReady } = useBackendChrome()
242
+ const overrideGrantedFeatureList = React.useMemo(
243
+ () => overrideFeaturePayload?.grantedFeatures ?? [],
244
+ [overrideFeaturePayload?.grantedFeatures],
245
+ )
246
+ const overrideHasBackendChromePayload = overrideFeaturePayload !== null
247
+ const filteredWidgetsOverride = React.useMemo(
248
+ () =>
249
+ widgetsOverride === undefined
250
+ ? undefined
251
+ : filterWidgetsByGrantedFeatures(
252
+ widgetsOverride,
253
+ overrideGrantedFeatureList,
254
+ overrideHasBackendChromePayload,
255
+ ),
256
+ [widgetsOverride, overrideGrantedFeatureList, overrideHasBackendChromePayload],
257
+ )
258
+ const effectiveWidgets = hasWidgetsOverride
259
+ ? (overrideBackendChromeReady ? filteredWidgetsOverride ?? [] : [])
260
+ : widgets
261
+ const effectiveLoading = hasWidgetsOverride ? !overrideBackendChromeReady : loading
262
+ const effectiveError = hasWidgetsOverride ? null : error
182
263
 
183
264
  if (effectiveLoading && effectiveWidgets.length === 0) {
184
265
  return null
@@ -217,6 +298,12 @@ export function InjectionSpot<TContext = unknown, TData = unknown>({
217
298
  export function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {
218
299
  const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])
219
300
  const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())
301
+ const { payload, isReady: backendChromeReady } = useBackendChrome()
302
+ const grantedFeatureList = React.useMemo(
303
+ () => payload?.grantedFeatures ?? [],
304
+ [payload?.grantedFeatures],
305
+ )
306
+ const hasBackendChromePayload = payload !== null
220
307
 
221
308
  React.useEffect(() => {
222
309
  return subscribeToInjectionRegistryChanges(() => {
@@ -225,8 +312,15 @@ export function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spot
225
312
  }, [])
226
313
 
227
314
  React.useEffect(() => {
228
- if (prefetchedWidgets && prefetchedWidgets.length) {
229
- setWidgets(prefetchedWidgets)
315
+ if (!backendChromeReady) {
316
+ setWidgetsIfChanged(setWidgets, [])
317
+ return
318
+ }
319
+ if (prefetchedWidgets !== undefined) {
320
+ setWidgetsIfChanged(
321
+ setWidgets,
322
+ filterWidgetsByGrantedFeatures(prefetchedWidgets, grantedFeatureList, hasBackendChromePayload),
323
+ )
230
324
  return
231
325
  }
232
326
  let mounted = true
@@ -234,14 +328,13 @@ export function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spot
234
328
  try {
235
329
  const loaded = await loadInjectionWidgetsForSpot(spotId)
236
330
  if (!mounted) return
237
- setWidgets(
238
- loaded.map((w) => ({
239
- widgetId: w.metadata.id,
240
- module: w,
241
- moduleId: w.moduleId,
242
- key: w.key,
243
- placement: w.placement,
244
- }))
331
+ setWidgetsIfChanged(
332
+ setWidgets,
333
+ filterWidgetsByGrantedFeatures(
334
+ loaded.map(toLoadedWidget),
335
+ grantedFeatureList,
336
+ hasBackendChromePayload,
337
+ )
245
338
  )
246
339
  } catch (err) {
247
340
  logger.error('Failed to load widgets for spot', { hook: 'useInjectionSpotEvents', spotId, err })
@@ -251,7 +344,7 @@ export function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spot
251
344
  return () => {
252
345
  mounted = false
253
346
  }
254
- }, [spotId, prefetchedWidgets, registryVersion])
347
+ }, [spotId, prefetchedWidgets, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload])
255
348
 
256
349
  const triggerEvent = React.useCallback(
257
350
  async (
@@ -0,0 +1,240 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as React from 'react'
4
+ import { act, render, renderHook, waitFor } from '@testing-library/react'
5
+ import type {
6
+ LoadedInjectionDataWidget,
7
+ LoadedInjectionWidget,
8
+ } from '@open-mercato/shared/modules/widgets/injection-loader'
9
+ import type { LoadedInjectionSpotWidget } from '../InjectionSpot'
10
+
11
+ const loadInjectionWidgetsForSpotMock = jest.fn()
12
+ const loadInjectionDataWidgetsForSpotMock = jest.fn()
13
+ const onLoadVisibleMock = jest.fn()
14
+ const onLoadHiddenMock = jest.fn()
15
+
16
+ let backendChromePayload: { grantedFeatures: string[] } | null = {
17
+ grantedFeatures: [],
18
+ }
19
+ let backendChromeReady = true
20
+
21
+ jest.mock('@open-mercato/shared/modules/widgets/injection-loader', () => ({
22
+ getInjectionRegistryVersion: () => 0,
23
+ subscribeToInjectionRegistryChanges: () => () => {},
24
+ loadInjectionWidgetsForSpot: (...args: unknown[]) => loadInjectionWidgetsForSpotMock(...args),
25
+ loadInjectionDataWidgetsForSpot: (...args: unknown[]) => loadInjectionDataWidgetsForSpotMock(...args),
26
+ }))
27
+
28
+ jest.mock('../../BackendChromeProvider', () => ({
29
+ useBackendChrome: () => ({
30
+ payload: backendChromePayload,
31
+ isLoading: !backendChromeReady,
32
+ isReady: backendChromeReady,
33
+ refresh: jest.fn(),
34
+ }),
35
+ }))
36
+
37
+ jest.mock('../WidgetSharedState', () => ({
38
+ getWidgetSharedState: () => ({}),
39
+ }))
40
+
41
+ import { InjectionSpot, useInjectionSpotEvents } from '../InjectionSpot'
42
+ import { useInjectionDataWidgets } from '../useInjectionDataWidgets'
43
+
44
+ const SPOT_ID = 'data-table:test.widgets:toolbar'
45
+
46
+ function setGrantedFeatures(features: string[]) {
47
+ backendChromePayload = { grantedFeatures: features }
48
+ backendChromeReady = true
49
+ }
50
+
51
+ function setBackendChromeLoading() {
52
+ backendChromePayload = null
53
+ backendChromeReady = false
54
+ }
55
+
56
+ function visualWidget(
57
+ id: string,
58
+ features: string[] | undefined,
59
+ onLoad: () => void,
60
+ ): LoadedInjectionWidget {
61
+ return {
62
+ metadata: { id, features },
63
+ moduleId: 'test_module',
64
+ key: id,
65
+ Widget: () => <div data-testid={id}>{id}</div>,
66
+ eventHandlers: {
67
+ onLoad: async () => {
68
+ onLoad()
69
+ },
70
+ },
71
+ }
72
+ }
73
+
74
+ function lifecycleWidget(
75
+ id: string,
76
+ features: string[] | undefined,
77
+ onBeforeSave: () => void,
78
+ ): LoadedInjectionWidget {
79
+ return {
80
+ metadata: { id, features },
81
+ moduleId: 'test_module',
82
+ key: id,
83
+ Widget: () => <div data-testid={id}>{id}</div>,
84
+ eventHandlers: {
85
+ onBeforeSave: async () => {
86
+ onBeforeSave()
87
+ return false
88
+ },
89
+ },
90
+ }
91
+ }
92
+
93
+ function loadedSpotWidget(widget: LoadedInjectionWidget): LoadedInjectionSpotWidget {
94
+ return {
95
+ widgetId: widget.metadata.id,
96
+ module: widget,
97
+ moduleId: widget.moduleId,
98
+ key: widget.key,
99
+ }
100
+ }
101
+
102
+ function dataWidget(id: string, features?: string[]): LoadedInjectionDataWidget {
103
+ return {
104
+ metadata: { id, features },
105
+ moduleId: 'test_module',
106
+ key: id,
107
+ columns: [],
108
+ }
109
+ }
110
+
111
+ describe('Injection widget feature gating', () => {
112
+ beforeEach(() => {
113
+ setGrantedFeatures([])
114
+ loadInjectionWidgetsForSpotMock.mockReset()
115
+ loadInjectionDataWidgetsForSpotMock.mockReset()
116
+ onLoadVisibleMock.mockReset()
117
+ onLoadHiddenMock.mockReset()
118
+ })
119
+
120
+ it('does not render or load component widgets when metadata features are missing', async () => {
121
+ loadInjectionWidgetsForSpotMock.mockResolvedValueOnce([
122
+ visualWidget('visible-widget', ['widgets.view'], onLoadVisibleMock),
123
+ visualWidget('hidden-widget', ['widgets.manage'], onLoadHiddenMock),
124
+ ])
125
+ setGrantedFeatures(['widgets.view'])
126
+
127
+ const { findByTestId, queryByTestId } = render(
128
+ <InjectionSpot spotId={SPOT_ID} context={{}} />,
129
+ )
130
+
131
+ await findByTestId('visible-widget')
132
+ expect(queryByTestId('hidden-widget')).toBeNull()
133
+ expect(onLoadVisibleMock).toHaveBeenCalledTimes(1)
134
+ expect(onLoadHiddenMock).not.toHaveBeenCalled()
135
+ })
136
+
137
+ it('filters component widget overrides by metadata features', () => {
138
+ const hiddenWidget = visualWidget('hidden-override-widget', ['widgets.manage'], onLoadHiddenMock)
139
+ setGrantedFeatures(['widgets.view'])
140
+
141
+ const { queryByTestId } = render(
142
+ <InjectionSpot
143
+ spotId={SPOT_ID}
144
+ context={{}}
145
+ widgetsOverride={[loadedSpotWidget(hiddenWidget)]}
146
+ />,
147
+ )
148
+
149
+ expect(queryByTestId('hidden-override-widget')).toBeNull()
150
+ })
151
+
152
+ it('filters headless data widgets by metadata features', async () => {
153
+ loadInjectionDataWidgetsForSpotMock.mockResolvedValueOnce([
154
+ dataWidget('always-visible'),
155
+ dataWidget('visible-data-widget', ['widgets.view']),
156
+ dataWidget('hidden-data-widget', ['widgets.manage']),
157
+ ])
158
+ setGrantedFeatures(['widgets.view'])
159
+
160
+ const { result } = renderHook(() => useInjectionDataWidgets(SPOT_ID))
161
+
162
+ await waitFor(() => expect(result.current.isLoading).toBe(false))
163
+ expect(result.current.widgets.map((widget) => widget.metadata.id)).toEqual([
164
+ 'always-visible',
165
+ 'visible-data-widget',
166
+ ])
167
+ })
168
+
169
+ it('does not reload raw lifecycle widgets when the prefetched feature-filtered list is empty', async () => {
170
+ const onBeforeSaveHiddenMock = jest.fn()
171
+ loadInjectionWidgetsForSpotMock.mockResolvedValueOnce([
172
+ lifecycleWidget('hidden-lifecycle-widget', ['widgets.manage'], onBeforeSaveHiddenMock),
173
+ ])
174
+ setGrantedFeatures(['widgets.view'])
175
+
176
+ const { result } = renderHook(() =>
177
+ useInjectionSpotEvents<Record<string, unknown>, Record<string, unknown>>(SPOT_ID, [])
178
+ )
179
+
180
+ await act(async () => {
181
+ await Promise.resolve()
182
+ })
183
+ expect(loadInjectionWidgetsForSpotMock).not.toHaveBeenCalled()
184
+
185
+ const beforeSaveResult = await result.current.triggerEvent('onBeforeSave', {}, {})
186
+
187
+ expect(beforeSaveResult).toEqual({ ok: true })
188
+ expect(onBeforeSaveHiddenMock).not.toHaveBeenCalled()
189
+ })
190
+
191
+ it('does not execute prefetched lifecycle widgets while backend chrome is loading', async () => {
192
+ const onBeforeSaveHiddenMock = jest.fn()
193
+ const hiddenWidget = lifecycleWidget('hidden-loading-widget', ['widgets.manage'], onBeforeSaveHiddenMock)
194
+ setBackendChromeLoading()
195
+
196
+ const { result } = renderHook(() =>
197
+ useInjectionSpotEvents<Record<string, unknown>, Record<string, unknown>>(
198
+ SPOT_ID,
199
+ [loadedSpotWidget(hiddenWidget)],
200
+ )
201
+ )
202
+
203
+ await act(async () => {
204
+ await Promise.resolve()
205
+ })
206
+
207
+ const beforeSaveResult = await result.current.triggerEvent('onBeforeSave', {}, {})
208
+
209
+ expect(beforeSaveResult).toEqual({ ok: true })
210
+ expect(onBeforeSaveHiddenMock).not.toHaveBeenCalled()
211
+ })
212
+
213
+ it('filters lifecycle widgets loaded directly by metadata features', async () => {
214
+ const onBeforeSaveHiddenMock = jest.fn()
215
+ let resolveWidgets: (widgets: LoadedInjectionWidget[]) => void = () => {}
216
+ const loadedWidgets = new Promise<LoadedInjectionWidget[]>((resolve) => {
217
+ resolveWidgets = resolve
218
+ })
219
+ loadInjectionWidgetsForSpotMock.mockReturnValueOnce(loadedWidgets)
220
+ setGrantedFeatures(['widgets.view'])
221
+
222
+ const { result } = renderHook(() =>
223
+ useInjectionSpotEvents<Record<string, unknown>, Record<string, unknown>>(SPOT_ID)
224
+ )
225
+
226
+ await act(async () => {
227
+ resolveWidgets([
228
+ lifecycleWidget('hidden-lifecycle-widget', ['widgets.manage'], onBeforeSaveHiddenMock),
229
+ ])
230
+ await loadedWidgets
231
+ })
232
+ await waitFor(() => expect(loadInjectionWidgetsForSpotMock).toHaveBeenCalledTimes(1))
233
+
234
+ const beforeSaveResult = await result.current.triggerEvent('onBeforeSave', {}, {})
235
+
236
+ expect(beforeSaveResult).toEqual({ ok: true })
237
+ expect(onBeforeSaveHiddenMock).not.toHaveBeenCalled()
238
+ expect(result.current.widgets).toEqual([])
239
+ })
240
+ })
@@ -8,6 +8,8 @@ import {
8
8
  subscribeToInjectionRegistryChanges,
9
9
  type LoadedInjectionDataWidget,
10
10
  } from '@open-mercato/shared/modules/widgets/injection-loader'
11
+ import { hasAllFeatures } from '@open-mercato/shared/security/features'
12
+ import { useBackendChrome } from '../BackendChromeProvider'
11
13
  import { createLogger } from '@open-mercato/shared/lib/logger'
12
14
 
13
15
  const logger = createLogger('ui').child({ component: 'useInjectionDataWidgets' })
@@ -20,6 +22,12 @@ export function useInjectionDataWidgets(spotId: InjectionSpotId): {
20
22
  const [widgets, setWidgets] = React.useState<LoadedInjectionDataWidget[]>([])
21
23
  const [isLoading, setIsLoading] = React.useState(true)
22
24
  const [error, setError] = React.useState<string | null>(null)
25
+ const { payload, isReady: backendChromeReady } = useBackendChrome()
26
+ const grantedFeatureList = React.useMemo(
27
+ () => payload?.grantedFeatures ?? [],
28
+ [payload?.grantedFeatures],
29
+ )
30
+ const hasBackendChromePayload = payload !== null
23
31
  // Re-load when the injection registry is (re-)populated. With the async
24
32
  // ClientBootstrap, registration can land after this hook first runs; the
25
33
  // version bump triggers a reload so injected menus/columns/fields appear
@@ -33,6 +41,11 @@ export function useInjectionDataWidgets(spotId: InjectionSpotId): {
33
41
  }, [])
34
42
 
35
43
  React.useEffect(() => {
44
+ if (!backendChromeReady) {
45
+ setIsLoading(true)
46
+ setError(null)
47
+ return
48
+ }
36
49
  let mounted = true
37
50
  const load = async () => {
38
51
  try {
@@ -40,7 +53,13 @@ export function useInjectionDataWidgets(spotId: InjectionSpotId): {
40
53
  setError(null)
41
54
  const loaded = await loadInjectionDataWidgetsForSpot(spotId)
42
55
  if (!mounted) return
43
- setWidgets(loaded)
56
+ setWidgets(
57
+ loaded.filter((widget) => {
58
+ if (!hasBackendChromePayload) return true
59
+ const features = widget.metadata.features ?? []
60
+ return features.length === 0 || hasAllFeatures(grantedFeatureList, features)
61
+ })
62
+ )
44
63
  } catch (loadError) {
45
64
  if (!mounted) return
46
65
  logger.error('Failed to load widgets for spot', { spotId, err: loadError })
@@ -54,7 +73,7 @@ export function useInjectionDataWidgets(spotId: InjectionSpotId): {
54
73
  return () => {
55
74
  mounted = false
56
75
  }
57
- }, [spotId, registryVersion])
76
+ }, [spotId, registryVersion, backendChromeReady, grantedFeatureList, hasBackendChromePayload])
58
77
 
59
78
  return { widgets, isLoading, error }
60
79
  }