@open-mercato/ui 0.6.7-develop.6584.1.bc9d39be70 → 0.6.7-develop.6586.1.015fe64dfb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/injection/InjectionSpot.js +93 -27
- package/dist/backend/injection/InjectionSpot.js.map +2 -2
- package/dist/backend/injection/useInjectionDataWidgets.js +21 -2
- package/dist/backend/injection/useInjectionDataWidgets.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/injection/InjectionSpot.tsx +120 -27
- package/src/backend/injection/__tests__/InjectionSpot.features.test.tsx +240 -0
- package/src/backend/injection/useInjectionDataWidgets.ts +21 -2
|
@@ -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
|
|
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 =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
placement: w.placement
|
|
69
|
-
}));
|
|
103
|
+
const widgetList = filterWidgetsByGrantedFeatures(
|
|
104
|
+
loaded.map(toLoadedWidget),
|
|
105
|
+
grantedFeatureList,
|
|
106
|
+
hasBackendChromePayload
|
|
107
|
+
);
|
|
70
108
|
setWidgets(widgetList);
|
|
71
|
-
if (
|
|
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
|
|
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: !
|
|
161
|
+
triggerOnLoad: !hasWidgetsOverride,
|
|
122
162
|
onEvent: stableOnEvent
|
|
123
163
|
});
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
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 (
|
|
161
|
-
setWidgets
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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": ";
|
|
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(
|
|
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;
|
|
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.
|
|
3
|
+
"version": "0.6.7-develop.6586.1.015fe64dfb",
|
|
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.
|
|
158
|
+
"@open-mercato/shared": "0.6.7-develop.6586.1.015fe64dfb",
|
|
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.
|
|
164
|
+
"@open-mercato/shared": "0.6.7-develop.6586.1.015fe64dfb",
|
|
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
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
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
|
|
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: !
|
|
238
|
+
triggerOnLoad: !hasWidgetsOverride,
|
|
177
239
|
onEvent: stableOnEvent,
|
|
178
240
|
})
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
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 (
|
|
229
|
-
setWidgets
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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(
|
|
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
|
}
|