From fc9e5d8f945d0263202da28147df0d4f875d521f Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Sat, 15 Nov 2025 18:46:45 -0500 Subject: [PATCH 01/11] feat(dashboard): create types and comparator function logic for boolean conditional formatting --- .../superset-ui-chart-controls/src/types.ts | 8 ++- .../src/utils/getColorFormatters.ts | 62 ++++++++++++++++--- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index eaca8133d445..36f773c8d5ca 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -462,6 +462,10 @@ export enum Comparator { EndsWith = 'ends with', Containing = 'containing', NotContaining = 'not containing', + IsTrue = 'is true', + IsFalse = 'is false', + IsNull = 'is null', + IsNotNull = 'is not null', } export const MultipleValueComparators = [ @@ -473,7 +477,7 @@ export const MultipleValueComparators = [ export type ConditionalFormattingConfig = { operator?: Comparator; - targetValue?: number | string; + targetValue?: number | string | boolean; targetValueLeft?: number; targetValueRight?: number; column?: string; @@ -486,7 +490,7 @@ export type ColorFormatters = { column: string; toAllRow?: boolean; toTextColor?: boolean; - getColorFromValue: (value: number | string) => string | undefined; + getColorFromValue: (value: number | string | boolean) => string | undefined; }[]; export default {}; 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 dfa48efdf405..c11d09f33e9a 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 @@ -32,13 +32,18 @@ const MIN_OPACITY_BOUNDED = 0.05; const MIN_OPACITY_UNBOUNDED = 0; const MAX_OPACITY = 1; export const getOpacity = ( - value: number | string, - cutoffPoint: number | string, - extremeValue: number | string, + value: number | string | boolean, + cutoffPoint: number | string | boolean, + extremeValue: number | string | boolean, minOpacity = MIN_OPACITY_BOUNDED, maxOpacity = MAX_OPACITY, ) => { - if (extremeValue === cutoffPoint || typeof value !== 'number') { + if ( + extremeValue === cutoffPoint || + typeof value !== 'number' || + typeof cutoffPoint === 'boolean' || + typeof extremeValue === 'boolean' + ) { return maxOpacity; } const numCutoffPoint = @@ -70,16 +75,21 @@ export const getColorFunction = ( targetValueRight, colorScheme, }: ConditionalFormattingConfig, - columnValues: number[] | string[], + columnValues: number[] | string[] | boolean[], alpha?: boolean, ) => { let minOpacity = MIN_OPACITY_BOUNDED; const maxOpacity = MAX_OPACITY; let comparatorFunction: ( - value: number | string, - allValues: number[] | string[], - ) => false | { cutoffValue: number | string; extremeValue: number | string }; + value: number | string | boolean, + allValues: number[] | string[] | boolean[], + ) => + | false + | { + cutoffValue: number | string | boolean; + extremeValue: number | string | boolean; + }; if (operator === undefined || colorScheme === undefined) { return () => undefined; } @@ -99,7 +109,10 @@ export const getColorFunction = ( switch (operator) { case Comparator.None: minOpacity = MIN_OPACITY_UNBOUNDED; - comparatorFunction = (value: number | string, allValues: number[]) => { + comparatorFunction = ( + value: number | string | boolean, + allValues: number[], + ) => { if (typeof value !== 'number') { return { cutoffValue: value!, extremeValue: value! }; } @@ -221,13 +234,38 @@ export const getColorFunction = ( !value?.toLowerCase().includes((targetValue as string).toLowerCase()) ? { cutoffValue: targetValue!, extremeValue: targetValue! } : false; + + break; + case Comparator.IsTrue: + comparatorFunction = (value: boolean) => + isBoolean(value) && value + ? { cutoffValue: targetValue!, extremeValue: targetValue! } + : false; + break; + case Comparator.IsFalse: + comparatorFunction = (value: boolean) => + isBoolean(value) && !value + ? { cutoffValue: targetValue!, extremeValue: targetValue! } + : false; + break; + case Comparator.IsNull: + comparatorFunction = (value: boolean) => + isBoolean(value) && value === null + ? { cutoffValue: targetValue!, extremeValue: targetValue! } + : false; + break; + case Comparator.IsNotNull: + comparatorFunction = (value: boolean) => + isBoolean(value) && value !== null + ? { cutoffValue: targetValue!, extremeValue: targetValue! } + : false; break; default: comparatorFunction = () => false; break; } - return (value: number | string) => { + return (value: number | string | boolean) => { const compareResult = comparatorFunction(value, columnValues); if (compareResult === false) return undefined; const { cutoffValue, extremeValue } = compareResult; @@ -289,3 +327,7 @@ export const getColorFormatters = memoizeOne( function isString(value: unknown) { return typeof value === 'string'; } + +function isBoolean(value: unknown) { + return typeof value === 'boolean'; +} From 390330d4f6f087cf79a56c86f4a980fdb38b4d03 Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Sat, 15 Nov 2025 22:11:01 -0500 Subject: [PATCH 02/11] feat(dashboard): add support for boolean conditional formatting in controlPanel of plugin-chart-table --- .../plugins/plugin-chart-table/src/controlPanel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index 8d4006e20672..ba00d26e05e3 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -797,7 +797,8 @@ const config: ControlPanelConfig = { if ( coltypes[index] === GenericDataType.Numeric || (!explore?.controls?.time_compare?.value && - coltypes[index] === GenericDataType.String) + (coltypes[index] === GenericDataType.String || + coltypes[index] === GenericDataType.Boolean)) ) { acc.push({ value: colname, From dd8276504e8dd83f35104bc3802e280b82898810 Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Wed, 26 Nov 2025 20:30:28 -0500 Subject: [PATCH 03/11] fix(dashboard): fix boolean conditional formatting types in getColorFormatters --- .../superset-ui-chart-controls/src/types.ts | 2 +- .../src/utils/getColorFormatters.ts | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 36f773c8d5ca..83bbcc5a0e63 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -490,7 +490,7 @@ export type ColorFormatters = { column: string; toAllRow?: boolean; toTextColor?: boolean; - getColorFromValue: (value: number | string | boolean) => string | undefined; + getColorFromValue: (value: number | string | boolean | null) => string | undefined; }[]; export default {}; 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 c11d09f33e9a..eb55e9aeba4a 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 @@ -32,7 +32,7 @@ const MIN_OPACITY_BOUNDED = 0.05; const MIN_OPACITY_UNBOUNDED = 0; const MAX_OPACITY = 1; export const getOpacity = ( - value: number | string | boolean, + value: number | string | boolean | null, cutoffPoint: number | string | boolean, extremeValue: number | string | boolean, minOpacity = MIN_OPACITY_BOUNDED, @@ -75,15 +75,15 @@ export const getColorFunction = ( targetValueRight, colorScheme, }: ConditionalFormattingConfig, - columnValues: number[] | string[] | boolean[], + columnValues: number[] | string[] | (boolean | null)[], alpha?: boolean, ) => { let minOpacity = MIN_OPACITY_BOUNDED; const maxOpacity = MAX_OPACITY; let comparatorFunction: ( - value: number | string | boolean, - allValues: number[] | string[] | boolean[], + value: number | string | boolean | null, + allValues: number[] | string[] | (boolean | null)[], ) => | false | { @@ -110,7 +110,7 @@ export const getColorFunction = ( case Comparator.None: minOpacity = MIN_OPACITY_UNBOUNDED; comparatorFunction = ( - value: number | string | boolean, + value: number | string | boolean | null, allValues: number[], ) => { if (typeof value !== 'number') { @@ -237,25 +237,25 @@ export const getColorFunction = ( break; case Comparator.IsTrue: - comparatorFunction = (value: boolean) => + comparatorFunction = (value: boolean | null) => isBoolean(value) && value ? { cutoffValue: targetValue!, extremeValue: targetValue! } : false; break; case Comparator.IsFalse: - comparatorFunction = (value: boolean) => + comparatorFunction = (value: boolean | null) => isBoolean(value) && !value ? { cutoffValue: targetValue!, extremeValue: targetValue! } : false; break; case Comparator.IsNull: - comparatorFunction = (value: boolean) => - isBoolean(value) && value === null + comparatorFunction = (value: boolean | null) => + value === null ? { cutoffValue: targetValue!, extremeValue: targetValue! } : false; break; case Comparator.IsNotNull: - comparatorFunction = (value: boolean) => + comparatorFunction = (value: boolean | null) => isBoolean(value) && value !== null ? { cutoffValue: targetValue!, extremeValue: targetValue! } : false; @@ -265,7 +265,7 @@ export const getColorFunction = ( break; } - return (value: number | string | boolean) => { + return (value: number | string | boolean | null) => { const compareResult = comparatorFunction(value, columnValues); if (compareResult === false) return undefined; const { cutoffValue, extremeValue } = compareResult; From cd3167ef2cd85b368de7b212a045b92f8e3ff224 Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Wed, 26 Nov 2025 23:36:56 -0500 Subject: [PATCH 04/11] test(dashboard): add unit testing for boolean conditional formatting in getColorFormatters --- .../superset-ui-chart-controls/src/types.ts | 4 +- .../test/utils/getColorFormatters.test.ts | 107 ++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 83bbcc5a0e63..c16078ba811b 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -490,7 +490,9 @@ export type ColorFormatters = { column: string; toAllRow?: boolean; toTextColor?: boolean; - getColorFromValue: (value: number | string | boolean | null) => string | undefined; + getColorFromValue: ( + value: number | string | boolean | null, + ) => string | undefined; }[]; export default {}; 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 c7d6c99b2cf0..a729afb69617 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 @@ -35,6 +35,9 @@ const countValues = mockData.map(row => row.count); const strData = [{ name: 'Brian' }, { name: 'Carlos' }, { name: 'Diana' }]; const strValues = strData.map(row => row.name); +const boolData = [{ isMember: true }, { isMember: false }, { isMember: null }]; +const boolValues = boolData.map(row => row.isMember); + test('round', () => { expect(round(1)).toEqual(1); expect(round(1, 2)).toEqual(1); @@ -443,6 +446,66 @@ test('getColorFunction None', () => { expect(colorFunction('Brian')).toEqual('#FF0000FF'); }); +test('getColorFunction IsTrue', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.IsTrue, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + boolValues, + ); + expect(colorFunction(true)).toEqual('#FF0000FF'); + expect(colorFunction(false)).toBeUndefined(); + expect(colorFunction(null)).toBeUndefined(); +}); + +test('getColorFunction IsFalse', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.IsFalse, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + boolValues, + ); + expect(colorFunction(true)).toBeUndefined(); + expect(colorFunction(false)).toEqual('#FF0000FF'); + expect(colorFunction(null)).toBeUndefined(); +}); + +test('getColorFunction IsNull', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.IsNull, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + boolValues, + ); + expect(colorFunction(true)).toBeUndefined(); + expect(colorFunction(false)).toBeUndefined(); + expect(colorFunction(null)).toEqual('#FF0000FF'); +}); + +test('getColorFunction IsNotNull', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.IsNotNull, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + boolValues, + ); + expect(colorFunction(true)).toEqual('#FF0000FF'); + expect(colorFunction(false)).toEqual('#FF0000FF'); + expect(colorFunction(null)).toBeUndefined(); +}); + test('correct column config', () => { const columnConfig = [ { @@ -532,3 +595,47 @@ test('correct column string config', () => { expect(colorFormatters[3].column).toEqual('name'); expect(colorFormatters[3].getColorFromValue('Carlos')).toEqual('#FF0000FF'); }); + +test('correct column boolean config', () => { + const columnConfigBoolean = [ + { + operator: Comparator.IsTrue, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + { + operator: Comparator.IsFalse, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + { + operator: Comparator.IsNull, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + { + operator: Comparator.IsNotNull, + targetValue: true, + colorScheme: '#FF0000', + column: 'isMember', + }, + ]; + const colorFormatters = getColorFormatters(columnConfigBoolean, boolData); + expect(colorFormatters.length).toEqual(4); + + expect(colorFormatters[0].column).toEqual('isMember'); + expect(colorFormatters[0].getColorFromValue(true)).toEqual('#FF0000FF'); + + expect(colorFormatters[1].column).toEqual('isMember'); + expect(colorFormatters[1].getColorFromValue(false)).toEqual('#FF0000FF'); + + expect(colorFormatters[2].column).toEqual('isMember'); + expect(colorFormatters[2].getColorFromValue(null)).toEqual('#FF0000FF'); + + expect(colorFormatters[3].column).toEqual('isMember'); + expect(colorFormatters[3].getColorFromValue(true)).toEqual('#FF0000FF'); + expect(colorFormatters[3].getColorFromValue(false)).toEqual('#FF0000FF'); +}); From 44846b95d7f323a9f7d85207898b0055f55911c2 Mon Sep 17 00:00:00 2001 From: Morris Date: Fri, 28 Nov 2025 01:14:53 -0500 Subject: [PATCH 05/11] feat(dashboard): apply boolean conditional formatting to dashboard --- .../FormattingPopoverContent.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index aa63c778a186..13d5438f739d 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -91,6 +91,13 @@ const stringOperatorOptions = [ { value: Comparator.NotContaining, label: t('not containing') }, ]; +const booleanOperatorOptions = [ + { value: Comparator.IsNull, label: t('is null') }, + { value: Comparator.IsTrue, label: t('is true') }, + { value: Comparator.IsFalse, label: t('is false') }, + { value: Comparator.IsNotNull, label: t('is not null') }, +]; + const targetValueValidator = ( compare: (targetValue: number, compareValue: number) => boolean, @@ -160,7 +167,9 @@ const renderOperator = ({ const options = columnType === GenericDataType.String ? stringOperatorOptions - : operatorOptions; + : columnType === GenericDataType.Boolean + ? booleanOperatorOptions + : operatorOptions; return ( { const columnTypeString = columnType === GenericDataType.String; + const columnTypeBoolean = columnType === GenericDataType.Boolean; const operatorColSpan = columnTypeString ? 8 : 6; const valueColSpan = columnTypeString ? 16 : 18; + if (columnTypeBoolean) { + return ( + + {renderOperator({ columnType })} + + + ); + } + return isOperatorNone(getFieldValue('operator')) ? ( {renderOperator({ columnType })} @@ -307,7 +333,9 @@ export const FormattingPopoverContent = ({ const defaultOperator = newColumnType === GenericDataType.String ? stringOperatorOptions[0].value - : operatorOptions[0].value; + : newColumnType === GenericDataType.Boolean + ? booleanOperatorOptions[0].value + : operatorOptions[0].value; form.setFieldsValue({ operator: defaultOperator, From 28ebb62206d875ac38949bf07cc09798c3735136 Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Fri, 28 Nov 2025 14:10:33 -0500 Subject: [PATCH 06/11] fix(dashboard): fix linting issue in FormattingPopoverContent --- .../ConditionalFormattingControl/FormattingPopoverContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index 13d5438f739d..687a6929ebac 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -205,7 +205,7 @@ const renderOperatorFields = ( label={t('Target value')} initialValue={''} hidden - /> + /> ); From 695fb150d025217acbff78c93c713a84003dd69f Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Fri, 28 Nov 2025 16:58:00 -0500 Subject: [PATCH 07/11] fix(dashboard): fix getColorFormatters type and logic --- .../superset-ui-chart-controls/src/types.ts | 2 +- .../src/utils/getColorFormatters.ts | 23 ++++--------------- .../test/utils/getColorFormatters.test.ts | 16 ++++++------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index c16078ba811b..02cb2ab53608 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -477,7 +477,7 @@ export const MultipleValueComparators = [ export type ConditionalFormattingConfig = { operator?: Comparator; - targetValue?: number | string | boolean; + targetValue?: number | string; targetValueLeft?: number; targetValueRight?: number; column?: string; 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 eb55e9aeba4a..ccf16208721c 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 @@ -33,17 +33,12 @@ const MIN_OPACITY_UNBOUNDED = 0; const MAX_OPACITY = 1; export const getOpacity = ( value: number | string | boolean | null, - cutoffPoint: number | string | boolean, - extremeValue: number | string | boolean, + cutoffPoint: number | string, + extremeValue: number | string, minOpacity = MIN_OPACITY_BOUNDED, maxOpacity = MAX_OPACITY, ) => { - if ( - extremeValue === cutoffPoint || - typeof value !== 'number' || - typeof cutoffPoint === 'boolean' || - typeof extremeValue === 'boolean' - ) { + if (extremeValue === cutoffPoint || typeof value !== 'number') { return maxOpacity; } const numCutoffPoint = @@ -84,12 +79,7 @@ export const getColorFunction = ( let comparatorFunction: ( value: number | string | boolean | null, allValues: number[] | string[] | (boolean | null)[], - ) => - | false - | { - cutoffValue: number | string | boolean; - extremeValue: number | string | boolean; - }; + ) => false | { cutoffValue: number | string; extremeValue: number | string }; if (operator === undefined || colorScheme === undefined) { return () => undefined; } @@ -109,10 +99,7 @@ export const getColorFunction = ( switch (operator) { case Comparator.None: minOpacity = MIN_OPACITY_UNBOUNDED; - comparatorFunction = ( - value: number | string | boolean | null, - allValues: number[], - ) => { + comparatorFunction = (value: number | string, allValues: number[]) => { if (typeof value !== 'number') { return { cutoffValue: value!, extremeValue: value! }; } 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 a729afb69617..3ceab82ef3cb 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 @@ -450,7 +450,7 @@ test('getColorFunction IsTrue', () => { const colorFunction = getColorFunction( { operator: Comparator.IsTrue, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, @@ -465,7 +465,7 @@ test('getColorFunction IsFalse', () => { const colorFunction = getColorFunction( { operator: Comparator.IsFalse, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, @@ -480,7 +480,7 @@ test('getColorFunction IsNull', () => { const colorFunction = getColorFunction( { operator: Comparator.IsNull, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, @@ -495,7 +495,7 @@ test('getColorFunction IsNotNull', () => { const colorFunction = getColorFunction( { operator: Comparator.IsNotNull, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, @@ -600,25 +600,25 @@ test('correct column boolean config', () => { const columnConfigBoolean = [ { operator: Comparator.IsTrue, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, { operator: Comparator.IsFalse, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, { operator: Comparator.IsNull, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, { operator: Comparator.IsNotNull, - targetValue: true, + targetValue: '', colorScheme: '#FF0000', column: 'isMember', }, From 8170736ff4c0a427a5d762121265741aa29f1ce6 Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Fri, 28 Nov 2025 17:18:25 -0500 Subject: [PATCH 08/11] fix(dashboard): make isNull comparator function works in the UI --- .../plugins/plugin-chart-table/src/TableChart.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index c2061d738d11..52018864c214 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -907,10 +907,6 @@ export default function TableChart( formatter: ColorFormatters[number], valueToFormat: any, ) => { - const hasValue = - valueToFormat !== undefined && valueToFormat !== null; - if (!hasValue) return; - const formatterResult = formatter.getColorFromValue(valueToFormat); if (!formatterResult) return; From a79f8bfe3e4ec79130926fc7e9908471ef6a3aea Mon Sep 17 00:00:00 2001 From: Morris Date: Sat, 29 Nov 2025 20:22:44 -0500 Subject: [PATCH 09/11] test(dashboard): add unit tests for boolean conditional formatting in TableChat and FormattingPopoverContent --- .../test/TableChart.test.tsx | 126 +++++++++++++++++- .../plugin-chart-table/test/testData.ts | 26 ++++ .../FormattingPopoverContent.test.tsx | 21 +++ 3 files changed, 172 insertions(+), 1 deletion(-) 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 d849f75d5832..495c5f5cf22e 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -613,7 +613,9 @@ describe('plugin-chart-table', () => { expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( '', ); - expect(getComputedStyle(screen.getByText('N/A')).background).toBe(''); + expect(getComputedStyle(screen.getByText('N/A')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); }); test('should display original label in grouped headers', () => { const props = transformProps(testData.comparison); @@ -986,6 +988,128 @@ describe('plugin-chart-table', () => { ); }); + test('render color with boolean column color formatter (operator is true)', () => { + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + expect(getComputedStyle(screen.getByText('true')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByText('false')).background).toBe(''); + }); + + test('render color with boolean column color formatter (operator is false)', () => { + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + expect(getComputedStyle(screen.getByText('false')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByText('true')).background).toBe(''); + }); + + test('render color with boolean column color formatter (operator is null)', () => { + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + expect(getComputedStyle(screen.getByText('N/A')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByText('true')).background).toBe(''); + expect(getComputedStyle(screen.getByText('false')).background).toBe(''); + }); + + test('render color with boolean column color formatter (operator is not null)', () => { + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + const trueElements = screen.getAllByText('true'); + const falseElements = screen.getAllByText('false'); + expect(getComputedStyle(trueElements[0]).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(falseElements[0]).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByText('N/A')).background).toBe(''); + }); + test('render color with column color formatter to entire row', () => { render( ProviderWrapper({ diff --git a/superset-frontend/plugins/plugin-chart-table/test/testData.ts b/superset-frontend/plugins/plugin-chart-table/test/testData.ts index 02d88e021e2c..ca3ed52e3342 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/testData.ts +++ b/superset-frontend/plugins/plugin-chart-table/test/testData.ts @@ -370,6 +370,31 @@ const bigint = { ], }; +const nameAndBoolean: TableChartProps = { + ...new ChartProps(basicChartProps), + queriesData: [ + { + ...basicQueryResult, + colnames: ['name', 'is_adult'], + coltypes: [GenericDataType.String, GenericDataType.Boolean], + data: [ + { + name: 'Alice', + is_adult: true, + }, + { + name: 'Bob', + is_adult: false, + }, + { + name: 'Carl', + is_adult: null, + }, + ], + }, + ], +}; + export default { basic, advanced, @@ -379,4 +404,5 @@ export default { empty, raw, bigint, + nameAndBoolean, }; diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx index aaa17cd67d3c..29b9fe7b26f6 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx @@ -40,6 +40,11 @@ const columnsStringType = [ { label: 'Column 2', value: 'column2', dataType: GenericDataType.String }, ]; +const columnsBooleanType = [ + { label: 'Column 1', value: 'column1', dataType: GenericDataType.Boolean }, + { label: 'Column 2', value: 'column2', dataType: GenericDataType.Boolean }, +]; + const extraColorChoices = [ { value: ColorSchemeEnum.Green, @@ -148,6 +153,22 @@ test('displays the correct input fields based on the selected string type operat expect(await screen.findByLabelText('Target value')).toBeInTheDocument(); }); +test('does not display the input fields when selected a boolean type operator', async () => { + render( + , + ); + + fireEvent.change(screen.getAllByLabelText('Operator')[0], { + target: { value: Comparator.IsTrue }, + }); + fireEvent.click(await screen.findByTitle('is true')); + expect(await screen.queryByLabelText('Target value')).toBeNull(); +}); + test('displays the toAllRow and toTextColor flags based on the selected numeric type operator', () => { render( Date: Sun, 30 Nov 2025 22:31:54 -0500 Subject: [PATCH 10/11] fix(dashboard): simplify design on choosing columnType in FormattingPopoverContent --- .../FormattingPopoverContent.tsx | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index 687a6929ebac..09d27184b44c 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -164,12 +164,17 @@ const renderOperator = ({ showOnlyNone, columnType, }: { showOnlyNone?: boolean; columnType?: GenericDataType } = {}) => { - const options = - columnType === GenericDataType.String - ? stringOperatorOptions - : columnType === GenericDataType.Boolean - ? booleanOperatorOptions - : operatorOptions; + let options; + switch (columnType) { + case GenericDataType.String: + options = stringOperatorOptions; + break; + case GenericDataType.Boolean: + options = booleanOperatorOptions; + break; + default: + options = operatorOptions; + } return ( { const newColumnType = columns.find(item => item.value === value)?.dataType; if (newColumnType !== previousColumnType) { - const defaultOperator = - newColumnType === GenericDataType.String - ? stringOperatorOptions[0].value - : newColumnType === GenericDataType.Boolean - ? booleanOperatorOptions[0].value - : operatorOptions[0].value; + let defaultOperator: Comparator; + + switch (newColumnType) { + case GenericDataType.String: + defaultOperator = stringOperatorOptions[0].value; + break; + + case GenericDataType.Boolean: + defaultOperator = booleanOperatorOptions[0].value; + break; + + default: + defaultOperator = operatorOptions[0].value; + } form.setFieldsValue({ operator: defaultOperator, From e2e05996600d0304551adc1e42a9095ecff09f15 Mon Sep 17 00:00:00 2001 From: Edison Liem Date: Tue, 2 Dec 2025 17:07:47 -0500 Subject: [PATCH 11/11] fix(dashboard): fix conditional formatting panel boolean operator menu size --- .../ConditionalFormattingControl/FormattingPopoverContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index 09d27184b44c..1156872c447e 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -197,7 +197,7 @@ const renderOperatorFields = ( ) => { const columnTypeString = columnType === GenericDataType.String; const columnTypeBoolean = columnType === GenericDataType.Boolean; - const operatorColSpan = columnTypeString ? 8 : 6; + const operatorColSpan = columnTypeString || columnTypeBoolean ? 8 : 6; const valueColSpan = columnTypeString ? 16 : 18; if (columnTypeBoolean) {