You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by am...@apache.org on 2026/06/24 12:05:01 UTC

(superset) 01/01: fix(plugin-chart-ag-grid-table): keep basic conditional formatting aligned after sort (#105973)

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

amaannawab923 pushed a commit to branch fix/105973-basic-conditional-formatting-sort
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 348d924c92a503014b6b7f3cf1a8cc2c17d72db9
Author: amaannawab923 <am...@gmail.com>
AuthorDate: Wed Jun 24 17:34:48 2026 +0530

    fix(plugin-chart-ag-grid-table): keep basic conditional formatting aligned after sort (#105973)
    
    The basic (increase/decrease) color formatters were built in the original
    query order and read positionally by the displayed AG Grid rowIndex. Once the
    table was sorted client-side, the displayed index no longer matched the data
    order, so the green/red background and arrow indicators were applied to the
    wrong rows.
    
    Attach each row's formatter to its row data object (non-enumerable, so it never
    leaks into exports/cross-filters) so it travels with the row through sorting,
    and resolve it via getRowBasicColorFormatter in both getCellStyle and
    NumericCellRenderer, falling back to the positional lookup. Add unit tests.
---
 .../plugin-chart-ag-grid-table/src/consts.ts       |  5 ++
 .../src/renderers/NumericCellRenderer.tsx          | 15 ++---
 .../src/transformProps.ts                          | 18 ++++++
 .../src/utils/getCellStyle.ts                      |  8 ++-
 .../src/utils/getRowBasicColorFormatter.ts         | 51 +++++++++++++++++
 .../test/utils/getRowBasicColorFormatter.test.ts   | 65 ++++++++++++++++++++++
 6 files changed, 153 insertions(+), 9 deletions(-)

diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts
index 888686e450a..b53785d847e 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts
@@ -44,3 +44,8 @@ export const FILTER_CONDITION_BODY_INDEX = {
 } as const;
 
 export const ROW_NUMBER_COL_ID = '__row_number__';
+
+// Non-enumerable key used to attach a row's basic (increase/decrease) color
+// formatter to the row data object so it travels with the row through AG Grid
+// client-side sorting (#105973).
+export const BASIC_COLOR_FORMATTERS_ROW_KEY = '__basicColorFormatters__';
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx
index 87529673110..32a8d2cd597 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx
@@ -24,6 +24,7 @@ import {
 import { CustomCellRendererProps } from '@superset-ui/core/components/ThemedAgGridReact';
 import { BasicColorFormatterType, InputColumn, ValueRange } from '../types';
 import { useIsDark } from '../utils/useTableTheme';
+import getRowBasicColorFormatter from '../utils/getRowBasicColorFormatter';
 
 const StyledTotalCell = styled.div`
   ${() => `
@@ -163,13 +164,13 @@ export const NumericCellRenderer = (
   let arrow = '';
   let arrowColor = '';
   if (hasBasicColorFormatters && col?.metricName) {
-    arrow =
-      basicColorFormatters?.[node?.rowIndex as number]?.[col.metricName]
-        ?.mainArrow;
-    arrowColor =
-      basicColorFormatters?.[node?.rowIndex as number]?.[
-        col.metricName
-      ]?.arrowColor?.toLowerCase();
+    const rowFormatter = getRowBasicColorFormatter(
+      node,
+      node?.rowIndex,
+      basicColorFormatters,
+    )?.[col.metricName];
+    arrow = rowFormatter?.mainArrow;
+    arrowColor = rowFormatter?.arrowColor?.toLowerCase();
   }
 
   const alignment =
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
index 1ad19b06b57..4135dc2df8f 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
@@ -46,6 +46,7 @@ import {
 } from '@superset-ui/chart-controls';
 import isEqualColumns from './utils/isEqualColumns';
 import DateWithFormatter from './utils/DateWithFormatter';
+import { BASIC_COLOR_FORMATTERS_ROW_KEY } from './consts';
 import {
   DataColumnMeta,
   TableChartProps,
@@ -703,6 +704,23 @@ const transformProps = (
 
   const basicColorFormatters =
     comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
+
+  // Attach each row's basic (increase/decrease) color formatter to the row data
+  // object so it travels with the row through AG Grid client-side sorting.
+  // basicColorFormatters is built in the original query order and was previously
+  // read positionally by the displayed rowIndex, which applied colors to the
+  // wrong rows once the table was sorted (#105973). The property is
+  // non-enumerable so it never leaks into exports, cross-filters or spreads.
+  if (basicColorFormatters) {
+    passedData.forEach((row, index) => {
+      Object.defineProperty(row, BASIC_COLOR_FORMATTERS_ROW_KEY, {
+        value: basicColorFormatters[index],
+        enumerable: false,
+        configurable: true,
+        writable: true,
+      });
+    });
+  }
   const columnColorFormatters =
     getColorFormatters(conditionalFormatting, passedData, theme) ?? [];
 
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts
index ac5eb26b54a..665909c6f7b 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts
@@ -24,6 +24,7 @@ import {
 } from '@superset-ui/chart-controls';
 import { CellClassParams } from '@superset-ui/core/components/ThemedAgGridReact';
 import { BasicColorFormatterType, InputColumn } from '../types';
+import getRowBasicColorFormatter from './getRowBasicColorFormatter';
 
 type CellStyleParams = CellClassParams & {
   hasColumnColorFormatters: boolean | undefined;
@@ -84,8 +85,11 @@ const getCellStyle = (params: CellStyleParams) => {
     col?.metricName &&
     node?.rowPinned !== 'bottom'
   ) {
-    backgroundColor =
-      basicColorFormatters?.[rowIndex]?.[col.metricName]?.backgroundColor;
+    backgroundColor = getRowBasicColorFormatter(
+      node,
+      rowIndex,
+      basicColorFormatters,
+    )?.[col.metricName]?.backgroundColor;
   }
 
   const textAlign =
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getRowBasicColorFormatter.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getRowBasicColorFormatter.ts
new file mode 100644
index 00000000000..254510be752
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getRowBasicColorFormatter.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { BASIC_COLOR_FORMATTERS_ROW_KEY } from '../consts';
+import { BasicColorFormatterType } from '../types';
+
+type RowFormatters = { [key: string]: BasicColorFormatterType };
+
+/**
+ * Resolves the basic (increase/decrease) color formatters for a given AG Grid
+ * row node.
+ *
+ * The formatter is attached to the row data object itself (see transformProps),
+ * so it follows the row through client-side sorting. Looking it up positionally
+ * by the displayed `rowIndex` was wrong once the user sorted the table, because
+ * the displayed index no longer matched the original data order (#105973).
+ *
+ * Falls back to the positional array for safety when no attached formatter is
+ * present.
+ */
+export default function getRowBasicColorFormatter(
+  node: { data?: Record<string, unknown> } | undefined,
+  rowIndex: number | null | undefined,
+  basicColorFormatters: RowFormatters[] | undefined,
+): RowFormatters | undefined {
+  const attached = node?.data?.[BASIC_COLOR_FORMATTERS_ROW_KEY] as
+    | RowFormatters
+    | undefined;
+  if (attached) {
+    return attached;
+  }
+  if (rowIndex == null) {
+    return undefined;
+  }
+  return basicColorFormatters?.[rowIndex];
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/utils/getRowBasicColorFormatter.test.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/utils/getRowBasicColorFormatter.test.ts
new file mode 100644
index 00000000000..2c5016cbc68
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/utils/getRowBasicColorFormatter.test.ts
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import getRowBasicColorFormatter from '../../src/utils/getRowBasicColorFormatter';
+import { BASIC_COLOR_FORMATTERS_ROW_KEY } from '../../src/consts';
+
+const red = { sales: { backgroundColor: 'red', mainArrow: '↓', arrowColor: 'red' } };
+const green = {
+  sales: { backgroundColor: 'green', mainArrow: '↑', arrowColor: 'green' },
+};
+
+// Positional array in the original (unsorted) query order: row 0 -> green, row 1 -> red.
+const positional = [green, red] as any;
+
+test('uses the formatter attached to the row, not the displayed rowIndex (#105973)', () => {
+  // After sorting, the row whose original formatter is `red` is displayed first
+  // (rowIndex 0). The positional lookup would wrongly return `green`.
+  const rowData: Record<string, unknown> = { sales: 5 };
+  Object.defineProperty(rowData, BASIC_COLOR_FORMATTERS_ROW_KEY, {
+    value: red,
+    enumerable: false,
+  });
+  const node = { data: rowData };
+
+  expect(getRowBasicColorFormatter(node, 0, positional)).toBe(red);
+  expect(
+    getRowBasicColorFormatter(node, 0, positional)?.sales.backgroundColor,
+  ).toBe('red');
+});
+
+test('falls back to positional lookup when no formatter is attached', () => {
+  const node = { data: { sales: 5 } };
+  expect(getRowBasicColorFormatter(node, 1, positional)).toBe(red);
+});
+
+test('returns undefined when nothing matches', () => {
+  expect(getRowBasicColorFormatter(undefined, null, positional)).toBeUndefined();
+  expect(
+    getRowBasicColorFormatter({ data: {} }, null, positional),
+  ).toBeUndefined();
+});
+
+test('attached formatter is non-enumerable so it does not leak into the row', () => {
+  const rowData: Record<string, unknown> = { sales: 5 };
+  Object.defineProperty(rowData, BASIC_COLOR_FORMATTERS_ROW_KEY, {
+    value: green,
+    enumerable: false,
+  });
+  expect(Object.keys(rowData)).toEqual(['sales']);
+});