You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by en...@apache.org on 2026/07/07 13:12:50 UTC
(superset) branch master updated: fix(ag-grid): keep basic conditional formatting aligned after sort (#41390)
This is an automated email from the ASF dual-hosted git repository.
EnxDev 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 ffd09829423 fix(ag-grid): keep basic conditional formatting aligned after sort (#41390)
ffd09829423 is described below
commit ffd098294234583562675d40ffed3260509875e6
Author: amaannawab923 <am...@gmail.com>
AuthorDate: Tue Jul 7 18:42:44 2026 +0530
fix(ag-grid): keep basic conditional formatting aligned after sort (#41390)
Co-authored-by: Enzo Martellucci <52...@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <en...@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
.../plugin-chart-ag-grid-table/src/consts.ts | 8 +++
.../src/renderers/NumericCellRenderer.tsx | 15 ++--
.../src/transformProps.ts | 19 +++++
.../src/utils/getCellStyle.ts | 8 ++-
.../src/utils/getRowBasicColorFormatter.ts | 50 +++++++++++++
.../test/utils/getRowBasicColorFormatter.test.ts | 81 ++++++++++++++++++++++
6 files changed, 172 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..a8fc68223f8 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,11 @@ export const FILTER_CONDITION_BODY_INDEX = {
} as const;
export const ROW_NUMBER_COL_ID = '__row_number__';
+
+// Symbol 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). A Symbol guarantees the key can never collide with a real
+// dataset column (even one literally named "__basicColorFormatters__") and is
+// automatically excluded from Object.keys, JSON serialization and spreads, so
+// it never leaks into exports or cross-filters.
+export const BASIC_COLOR_FORMATTERS_ROW_KEY = Symbol('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..11d40569065 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 656f139e85a..d6bcffb7cc6 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,24 @@ 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 key is a Symbol so it
+ // can never collide with a real dataset column and 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..ad7e7cea411
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getRowBasicColorFormatter.ts
@@ -0,0 +1,50 @@
+/**
+ * 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';
+
+export 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 | symbol, 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..63933c676a7
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/utils/getRowBasicColorFormatter.test.ts
@@ -0,0 +1,81 @@
+/**
+ * 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, {
+ RowFormatters,
+} from '../../src/utils/getRowBasicColorFormatter';
+import { BASIC_COLOR_FORMATTERS_ROW_KEY } from '../../src/consts';
+
+const red: RowFormatters = {
+ sales: { backgroundColor: 'red', mainArrow: '↓', arrowColor: 'red' },
+};
+const green: RowFormatters = {
+ sales: { backgroundColor: 'green', mainArrow: '↑', arrowColor: 'green' },
+};
+
+// Positional array in the original (unsorted) query order: row 0 -> green, row 1 -> red.
+const positional: RowFormatters[] = [green, red];
+
+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 | symbol, unknown> = { sales: 5 };
+ rowData[BASIC_COLOR_FORMATTERS_ROW_KEY] = red;
+ 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 a Symbol so it does not leak into the row', () => {
+ const rowData: Record<string | symbol, unknown> = { sales: 5 };
+ rowData[BASIC_COLOR_FORMATTERS_ROW_KEY] = green;
+ // Symbol keys are excluded from Object.keys, JSON serialization and spreads.
+ expect(Object.keys(rowData)).toEqual(['sales']);
+ expect(JSON.stringify(rowData)).toBe('{"sales":5}');
+});
+
+test('does not collide with a real column named "__basicColorFormatters__"', () => {
+ // The old string key would have overwritten this user column; the Symbol key
+ // leaves it untouched while still resolving the attached formatter (#105973).
+ const rowData: Record<string | symbol, unknown> = {
+ sales: 5,
+ __basicColorFormatters__: 'user data',
+ };
+ rowData[BASIC_COLOR_FORMATTERS_ROW_KEY] = red;
+ const node = { data: rowData };
+
+ expect(node.data.__basicColorFormatters__).toBe('user data');
+ expect(getRowBasicColorFormatter(node, 0, positional)).toBe(red);
+});