diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index f8105c5198f..9faa71d8455 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -345,6 +345,7 @@ export default function TableChart( hasServerPageLengthChanged, serverPageLength, slice_id, + columnLabelToNameMap = {}, } = props; const comparisonColumns = useMemo( @@ -457,19 +458,22 @@ export default function TableChart( groupBy.length === 0 ? [] : groupBy.map(col => { + // Resolve adhoc column labels back to original column names + // so that cross-filters work on the receiving chart + const resolvedCol = columnLabelToNameMap[col] ?? col; const val = ensureIsArray(updatedFilters?.[col]); if (!val.length) return { - col, + col: resolvedCol, op: 'IS NULL' as const, }; return { - col, + col: resolvedCol, op: 'IN' as const, val: val.map(el => el instanceof Date ? el.getTime() : el!, ), - grain: col === DTTM_ALIAS ? timeGrain : undefined, + grain: resolvedCol === DTTM_ALIAS ? timeGrain : undefined, }; }), }, @@ -485,7 +489,13 @@ export default function TableChart( isCurrentValueSelected: isActiveFilterValue(key, value), }; }, - [filters, isActiveFilterValue, timestampFormatter, timeGrain], + [ + filters, + isActiveFilterValue, + timestampFormatter, + timeGrain, + columnLabelToNameMap, + ], ); const toggleFilter = useCallback( diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts index 30ad5f9ed69..ad1977ba8de 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts @@ -29,6 +29,7 @@ import { getNumberFormatter, getTimeFormatter, getTimeFormatterForGranularity, + isAdhocColumn, normalizeCurrency, NumberFormats, QueryMode, @@ -532,6 +533,20 @@ const transformProps = ( comparison_type, slice_id, } = formData; + // Build a mapping from column labels to original column names. + // When a user creates an adhoc column with a custom label (e.g. sqlExpression: "state", + // label: "State_Renamed"), the query result uses the label as the column name. + // Cross-filtering needs the original column name to work on the receiving chart. + const columnLabelToNameMap: Record = {}; + const formColumns = ensureIsArray( + queryMode === QueryMode.Raw ? formData.all_columns : formData.groupby, + ); + formColumns.forEach(col => { + if (isAdhocColumn(col) && col.label && col.label !== col.sqlExpression) { + columnLabelToNameMap[col.label] = col.sqlExpression; + } + }); + const isUsingTimeComparison = !isEmpty(time_compare) && queryMode === QueryMode.Aggregate && @@ -791,6 +806,7 @@ const transformProps = ( hasServerPageLengthChanged, serverPageLength, slice_id, + columnLabelToNameMap, }; }; diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts index e7f02eaf6d7..fda8b78ab26 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts @@ -124,6 +124,10 @@ export interface TableChartTransformedProps { hasServerPageLengthChanged: boolean; serverPageLength: number; slice_id: number; + // Maps column labels (used as keys in query results) back to original + // column names for cross-filtering, so that adhoc columns with custom labels + // emit the correct column name in cross-filter data masks + columnLabelToNameMap?: Record; } export default {}; 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 7b702bb24a6..7cc0ef9239a 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -1787,5 +1787,145 @@ describe('plugin-chart-table', () => { }); }); }); + + test('should build columnLabelToNameMap for adhoc columns with custom labels', () => { + const result = transformProps({ + ...testData.basic, + rawFormData: { + ...testData.basic.rawFormData, + query_mode: QueryMode.Aggregate, + groupby: [ + { + sqlExpression: 'name', + label: 'Name_Renamed', + expressionType: 'SQL', + }, + ], + metrics: ['sum__num'], + }, + emitCrossFilters: true, + queriesData: [ + { + ...testData.basic.queriesData[0], + colnames: ['Name_Renamed', 'sum__num'], + coltypes: [GenericDataType.String, GenericDataType.Numeric], + data: [{ Name_Renamed: 'Michael', sum__num: 2467063 }], + }, + ], + }); + expect(result.columnLabelToNameMap).toEqual({ + Name_Renamed: 'name', + }); + }); + + test('should not populate columnLabelToNameMap for physical columns', () => { + const result = transformProps({ + ...testData.basic, + rawFormData: { + ...testData.basic.rawFormData, + query_mode: QueryMode.Aggregate, + groupby: ['name'], + metrics: ['sum__num'], + }, + emitCrossFilters: true, + queriesData: [ + { + ...testData.basic.queriesData[0], + colnames: ['name', 'sum__num'], + coltypes: [GenericDataType.String, GenericDataType.Numeric], + data: [{ name: 'Michael', sum__num: 2467063 }], + }, + ], + }); + expect(result.columnLabelToNameMap).toEqual({}); + }); + + test('should not populate columnLabelToNameMap when adhoc label matches sqlExpression', () => { + const result = transformProps({ + ...testData.basic, + rawFormData: { + ...testData.basic.rawFormData, + query_mode: QueryMode.Aggregate, + groupby: [ + { + sqlExpression: 'name', + label: 'name', + expressionType: 'SQL', + }, + ], + metrics: ['sum__num'], + }, + emitCrossFilters: true, + queriesData: [ + { + ...testData.basic.queriesData[0], + colnames: ['name', 'sum__num'], + coltypes: [GenericDataType.String, GenericDataType.Numeric], + data: [{ name: 'Michael', sum__num: 2467063 }], + }, + ], + }); + expect(result.columnLabelToNameMap).toEqual({}); + }); + + test('cross-filter on adhoc column with custom label emits original column name', () => { + const setDataMask = jest.fn(); + const baseProps = transformProps({ + ...testData.basic, + rawFormData: { + ...testData.basic.rawFormData, + query_mode: QueryMode.Aggregate, + groupby: [ + { + sqlExpression: 'name', + label: 'Name_Renamed', + expressionType: 'SQL', + }, + ], + metrics: ['sum__num'], + }, + filterState: { filters: {} }, + ownState: {}, + hooks: { + onAddFilter: jest.fn(), + setDataMask, + onContextMenu: jest.fn(), + }, + emitCrossFilters: true, + queriesData: [ + { + ...testData.basic.queriesData[0], + colnames: ['Name_Renamed', 'sum__num'], + coltypes: [GenericDataType.String, GenericDataType.Numeric], + data: [ + { Name_Renamed: 'Michael', sum__num: 2467063 }, + { Name_Renamed: 'Joe', sum__num: 2467 }, + ], + }, + ], + }); + + render( + + + , + ); + + // Verify the table rendered with data + expect(screen.getByText('Michael')).toBeInTheDocument(); + + // Find the td cell containing "Michael" and click it + const cell = screen.getByText('Michael').closest('td')!; + fireEvent.click(cell); + + expect(setDataMask).toHaveBeenCalled(); + const lastCall = + setDataMask.mock.calls[setDataMask.mock.calls.length - 1][0]; + const { filters } = lastCall.extraFormData; + expect(filters).toHaveLength(1); + // Should emit the original column name, not the label + expect(filters[0].col).toBe('name'); + expect(filters[0].val).toEqual(['Michael']); + }); }); });