@capillarytech/cap-ui-utils 3.0.17 → 3.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capillarytech/cap-ui-utils",
3
- "version": "3.0.17",
3
+ "version": "3.0.18",
4
4
  "description": "Utility functions shared accross all the modules",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -2,28 +2,36 @@ import { useEffect, useRef } from 'react';
2
2
  import MFEEventBus from './mfeEventBus';
3
3
 
4
4
  /**
5
- * Emit the module header for a given path. For route-based settings, the
6
- * settings route shows the settings header (with a back button); everything
7
- * else shows the main header. This runs on every navigation so both the MFE
8
- * back button and the browser back button land on the correct header.
9
- * No-op when settings is not route-based (header is registered once on mount).
5
+ * Register the module header for a given path. Resolution order:
6
+ * 1. settingsRoute prefix -> settings header (back button, no settings icon)
7
+ * 2. first matching pathConfigs entry -> its header
8
+ * 3. fallback `header`
9
+ * Only used when the header is path-aware (settingsRoute or pathConfigs set);
10
+ * a purely static header is registered once on mount instead.
10
11
  */
11
- const emitHeaderForPath = (cfg, pathname) => {
12
- const { header, settingsRoute, settingsHeader } = cfg;
13
- if (!settingsRoute) return;
14
- if (pathname.startsWith(settingsRoute)) {
12
+ const registerHeaderForPath = (cfg, pathname) => {
13
+ const { pathConfigs, header, settingsRoute, settingsHeader } = cfg;
14
+ if (settingsRoute && pathname.startsWith(settingsRoute)) {
15
15
  MFEEventBus.emit('module:header-register', {
16
16
  ...header,
17
17
  ...settingsHeader,
18
18
  showSettings: false,
19
19
  showBackButton: true,
20
20
  });
21
- } else {
22
- MFEEventBus.emit('module:header-register', header);
21
+ return;
23
22
  }
23
+ if (pathConfigs && pathConfigs.length) {
24
+ const hit = pathConfigs.find(({ match }) =>
25
+ typeof match === 'function' ? match(pathname) : pathname.startsWith(match));
26
+ if (hit) {
27
+ MFEEventBus.emit('module:header-register', hit.header);
28
+ return;
29
+ }
30
+ }
31
+ if (header) MFEEventBus.emit('module:header-register', header);
24
32
  };
25
33
 
26
- const emitRouteChange = pathname =>
34
+ const emitRouteChangeFor = pathname =>
27
35
  MFEEventBus.emit('module:route-change', { pathname });
28
36
 
29
37
  /**
@@ -31,12 +39,13 @@ const emitRouteChange = pathname =>
31
39
  *
32
40
  * Common hook wrapping the MFEEventBus module-header wiring duplicated across
33
41
  * every remote app. Registers the module header, keeps it in sync on
34
- * navigation (including browser back), and handles the settings button — for
35
- * both modal-style settings (callback) and route-based settings (own URL).
42
+ * navigation (including browser back), and handles the settings/back buttons.
43
+ * Scales from a single static header (launchpad-style) up to a per-path header
44
+ * map with a header dropdown, extra buttons and arbitrary custom events (neo).
36
45
  *
37
- * Router-version agnostic: it never calls useLocation/useNavigate itself
38
- * (those throw without the matching Router context and break the rules of
39
- * hooks when called conditionally). The caller passes the router primitives:
46
+ * Router-version agnostic: it never calls useLocation/useNavigate itself (those
47
+ * throw without the matching Router context and break the rules of hooks when
48
+ * called conditionally). The caller passes the router primitives:
40
49
  * - React Router v5: pass `history` (history.location/.listen/.push/.goBack)
41
50
  * - React Router v6: pass `location` (from useLocation) and `navigate`
42
51
  * (from useNavigate)
@@ -50,11 +59,20 @@ const emitRouteChange = pathname =>
50
59
  * @param {object} [config.history] React Router v5 history object
51
60
  * @param {object} [config.location] React Router v6 location ({ pathname })
52
61
  * @param {function} [config.navigate] React Router v6 navigate function
53
- * @param {object} config.header { name, description, showBorder, showSettings }
62
+ * @param {object} [config.header] Default header, e.g.
63
+ * { name, description, showBorder, showSettings, ...extra }
64
+ * Any extra fields (showSelect, selectOptions,
65
+ * extraButton, settingsRoute, ...) pass straight
66
+ * through to the host.
67
+ * @param {Array} [config.pathConfigs] Path-aware header map, first match wins:
68
+ * [{ match: string|((pathname)=>boolean), header }]
54
69
  * @param {string[]} [config.landingRoutes] Exact paths where the header is shown
55
- * @param {function} [config.onSettingsClick] Modal settings handler (no route change)
56
- * @param {string} [config.settingsRoute] Route-based settings URL (enables push/back)
70
+ * @param {function} [config.onSettingsClick] Settings handler (modal / custom nav)
71
+ * @param {string} [config.settingsRoute] Route-based settings URL (enables push + back)
57
72
  * @param {object} [config.settingsHeader] { name, description } shown on settingsRoute
73
+ * @param {object} [config.listeners] { eventName: (payload)=>void } custom bus listeners
74
+ * @param {boolean} [config.emitRouteChange] Emit module:route-change on nav (default true).
75
+ * Set false to preserve repos that never emitted it.
58
76
  */
59
77
  export default function useMFEModuleHeader(config) {
60
78
  const {
@@ -64,10 +82,15 @@ export default function useMFEModuleHeader(config) {
64
82
  header,
65
83
  landingRoutes,
66
84
  settingsRoute,
85
+ pathConfigs,
86
+ onSettingsClick,
87
+ listeners,
88
+ emitRouteChange = true,
67
89
  } = config;
68
90
 
69
- const isRouteBased = Boolean(settingsRoute);
70
- const showSettings = Boolean(header && header.showSettings);
91
+ const isPathAware = Boolean(settingsRoute) || Boolean(pathConfigs && pathConfigs.length);
92
+ const hasSettingsClick = Boolean(settingsRoute) || Boolean(onSettingsClick);
93
+ const listenerNames = listeners ? Object.keys(listeners).sort().join(',') : '';
71
94
 
72
95
  // Latest config in a ref so callers can pass inline objects/handlers without
73
96
  // memoizing: the long-lived listeners always read fresh values and never
@@ -75,59 +98,73 @@ export default function useMFEModuleHeader(config) {
75
98
  const cfgRef = useRef(config);
76
99
  cfgRef.current = config;
77
100
 
78
- // Header register (non-route-based) + routes, once on mount.
101
+ // Static header register (path-unaware) + routes, once on mount.
79
102
  useEffect(() => {
80
- if (!isRouteBased) {
103
+ if (!isPathAware && header) {
81
104
  MFEEventBus.emit('module:header-register', header);
82
105
  }
83
106
  if (landingRoutes && landingRoutes.length) {
84
107
  MFEEventBus.emit('module:header-routes', {
85
108
  landingRoutes,
86
- ...(isRouteBased ? { settingsRoute } : {}),
109
+ ...(settingsRoute ? { settingsRoute } : {}),
87
110
  });
88
111
  }
89
112
  return () => MFEEventBus.emit('module:header-clear', {});
90
113
  }, []);
91
114
 
92
- // v5 navigation: single history.listen drives both header + route-change.
115
+ // v5 navigation: single history.listen drives path-aware header + route-change.
93
116
  useEffect(() => {
94
117
  if (!history) return undefined;
95
118
  const { pathname } = history.location;
96
- emitHeaderForPath(cfgRef.current, pathname);
97
- emitRouteChange(pathname);
119
+ if (isPathAware) registerHeaderForPath(cfgRef.current, pathname);
120
+ if (emitRouteChange) emitRouteChangeFor(pathname);
98
121
  return history.listen(loc => {
99
- emitHeaderForPath(cfgRef.current, loc.pathname);
100
- emitRouteChange(loc.pathname);
122
+ if (isPathAware) registerHeaderForPath(cfgRef.current, loc.pathname);
123
+ if (emitRouteChange) emitRouteChangeFor(loc.pathname);
101
124
  });
102
125
  }, [history]);
103
126
 
104
127
  // v6 navigation: re-run on pathname change (skipped when v5 history is used).
105
128
  useEffect(() => {
106
129
  if (history || !location) return undefined;
107
- emitHeaderForPath(cfgRef.current, location.pathname);
108
- emitRouteChange(location.pathname);
130
+ if (isPathAware) registerHeaderForPath(cfgRef.current, location.pathname);
131
+ if (emitRouteChange) emitRouteChangeFor(location.pathname);
109
132
  return undefined;
110
133
  }, [history, location && location.pathname]);
111
134
 
112
135
  // Settings button.
113
136
  useEffect(() => {
114
- if (!showSettings) return undefined;
137
+ if (!hasSettingsClick) return undefined;
115
138
  return MFEEventBus.on('module:settings-click', () => {
116
- if (isRouteBased) {
139
+ if (settingsRoute) {
117
140
  if (history) history.push(settingsRoute);
118
141
  else if (navigate) navigate(settingsRoute);
119
142
  } else if (cfgRef.current.onSettingsClick) {
120
143
  cfgRef.current.onSettingsClick();
121
144
  }
122
145
  });
123
- }, [history, navigate, settingsRoute, showSettings]);
146
+ }, [history, navigate, settingsRoute, hasSettingsClick]);
124
147
 
125
- // Back button (route-based settings only).
148
+ // Back button (route-based settings or path-aware headers with a back button).
126
149
  useEffect(() => {
127
- if (!isRouteBased) return undefined;
150
+ if (!isPathAware) return undefined;
128
151
  return MFEEventBus.on('module:back-click', () => {
129
152
  if (history) history.goBack();
130
153
  else if (navigate) navigate(-1);
131
154
  });
132
- }, [history, navigate, settingsRoute]);
155
+ }, [history, navigate, isPathAware]);
156
+
157
+ // Arbitrary custom event listeners. Handlers read from the ref so they can be
158
+ // inline; re-subscribes only when the set of event names changes.
159
+ useEffect(() => {
160
+ const map = cfgRef.current.listeners;
161
+ if (!map) return undefined;
162
+ const unsubs = Object.keys(map).map(name =>
163
+ MFEEventBus.on(name, payload => {
164
+ const fn = cfgRef.current.listeners && cfgRef.current.listeners[name];
165
+ if (fn) fn(payload);
166
+ }));
167
+ return () => unsubs.forEach(unsub => unsub());
168
+ // eslint-disable-next-line react-hooks/exhaustive-deps
169
+ }, [listenerNames]);
133
170
  }