From d3a1659633ea2b370d38b39e6ffa561cf208769b Mon Sep 17 00:00:00 2001
From: "LI,JHE-CHEN" <103923510+RoyLee1224@users.noreply.github.com>
Date: Sat, 15 Nov 2025 00:58:44 -0500
Subject: [PATCH] [v3-1-test] Fix group TI tab memory (#58288)

* fix: Prevent preserving tabs when navigating to Task Groups

* refactor: Centralize keyboard navigation logic to use

* test: update test,  Groups should not preserve tabs
(cherry picked from commit 7b232c39200631448d5041d1577e03c5a3e5b140)

Co-authored-by: LI,JHE-CHEN <103923510+RoyLee1224@users.noreply.github.com>
---
 .../ui/src/hooks/navigation/useNavigation.ts  | 22 +++++++++----------
 .../src/airflow/ui/src/utils/links.test.ts    | 15 +++++++++++--
 .../src/airflow/ui/src/utils/links.ts         |  3 ++-
 3 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts b/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
index f8a63bc29eea2..0c831ecb3094a 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
@@ -21,7 +21,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
 
 import type { GridRunsResponse } from "openapi/requests";
 import type { GridTask } from "src/layouts/Details/Grid/utils";
-import { getTaskInstanceAdditionalPath } from "src/utils/links";
+import { buildTaskInstanceUrl } from "src/utils/links";
 
 import type {
   NavigationDirection,
@@ -72,7 +72,6 @@ const buildPath = (params: {
 }): string => {
   const { dagId, mapIndex = "-1", mode, pathname, run, task } = params;
   const groupPath = task.isGroup ? "group/" : "";
-  const additionalPath = getTaskInstanceAdditionalPath(pathname);
 
   switch (mode) {
     case "run":
@@ -80,16 +79,15 @@ const buildPath = (params: {
     case "task":
       return `/dags/${dagId}/tasks/${groupPath}${task.id}`;
     case "TI":
-      if (task.is_mapped ?? false) {
-        if (mapIndex !== "-1") {
-          // For mapped tasks with specific map index, we need to construct the path manually
-          return `/dags/${dagId}/runs/${run.run_id}/tasks/${groupPath}${task.id}/mapped/${mapIndex}${additionalPath}`;
-        }
-
-        return `/dags/${dagId}/runs/${run.run_id}/tasks/${groupPath}${task.id}/mapped${additionalPath}`;
-      }
-
-      return `/dags/${dagId}/runs/${run.run_id}/tasks/${groupPath}${task.id}${additionalPath}`;
+      return buildTaskInstanceUrl({
+        currentPathname: pathname,
+        dagId,
+        isGroup: task.isGroup,
+        isMapped: task.is_mapped ?? false,
+        mapIndex,
+        runId: run.run_id,
+        taskId: task.id,
+      });
     default:
       return `/dags/${dagId}`;
   }
diff --git a/airflow-core/src/airflow/ui/src/utils/links.test.ts b/airflow-core/src/airflow/ui/src/utils/links.test.ts
index 2f4720897eeed..47dd032ebc519 100644
--- a/airflow-core/src/airflow/ui/src/utils/links.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/links.test.ts
@@ -232,7 +232,18 @@ describe("buildTaskInstanceUrl", () => {
       }),
     ).toBe("/dags/my_dag/runs/run_123/tasks/mapped_task/mapped");
 
-    // Complex: group + mapped + sub-routes
+    // Groups should never preserve tabs (only have "Task Instances" tab)
+    expect(
+      buildTaskInstanceUrl({
+        currentPathname: "/dags/old/runs/old/tasks/old_task/rendered_templates",
+        dagId: "new_dag",
+        isGroup: true,
+        runId: "new_run",
+        taskId: "new_group",
+      }),
+    ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group");
+
+    // Groups should never preserve tabs even for mapped groups
     expect(
       buildTaskInstanceUrl({
         currentPathname: "/dags/old/runs/old/tasks/group/old_group/events",
@@ -243,6 +254,6 @@ describe("buildTaskInstanceUrl", () => {
         runId: "new_run",
         taskId: "new_group",
       }),
-    ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group/mapped/3/events");
+    ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group/mapped/3");
   });
 });
diff --git a/airflow-core/src/airflow/ui/src/utils/links.ts b/airflow-core/src/airflow/ui/src/utils/links.ts
index 80041700860f8..3beafb06afea1 100644
--- a/airflow-core/src/airflow/ui/src/utils/links.ts
+++ b/airflow-core/src/airflow/ui/src/utils/links.ts
@@ -84,7 +84,8 @@ export const buildTaskInstanceUrl = (params: {
 }): string => {
   const { currentPathname, dagId, isGroup = false, isMapped = false, mapIndex, runId, taskId } = params;
   const groupPath = isGroup ? "group/" : "";
-  const additionalPath = getTaskInstanceAdditionalPath(currentPathname);
+  // Task groups only have "Task Instances" tab, so never preserve tabs for groups
+  const additionalPath = isGroup ? "" : getTaskInstanceAdditionalPath(currentPathname);
 
   let basePath = `/dags/${dagId}/runs/${runId}/tasks/${groupPath}${taskId}`;
 
