diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index 2148904e537..814d82f9d45 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -796,45 +796,63 @@ const config: ControlPanelConfig = { }, ); } - const { colnames, coltypes } = + const { colnames: queryColnames, coltypes: queryColtypes } = chart?.queriesResponse?.[0] ?? {}; - const allColumns = - Array.isArray(colnames) && Array.isArray(coltypes) - ? [ - { - value: ObjectFormattingEnum.ENTIRE_ROW, - label: t('entire row'), - dataType: GenericDataType.String, - }, - ...colnames.map((colname: string, index: number) => ({ + const hasQueryColumns = + Array.isArray(queryColnames) && + Array.isArray(queryColtypes) && + queryColnames.length > 0; + + // Fall back to datasource columns when query results are empty + const datasourceColumns = ensureIsArray( + (explore?.datasource as Dataset)?.columns, + ); + const colnames = hasQueryColumns + ? queryColnames + : datasourceColumns.map((col: ColumnMeta) => col.column_name); + const coltypes = hasQueryColumns + ? queryColtypes + : datasourceColumns.map( + (col: ColumnMeta) => + col.type_generic ?? GenericDataType.String, + ); + + const hasColumns = colnames.length > 0; + const allColumns = hasColumns + ? [ + { + value: ObjectFormattingEnum.ENTIRE_ROW, + label: t('entire row'), + dataType: GenericDataType.String, + }, + ...colnames.map((colname: string, index: number) => ({ + value: colname, + label: Array.isArray(verboseMap) + ? colname + : (verboseMap[colname] ?? colname), + dataType: coltypes[index], + })), + ] + : []; + const numericColumns = hasColumns + ? colnames.reduce((acc, colname, index) => { + if ( + coltypes[index] === GenericDataType.Numeric || + (!hasTimeComparison && + (coltypes[index] === GenericDataType.String || + coltypes[index] === GenericDataType.Boolean)) + ) { + acc.push({ value: colname, label: Array.isArray(verboseMap) ? colname : (verboseMap[colname] ?? colname), dataType: coltypes[index], - })), - ] - : []; - const numericColumns = - Array.isArray(colnames) && Array.isArray(coltypes) - ? colnames.reduce((acc, colname, index) => { - if ( - coltypes[index] === GenericDataType.Numeric || - (!hasTimeComparison && - (coltypes[index] === GenericDataType.String || - coltypes[index] === GenericDataType.Boolean)) - ) { - acc.push({ - value: colname, - label: Array.isArray(verboseMap) - ? colname - : (verboseMap[colname] ?? colname), - dataType: coltypes[index], - }); - } - return acc; - }, []) - : []; + }); + } + return acc; + }, []) + : []; const columnOptions = hasTimeComparison ? processComparisonColumns( numericColumns || [], diff --git a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx index b85535d5340..9a35494e78a 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx @@ -25,6 +25,7 @@ import { ControlPanelState, ControlState, ColorSchemeEnum, + ObjectFormattingEnum, } from '@superset-ui/chart-controls'; import config from '../src/controlPanel'; @@ -55,11 +56,12 @@ const createMockControlState = (value: string[] | undefined): ControlState => ({ const createMockExplore = ( timeCompareValue: string[] | undefined, + datasourceColumns: Partial['columns'] = [], ): ControlPanelState => ({ slice: { slice_id: 123 }, datasource: { verbose_map: { col1: 'Column 1', col2: 'Column 2' }, - columns: [], + columns: datasourceColumns, } as Partial as Dataset, controls: { time_compare: createMockControlState(timeCompareValue), @@ -206,3 +208,144 @@ test('static extraColorChoices removed from config', () => { expect(controlConfig?.extraColorChoices).toBeUndefined(); }); + +test('columnOptions falls back to datasource columns when queriesResponse is empty', () => { + const controlConfig = findConditionalFormattingControl(); + expect(controlConfig).toBeTruthy(); + + const datasourceColumns = [ + { column_name: 'revenue', type_generic: GenericDataType.Numeric }, + { column_name: 'name', type_generic: GenericDataType.String }, + ]; + const explore = createMockExplore(undefined, datasourceColumns); + const chart = { chartStatus: 'success' as const, queriesResponse: null }; + const result = controlConfig!.mapStateToProps!( + explore, + createMockControlStateForConditionalFormatting(), + chart, + ); + + expect(result.columnOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ value: 'revenue' }), + expect.objectContaining({ value: 'name' }), + ]), + ); + expect(result.allColumns).toEqual( + expect.arrayContaining([ + expect.objectContaining({ value: 'revenue' }), + expect.objectContaining({ value: 'name' }), + ]), + ); +}); + +test('columnOptions prefers queriesResponse over datasource columns', () => { + const controlConfig = findConditionalFormattingControl(); + expect(controlConfig).toBeTruthy(); + + const datasourceColumns = [ + { column_name: 'revenue', type_generic: GenericDataType.Numeric }, + { column_name: 'extra_col', type_generic: GenericDataType.String }, + ]; + const explore = createMockExplore(undefined, datasourceColumns); + const chart = createMockChart(); + const result = controlConfig!.mapStateToProps!( + explore, + createMockControlStateForConditionalFormatting(), + chart, + ); + + expect(result.columnOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ value: 'col1' }), + expect.objectContaining({ value: 'col2' }), + ]), + ); + expect(result.columnOptions).not.toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'extra_col' })]), + ); +}); + +test('columnOptions falls back to datasource when queriesResponse has empty colnames', () => { + const controlConfig = findConditionalFormattingControl(); + expect(controlConfig).toBeTruthy(); + + const datasourceColumns = [ + { column_name: 'revenue', type_generic: GenericDataType.Numeric }, + ]; + const explore = createMockExplore(undefined, datasourceColumns); + const chart = { + chartStatus: 'success' as const, + queriesResponse: [{ colnames: [], coltypes: [] }], + }; + const result = controlConfig!.mapStateToProps!( + explore, + createMockControlStateForConditionalFormatting(), + chart, + ); + + expect(result.columnOptions).toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'revenue' })]), + ); +}); + +test('columnOptions returns empty when both queriesResponse and datasource have no columns', () => { + const controlConfig = findConditionalFormattingControl(); + expect(controlConfig).toBeTruthy(); + + const explore = createMockExplore(undefined, []); + const chart = { chartStatus: 'success' as const, queriesResponse: null }; + const result = controlConfig!.mapStateToProps!( + explore, + createMockControlStateForConditionalFormatting(), + chart, + ); + + expect(result.columnOptions).toEqual([]); + expect(result.allColumns).toEqual([]); +}); + +test('allColumns includes ENTIRE_ROW when falling back to datasource columns', () => { + const controlConfig = findConditionalFormattingControl(); + expect(controlConfig).toBeTruthy(); + + const datasourceColumns = [ + { column_name: 'revenue', type_generic: GenericDataType.Numeric }, + ]; + const explore = createMockExplore(undefined, datasourceColumns); + const chart = { chartStatus: 'success' as const, queriesResponse: null }; + const result = controlConfig!.mapStateToProps!( + explore, + createMockControlStateForConditionalFormatting(), + chart, + ); + + expect(result.allColumns).toEqual( + expect.arrayContaining([ + expect.objectContaining({ value: ObjectFormattingEnum.ENTIRE_ROW }), + ]), + ); +}); + +test('columnOptions defaults type_generic to String when missing from datasource columns', () => { + const controlConfig = findConditionalFormattingControl(); + expect(controlConfig).toBeTruthy(); + + const datasourceColumns = [{ column_name: 'untyped_col' }]; + const explore = createMockExplore(undefined, datasourceColumns); + const chart = { chartStatus: 'success' as const, queriesResponse: null }; + const result = controlConfig!.mapStateToProps!( + explore, + createMockControlStateForConditionalFormatting(), + chart, + ); + + expect(result.columnOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'untyped_col', + dataType: GenericDataType.String, + }), + ]), + ); +});