This is an automated email from the ASF dual-hosted git repository.
michaelsmolina 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 19ec7b48a0f fix: Conditional formatting painting empty cells (#37894)
19ec7b48a0f is described below
commit 19ec7b48a0f94c350306f7379dd8466ae022e80e
Author: Michael S. Molina <[email protected]>
AuthorDate: Thu Feb 12 10:22:00 2026 -0300
fix: Conditional formatting painting empty cells (#37894)
---
.../packages/superset-core/src/utils/index.ts | 1 +
.../superset-core/src/utils/isBlank.test.ts | 59 +++++++++++
.../src/utils/{index.ts => isBlank.ts} | 10 +-
.../src/utils/getColorFormatters.ts | 13 +--
.../test/utils/getColorFormatters.test.ts | 111 +++++++++++++++++++++
.../plugin-chart-table/test/TableChart.test.tsx | 4 +-
6 files changed, 186 insertions(+), 12 deletions(-)
diff --git a/superset-frontend/packages/superset-core/src/utils/index.ts
b/superset-frontend/packages/superset-core/src/utils/index.ts
index 724adae6249..62f2b9bfbdf 100644
--- a/superset-frontend/packages/superset-core/src/utils/index.ts
+++ b/superset-frontend/packages/superset-core/src/utils/index.ts
@@ -17,4 +17,5 @@
* under the License.
*/
+export { default as isBlank } from './isBlank';
export { default as logging } from './logging';
diff --git a/superset-frontend/packages/superset-core/src/utils/isBlank.test.ts
b/superset-frontend/packages/superset-core/src/utils/isBlank.test.ts
new file mode 100644
index 00000000000..52a9b340333
--- /dev/null
+++ b/superset-frontend/packages/superset-core/src/utils/isBlank.test.ts
@@ -0,0 +1,59 @@
+/**
+ * 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 isBlank from './isBlank';
+
+test('returns true for null', () => {
+ expect(isBlank(null)).toBe(true);
+});
+
+test('returns true for undefined', () => {
+ expect(isBlank(undefined)).toBe(true);
+});
+
+test('returns true for empty string', () => {
+ expect(isBlank('')).toBe(true);
+});
+
+test('returns true for whitespace-only strings', () => {
+ expect(isBlank(' ')).toBe(true);
+ expect(isBlank(' ')).toBe(true);
+ expect(isBlank('\t')).toBe(true);
+ expect(isBlank('\n')).toBe(true);
+ expect(isBlank(' \t\n ')).toBe(true);
+});
+
+test('returns false for non-empty strings', () => {
+ expect(isBlank('hello')).toBe(false);
+ expect(isBlank(' hello ')).toBe(false);
+});
+
+test('returns true for NaN', () => {
+ expect(isBlank(NaN)).toBe(true);
+});
+
+test('returns false for numbers', () => {
+ expect(isBlank(0)).toBe(false);
+ expect(isBlank(50)).toBe(false);
+ expect(isBlank(-1)).toBe(false);
+});
+
+test('returns false for booleans', () => {
+ expect(isBlank(true)).toBe(false);
+ expect(isBlank(false)).toBe(false);
+});
diff --git a/superset-frontend/packages/superset-core/src/utils/index.ts
b/superset-frontend/packages/superset-core/src/utils/isBlank.ts
similarity index 72%
copy from superset-frontend/packages/superset-core/src/utils/index.ts
copy to superset-frontend/packages/superset-core/src/utils/isBlank.ts
index 724adae6249..e409dabe529 100644
--- a/superset-frontend/packages/superset-core/src/utils/index.ts
+++ b/superset-frontend/packages/superset-core/src/utils/isBlank.ts
@@ -16,5 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { isEmpty, isNaN, isNil, isString, trim } from 'lodash';
-export { default as logging } from './logging';
+/**
+ * Checks if a value is null, undefined, NaN, or a whitespace-only string.
+ */
+export default function isBlank(value: unknown): boolean {
+ return (
+ isNil(value) || isNaN(value) || (isString(value) && isEmpty(trim(value)))
+ );
+}
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
index 3bdb9a0ad6a..dbfa4b28428 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
@@ -17,6 +17,8 @@
* under the License.
*/
import memoizeOne from 'memoize-one';
+import { isString, isBoolean } from 'lodash';
+import { isBlank } from '@apache-superset/core';
import { addAlpha, DataRecord } from '@superset-ui/core';
import {
ColorFormatters,
@@ -254,6 +256,9 @@ export const getColorFunction = (
}
return (value: number | string | boolean | null) => {
+ if (isBlank(value) && operator !== Comparator.IsNull) {
+ return undefined;
+ }
const compareResult = comparatorFunction(value, columnValues);
if (compareResult === false) return undefined;
const { cutoffValue, extremeValue } = compareResult;
@@ -318,11 +323,3 @@ export const getColorFormatters = memoizeOne(
[],
) ?? [],
);
-
-function isString(value: unknown) {
- return typeof value === 'string';
-}
-
-function isBoolean(value: unknown) {
- return typeof value === 'boolean';
-}
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
index e1be6ae36e3..0c0563049fe 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
@@ -506,6 +506,117 @@ test('getColorFunction IsNotNull', () => {
expect(colorFunction(null)).toBeUndefined();
});
+test('getColorFunction returns undefined for null values on numeric
comparators', () => {
+ const operators = [
+ { operator: Comparator.LessThan, targetValue: 50 },
+ { operator: Comparator.LessOrEqual, targetValue: 50 },
+ { operator: Comparator.GreaterThan, targetValue: 50 },
+ { operator: Comparator.GreaterOrEqual, targetValue: 50 },
+ { operator: Comparator.Equal, targetValue: 50 },
+ { operator: Comparator.NotEqual, targetValue: 50 },
+ ];
+ operators.forEach(({ operator, targetValue }) => {
+ const colorFunction = getColorFunction(
+ {
+ operator,
+ targetValue,
+ colorScheme: '#FF0000',
+ column: 'count',
+ },
+ countValues,
+ );
+ expect(colorFunction(null)).toBeUndefined();
+ expect(colorFunction(undefined as unknown as null)).toBeUndefined();
+ });
+});
+
+test('getColorFunction returns undefined for null values on Between
comparators', () => {
+ const operators = [
+ Comparator.Between,
+ Comparator.BetweenOrEqual,
+ Comparator.BetweenOrLeftEqual,
+ Comparator.BetweenOrRightEqual,
+ ];
+ operators.forEach(operator => {
+ const colorFunction = getColorFunction(
+ {
+ operator,
+ targetValueLeft: -10,
+ targetValueRight: 50,
+ colorScheme: '#FF0000',
+ column: 'count',
+ },
+ countValues,
+ );
+ expect(colorFunction(null)).toBeUndefined();
+ expect(colorFunction(undefined as unknown as null)).toBeUndefined();
+ });
+});
+
+test('getColorFunction returns undefined for null values on None operator', ()
=> {
+ const colorFunction = getColorFunction(
+ {
+ operator: Comparator.None,
+ colorScheme: '#FF0000',
+ column: 'count',
+ },
+ countValues,
+ );
+ expect(colorFunction(null)).toBeUndefined();
+ expect(colorFunction(undefined as unknown as null)).toBeUndefined();
+});
+
+test('getColorFunction returns undefined for null values on string
comparators', () => {
+ const operators = [
+ Comparator.BeginsWith,
+ Comparator.EndsWith,
+ Comparator.Containing,
+ Comparator.NotContaining,
+ ];
+ operators.forEach(operator => {
+ const colorFunction = getColorFunction(
+ {
+ operator,
+ targetValue: 'test',
+ colorScheme: '#FF0000',
+ column: 'name',
+ },
+ strValues,
+ );
+ expect(colorFunction(null)).toBeUndefined();
+ expect(colorFunction(undefined as unknown as null)).toBeUndefined();
+ });
+});
+
+test('getColorFunction returns undefined for empty and whitespace string
values', () => {
+ const colorFunction = getColorFunction(
+ {
+ operator: Comparator.LessThan,
+ targetValue: 50,
+ colorScheme: '#FF0000',
+ column: 'count',
+ },
+ countValues,
+ );
+ expect(colorFunction('' as unknown as number)).toBeUndefined();
+ expect(colorFunction(' ' as unknown as number)).toBeUndefined();
+ expect(colorFunction('\t' as unknown as number)).toBeUndefined();
+});
+
+test('getColorFunction IsNull still matches null values', () => {
+ const colorFunction = getColorFunction(
+ {
+ operator: Comparator.IsNull,
+ targetValue: '',
+ colorScheme: '#FF0000',
+ column: 'isMember',
+ },
+ boolValues,
+ );
+ expect(colorFunction(null)).toEqual('#FF0000FF');
+ expect(colorFunction(true)).toBeUndefined();
+});
+
test('correct column config', () => {
const columnConfig = [
{
diff --git
a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index d907c1e3eb8..3d9da78b875 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -614,9 +614,7 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'',
);
- expect(getComputedStyle(screen.getByText('N/A')).background).toBe(
- 'rgba(172, 225, 196, 1)',
- );
+ expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
test('should display original label in grouped headers', () => {
const props = transformProps(testData.comparison);