You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/25 22:33:36 UTC

(superset) branch master updated: fix(menu): highlight active nav tab in non-English locales (#41183)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 95ecdd37537 fix(menu): highlight active nav tab in non-English locales (#41183)
95ecdd37537 is described below

commit 95ecdd3753745ae4a563ad47e7a09942435d2d49
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Thu Jun 25 15:33:30 2026 -0700

    fix(menu): highlight active nav tab in non-English locales (#41183)
    
    Co-authored-by: Claude Code <no...@anthropic.com>
---
 superset-frontend/src/features/home/Menu.test.tsx | 108 ++++++++++++++++++++++
 superset-frontend/src/features/home/Menu.tsx      |  34 +++++--
 2 files changed, 134 insertions(+), 8 deletions(-)

diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx
index 79b9bcac758..99555d14296 100644
--- a/superset-frontend/src/features/home/Menu.test.tsx
+++ b/superset-frontend/src/features/home/Menu.test.tsx
@@ -796,3 +796,111 @@ test('brand link falls back to brand.path when theme brandLogoUrl is absent', as
   // ensureAppRoot must have been applied: /welcome/ → /superset/welcome/
   expect(brandLink).toHaveAttribute('href', '/superset/welcome/');
 });
+
+// --- Active tab highlighting (regression tests for issue #36403) ---
+//
+// The active top-level tab is highlighted by matching the current route to a
+// menu item. The matching must rely on a stable identifier (the FAB `name`),
+// not the displayed label, otherwise highlighting breaks for any non-English
+// locale where the label is translated.
+
+// Returns the top-level <li> that contains the given visible text, so we can
+// assert whether antd marked it as the selected menu item.
+const getMenuItemByText = (text: string): HTMLElement | null =>
+  screen.getByText(text).closest('li');
+
+// Scoped in a describe so the route-resetting afterEach only applies to these
+// tests and does not leak into the rest of the file.
+describe('active tab highlighting (regression #36403)', () => {
+  afterEach(() => {
+    // Reset the route so a pushed path does not leak into the next test.
+    window.history.pushState({}, '', '/');
+  });
+
+  test('highlights the active top-level tab on a matching route (English)', async () => {
+    useSelectorMock.mockReturnValue({ roles: user.roles });
+    window.history.pushState({}, '', '/dashboard/list/');
+
+    render(<Menu {...mockedProps} />, {
+      useRedux: true,
+      useQueryParams: true,
+      useRouter: true,
+      useTheme: true,
+    });
+
+    await screen.findByText('Dashboards');
+    expect(getMenuItemByText('Dashboards')).toHaveClass(
+      'ant-menu-item-selected',
+    );
+  });
+
+  test('highlights the active top-level tab when the label is localized', async () => {
+    // Russian locale: the FAB `name` stays the stable English identifier while
+    // the displayed `label` is translated. Highlighting must still work.
+    const localizedProps = {
+      ...mockedProps,
+      data: {
+        ...mockedProps.data,
+        menu: mockedProps.data.menu.map(item =>
+          item.name === 'Dashboards' ? { ...item, label: 'Дашборды' } : item,
+        ),
+      },
+    };
+
+    useSelectorMock.mockReturnValue({ roles: user.roles });
+    window.history.pushState({}, '', '/dashboard/list/');
+
+    render(<Menu {...localizedProps} />, {
+      useRedux: true,
+      useQueryParams: true,
+      useRouter: true,
+      useTheme: true,
+    });
+
+    await screen.findByText('Дашборды');
+    expect(getMenuItemByText('Дашборды')).toHaveClass('ant-menu-item-selected');
+  });
+
+  test('highlights the active SQL tab when the label is localized', async () => {
+    // The SQL Lab top-level entry is a FAB category: its stable `name` is
+    // "SQL Lab" while its label ("SQL") is localized.
+    const localizedProps = {
+      ...mockedProps,
+      data: {
+        ...mockedProps.data,
+        menu: [
+          ...mockedProps.data.menu,
+          {
+            name: 'SQL Lab',
+            icon: 'fa-flask',
+            label: 'SQL запросы',
+            childs: [
+              {
+                name: 'SQL Editor',
+                label: 'SQL Lab',
+                url: '/sqllab/',
+                index: 1,
+              },
+            ],
+          },
+        ],
+      },
+    };
+
+    useSelectorMock.mockReturnValue({ roles: user.roles });
+    window.history.pushState({}, '', '/sqllab/');
+
+    render(<Menu {...localizedProps} />, {
+      useRedux: true,
+      useQueryParams: true,
+      useRouter: true,
+      useTheme: true,
+    });
+
+    await screen.findByText('SQL запросы');
+    // SQL Lab renders as a submenu, so antd marks it with the submenu variant.
+    expect(getMenuItemByText('SQL запросы')).toHaveClass(
+      'ant-menu-submenu-selected',
+    );
+  });
+});
diff --git a/superset-frontend/src/features/home/Menu.tsx b/superset-frontend/src/features/home/Menu.tsx
index 6b950f9fff3..a7e8d2dcdf3 100644
--- a/superset-frontend/src/features/home/Menu.tsx
+++ b/superset-frontend/src/features/home/Menu.tsx
@@ -211,6 +211,16 @@ export function Menu({
     SavedQueries = '/savedqueryview',
   }
 
+  // Stable Flask-AppBuilder menu identifiers (`name`), used as menu item keys.
+  // These are locale-independent, unlike the displayed labels, so matching the
+  // active tab against them keeps highlighting working in every language.
+  enum MenuKeys {
+    Dashboards = 'Dashboards',
+    Charts = 'Charts',
+    Datasets = 'Datasets',
+    SqlLab = 'SQL Lab',
+  }
+
   const defaultTabSelection: string[] = [];
   const [activeTabs, setActiveTabs] = useState(defaultTabSelection);
   const location = useLocation();
@@ -218,16 +228,16 @@ export function Menu({
     const path = location.pathname;
     switch (true) {
       case path.startsWith(Paths.Dashboard):
-        setActiveTabs([t('Dashboards')]);
+        setActiveTabs([MenuKeys.Dashboards]);
         break;
       case path.startsWith(Paths.Chart) || path.startsWith(Paths.Explore):
-        setActiveTabs([t('Charts')]);
+        setActiveTabs([MenuKeys.Charts]);
         break;
       case path.startsWith(Paths.Datasets):
-        setActiveTabs([datasetsLabel()]);
+        setActiveTabs([MenuKeys.Datasets]);
         break;
       case path.startsWith(Paths.SqlLab) || path.startsWith(Paths.SavedQueries):
-        setActiveTabs(['SQL']);
+        setActiveTabs([MenuKeys.SqlLab]);
         break;
       default:
         setActiveTabs(defaultTabSelection);
@@ -242,10 +252,14 @@ export function Menu({
     childs,
     url,
     isFrontendRoute,
+    name,
   }: MenuObjectProps): MenuItem => {
+    // Key items by the stable FAB `name` so active-tab matching is independent
+    // of the localized label. Fall back to the label when no name is provided.
+    const key = name ?? label;
     if (url && isFrontendRoute) {
       return {
-        key: label,
+        key,
         label: (
           <NavLink role="button" to={url} activeClassName="is-active">
             {label}
@@ -256,7 +270,7 @@ export function Menu({
 
     if (url) {
       return {
-        key: label,
+        key,
         label: <Typography.Link href={url}>{label}</Typography.Link>,
       };
     }
@@ -268,7 +282,11 @@ export function Menu({
       } else if (typeof child !== 'string') {
         Object.assign(child, { label: t(child.label) });
         childItems.push({
-          key: `${child.label}`,
+          // Key children by the stable FAB `name` as well, so a child whose
+          // localized label coincides with a parent key (e.g. the "SQL Editor"
+          // child labeled "SQL Lab" under the "SQL Lab" category) doesn't
+          // collide with that parent. Fall back to the label when no name.
+          key: child.name ?? `${child.label}`,
           label: child.isFrontendRoute ? (
             <NavLink to={child.url || ''} exact activeClassName="is-active">
               {child.label}
@@ -281,7 +299,7 @@ export function Menu({
     });
 
     return {
-      key: label,
+      key,
       label,
       ...(screens.md && {
         icon: <Icons.DownOutlined iconSize="xs" />,