@eeacms/volto-eea-website-theme 4.3.1 → 4.3.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [4.3.2](https://github.com/eea/volto-eea-website-theme/compare/4.3.1...4.3.2) - 15 May 2026
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix(Header): use navroot language to fetch navigation, fix subsite case - refs #303244 [Miu Razvan - [`c25711e`](https://github.com/eea/volto-eea-website-theme/commit/c25711ec3f8492d442c9a130e5e4cb9feb1e80ad)]
12
+
7
13
  ### [4.3.1](https://github.com/eea/volto-eea-website-theme/compare/4.3.0...4.3.1) - 14 May 2026
8
14
 
9
15
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "4.3.1",
3
+ "version": "4.3.2",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -3,26 +3,24 @@
3
3
  * @module components/theme/Header/Header
4
4
  */
5
5
 
6
- import React from 'react';
7
- import { Dropdown, Image } from 'semantic-ui-react';
6
+ import loadable from '@loadable/component';
7
+ import cx from 'classnames';
8
+ import { useEffect, useMemo, useRef } from 'react';
8
9
  import { connect, useDispatch, useSelector } from 'react-redux';
9
-
10
10
  import { withRouter } from 'react-router-dom';
11
+ import { compose } from 'redux';
12
+ import { Dropdown, Image } from 'semantic-ui-react';
13
+
14
+ import { getNavigation } from '@plone/volto/actions/navigation/navigation';
11
15
  import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
12
16
  import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
13
17
  import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
14
- import { getNavigation } from '@plone/volto/actions/navigation/navigation';
15
- import { getNavigationSettings } from '@eeacms/volto-eea-website-theme/actions';
16
- import Header from '@eeacms/volto-eea-design-system/ui/Header/Header';
17
- import EEALogo from '@eeacms/volto-eea-website-theme/components/theme/Logo';
18
- import { usePrevious } from '@eeacms/volto-eea-design-system/helpers';
19
- import eeaFlag from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/eea.png';
20
-
21
18
  import config from '@plone/volto/registry';
22
- import { compose } from 'redux';
23
19
 
24
- import cx from 'classnames';
25
- import loadable from '@loadable/component';
20
+ import eeaFlag from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/eea.png';
21
+ import Header from '@eeacms/volto-eea-design-system/ui/Header/Header';
22
+ import { getNavigationSettings } from '@eeacms/volto-eea-website-theme/actions';
23
+ import EEALogo from '@eeacms/volto-eea-website-theme/components/theme/Logo';
26
24
 
27
25
  const LazyLanguageSwitcher = loadable(() => import('./LanguageSwitcher'));
28
26
  const EMPTY_NAVIGATION_SETTINGS = {};
@@ -32,15 +30,79 @@ function removeTrailingSlash(path) {
32
30
  }
33
31
 
34
32
  /**
35
- * EEA Specific Header component.
33
+ * Merge backend navigation settings into the config-level menu layouts.
36
34
  */
37
- const EEAHeader = ({ pathname, token, items, history, subsite }) => {
38
- const router_pathname = useSelector((state) => {
39
- return removeTrailingSlash(state.router?.location?.pathname) || '';
35
+ function buildEnhancedLayouts(items, navigationSettings) {
36
+ const configLayouts = config.settings?.menuItemsLayouts || {};
37
+ const enhancedLayouts = { ...configLayouts };
38
+
39
+ if (!items) return enhancedLayouts;
40
+
41
+ items.forEach(() => {
42
+ Object.keys(navigationSettings).forEach((routeId) => {
43
+ const route = navigationSettings[routeId];
44
+ const backendSettings = {};
45
+
46
+ if (route.hideChildrenFromNavigation !== undefined) {
47
+ backendSettings.hideChildrenFromNavigation =
48
+ route.hideChildrenFromNavigation;
49
+ }
50
+
51
+ if (route.menuItemChildrenListColumns !== undefined) {
52
+ backendSettings.menuItemChildrenListColumns = Array.isArray(
53
+ route.menuItemChildrenListColumns,
54
+ )
55
+ ? route.menuItemChildrenListColumns
56
+ .map((val) => (typeof val === 'string' ? parseInt(val, 10) : val))
57
+ .filter((val) => !isNaN(val))
58
+ : route.menuItemChildrenListColumns;
59
+ }
60
+
61
+ if (route.menuItemColumns !== undefined) {
62
+ backendSettings.menuItemColumns = route.menuItemColumns;
63
+ }
64
+
65
+ if (Object.keys(backendSettings).length > 0) {
66
+ enhancedLayouts[routeId] = {
67
+ ...enhancedLayouts[routeId],
68
+ ...backendSettings,
69
+ };
70
+ }
71
+ });
40
72
  });
41
73
 
74
+ return enhancedLayouts;
75
+ }
76
+
77
+ /**
78
+ * EEA Specific Header component.
79
+ */
80
+ const EEAHeader = ({ pathname, token, items, history, navroot, subsite }) => {
81
+ // Config / static derived values
82
+ const { eea } = config.settings;
83
+ const headerOpts = eea.headerOpts || {};
84
+ const { logo, logoWhite } = headerOpts;
85
+
42
86
  const isSubsite = subsite?.['@type'] === 'Subsite';
43
87
 
88
+ // Redux state
89
+ const dispatch = useDispatch();
90
+ const width = useSelector((state) => state.screen?.width);
91
+
92
+ const router_pathname = useSelector(
93
+ (state) => removeTrailingSlash(state.router?.location?.pathname) || '',
94
+ );
95
+
96
+ const headerSettings = useSelector(
97
+ (state) => state.reduxAsyncConnect?.headerSettings,
98
+ );
99
+
100
+ const navigationSettings =
101
+ useSelector((state) => state.navigationSettings?.settings) ||
102
+ EMPTY_NAVIGATION_SETTINGS;
103
+
104
+ const updateRequest = useSelector((state) => state.content.update);
105
+
44
106
  const isHomePageInverse = useSelector((state) => {
45
107
  const layout = state.content?.data?.layout;
46
108
  const has_home_layout =
@@ -55,85 +117,69 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
55
117
  );
56
118
  });
57
119
 
58
- const { eea } = config.settings;
59
- const headerOpts = eea.headerOpts || {};
60
- const { logo, logoWhite } = headerOpts;
61
- const width = useSelector((state) => state.screen?.width);
62
- const dispatch = useDispatch();
63
-
64
- const headerSettings = useSelector(
65
- (state) => state.reduxAsyncConnect?.headerSettings,
66
- );
120
+ const prevTokenRef = useRef(undefined);
67
121
 
122
+ // Derived / memoized values
68
123
  const headerSearchBox =
69
124
  headerSettings?.searchBox || eea.headerSearchBox || [];
70
- const previousToken = usePrevious(token);
71
- const navigationSettings =
72
- useSelector((state) => state.navigationSettings?.settings) ||
73
- EMPTY_NAVIGATION_SETTINGS;
74
- const updateRequest = useSelector((state) => state.content.update);
75
-
76
- // Combine navigation settings from backend with config fallback
77
- const configLayouts = config.settings?.menuItemsLayouts || {};
78
- const enhancedLayouts = { ...configLayouts };
79
125
 
80
- // Map navigation settings to menu item URLs
81
- if (items) {
82
- items.forEach((menuItem) => {
83
- // Check if we have navigation settings for any route that might match this menu item
84
- Object.keys(navigationSettings).forEach((routeId) => {
85
- const route = navigationSettings[routeId];
86
- const backendSettings = {};
87
-
88
- if (route.hideChildrenFromNavigation !== undefined) {
89
- backendSettings.hideChildrenFromNavigation =
90
- route.hideChildrenFromNavigation;
91
- }
126
+ const enhancedLayouts = buildEnhancedLayouts(items, navigationSettings);
127
+
128
+ // Prefer navroot.language; fall back to extracting language from pathname
129
+ // (validated against supportedLanguages) when navroot is not yet loaded.
130
+ const navrootLang = useMemo(() => {
131
+ const { supportedLanguages, navigationLanguage } = config.settings;
132
+ if (navroot?.language?.token) return navroot.language.token;
133
+ const supported = supportedLanguages || [];
134
+ const first = pathname.split('/').filter(Boolean)[0];
135
+ if (first === undefined) return navigationLanguage || null;
136
+ return supported.includes(first) ? first : null;
137
+ }, [navroot, pathname]);
138
+
139
+ // Normalize pathname for menu active-item matching when using
140
+ // navigationLanguage. Menu items come from the configured language; rewrite
141
+ // the current language prefix to match. E.g. navLang='en' on /fr/topics ->
142
+ // /en/topics. Uses navroot.language as source of truth instead of parsing
143
+ // the first path segment.
144
+ const normalizedPathname = useMemo(() => {
145
+ const navLang = config.settings.navigationLanguage;
146
+ if (!navLang || !navrootLang || navrootLang === navLang) return pathname;
92
147
 
93
- if (route.menuItemChildrenListColumns !== undefined) {
94
- // Convert strings back to integers for header usage
95
- backendSettings.menuItemChildrenListColumns = Array.isArray(
96
- route.menuItemChildrenListColumns,
97
- )
98
- ? route.menuItemChildrenListColumns
99
- .map((val) =>
100
- typeof val === 'string' ? parseInt(val, 10) : val,
101
- )
102
- .filter((val) => !isNaN(val))
103
- : route.menuItemChildrenListColumns;
104
- }
148
+ const prefix = `/${navrootLang}`;
149
+ if (pathname === prefix) return `/${navLang}`;
150
+ if (pathname.startsWith(`${prefix}/`)) {
151
+ return `/${navLang}${pathname.slice(prefix.length)}`;
152
+ }
153
+ return pathname;
154
+ }, [pathname, navrootLang]);
105
155
 
106
- if (route.menuItemColumns !== undefined) {
107
- // Use menuItemColumns directly as they're already in semantic UI format
108
- backendSettings.menuItemColumns = route.menuItemColumns;
109
- }
156
+ const baseUrl = useMemo(() => {
157
+ const { settings } = config;
158
+ const navLang = settings.navigationLanguage;
159
+ let url = getBaseUrl(pathname);
110
160
 
111
- if (Object.keys(backendSettings).length > 0) {
112
- // Override the config setting with backend data
113
- enhancedLayouts[routeId] = {
114
- ...enhancedLayouts[routeId],
115
- ...backendSettings,
116
- };
117
- }
118
- });
119
- });
120
- }
161
+ if (isSubsite || !navLang || !navrootLang) {
162
+ return url;
163
+ }
121
164
 
122
- // Memoize navigationBaseUrl so it doesn't change on every pathname change
123
- // when navigationLanguage is set to a fixed language
124
- const navigationBaseUrl = React.useMemo(() => {
125
- const { settings } = config;
126
- return settings.navigationLanguage
127
- ? `/${settings.navigationLanguage}`
128
- : getBaseUrl(pathname);
129
- }, [pathname]);
165
+ // When the current navroot's language differs from the configured
166
+ // navigationLanguage, override the base url so navigation is fetched from
167
+ // the configured language root instead of the current navroot.
168
+ if (navLang !== navrootLang) {
169
+ url = `/${settings.navigationLanguage}`;
170
+ } else if (!url && navLang === navrootLang) {
171
+ url = `/${navLang}`;
172
+ }
173
+ return url;
174
+ }, [pathname, navrootLang, isSubsite]);
130
175
 
131
- React.useEffect(() => {
176
+ // Fetch navigation settings on pathname change.
177
+ useEffect(() => {
132
178
  dispatch(getNavigationSettings(pathname));
133
179
  }, [dispatch, pathname]);
134
180
 
135
- // Separate effect for update request to avoid duplicate calls
136
- React.useEffect(() => {
181
+ // Re-fetch navigation settings after a content update for the current page.
182
+ useEffect(() => {
137
183
  if (
138
184
  updateRequest?.loaded &&
139
185
  removeTrailingSlash(updateRequest?.content?.['@id'] || '') ===
@@ -143,46 +189,36 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
143
189
  }
144
190
  }, [updateRequest, dispatch, pathname]);
145
191
 
146
- React.useEffect(() => {
192
+ // Fetch the main navigation tree.
193
+ // Cases that force a fetch:
194
+ // 1. Language mismatch — the current navroot's language differs from the
195
+ // configured navigationLanguage, so the API expander's navigation (if
196
+ // any) is in the wrong language and must be replaced.
197
+ // 2. Token change — auth state affects which nav items are visible, so
198
+ // expander data loaded under the previous token may be stale.
199
+ // 3. No expander available — backend did not pre-supply navigation for
200
+ // this base url, so we fetch it explicitly.
201
+ // Otherwise the expander already supplied correct navigation; no fetch.
202
+ useEffect(() => {
147
203
  const { settings } = config;
204
+ const navLang = settings.navigationLanguage;
205
+ const langMismatch = navLang && navrootLang && navrootLang !== navLang;
206
+ const tokenChanged = prevTokenRef.current !== token;
148
207
 
149
- // When navigationLanguage is configured, always fetch navigation from that language
150
- // We MUST call getNavigation directly because API expanders fetch navigation for the current page
151
- if (settings.navigationLanguage) {
152
- // Always fetch navigation for the configured language
153
- dispatch(getNavigation(navigationBaseUrl, settings.navDepth));
154
- } else {
155
- // When navigationLanguage is not configured, fetch navigation for current page language
156
- // Check if navigation data needs to be fetched based on the API expander availability
157
- if (!hasApiExpander('navigation', navigationBaseUrl)) {
158
- dispatch(getNavigation(navigationBaseUrl, settings.navDepth));
159
- }
160
-
161
- // Additional check for token changes
162
- if (token !== previousToken) {
163
- dispatch(getNavigation(navigationBaseUrl, settings.navDepth));
164
- }
165
- }
166
- }, [navigationBaseUrl, token, dispatch, previousToken]);
167
-
168
- // Normalize pathname for menu matching when using navigationLanguage
169
- // This ensures menu items from the configured language match correctly even when on other language pages
170
- const normalizedPathname = React.useMemo(() => {
171
- const navLang = config.settings.navigationLanguage;
172
- if (!navLang) {
173
- return pathname;
208
+ if (
209
+ langMismatch ||
210
+ tokenChanged ||
211
+ !hasApiExpander('navigation', baseUrl)
212
+ ) {
213
+ dispatch(getNavigation(baseUrl, settings.navDepth));
174
214
  }
215
+ }, [dispatch, baseUrl, navrootLang, token]);
175
216
 
176
- // Replace the language prefix with the configured navigation language for menu matching
177
- // e.g., if navLang='en': /fr/topics -> /en/topics
178
- const pathParts = pathname.split('/').filter(Boolean);
179
- if (pathParts.length > 0 && pathParts[0].length === 2) {
180
- // First segment is a language code, replace it with the navigation language
181
- const rest = pathParts.slice(1).join('/');
182
- return rest ? `/${navLang}/${rest}` : `/${navLang}`;
183
- }
184
- return pathname;
185
- }, [pathname]);
217
+ // Track the previous token value. Runs after the fetch effect so the
218
+ // comparison above sees the value from the prior render.
219
+ useEffect(() => {
220
+ prevTokenRef.current = token;
221
+ }, [token]);
186
222
 
187
223
  return (
188
224
  <Header menuItems={items}>
@@ -327,6 +363,7 @@ export default compose(
327
363
  (state) => ({
328
364
  token: state.userSession.token,
329
365
  items: state.navigation.items,
366
+ navroot: state.content.data?.['@components']?.navroot?.navroot,
330
367
  subsite: state.content.data?.['@components']?.subsite,
331
368
  }),
332
369
  { getNavigation },