fix(table): cross-filtering breaks after renaming column labels via Custom SQL (#38858)

(cherry picked from commit aba7e6dae4)
This commit is contained in:
Enzo Martellucci
2026-04-10 06:02:18 +02:00
committed by Michael S. Molina
parent 3fdbbb6e7e
commit 1f7838367f
4 changed files with 174 additions and 4 deletions

View File

@@ -343,6 +343,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
hasServerPageLengthChanged,
serverPageLength,
slice_id,
columnLabelToNameMap = {},
} = props;
const comparisonColumns = useMemo(
@@ -455,19 +456,22 @@ export default function TableChart<D extends DataRecord = DataRecord>(
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,
};
}),
},
@@ -483,7 +487,13 @@ export default function TableChart<D extends DataRecord = DataRecord>(
isCurrentValueSelected: isActiveFilterValue(key, value),
};
},
[filters, isActiveFilterValue, timestampFormatter, timeGrain],
[
filters,
isActiveFilterValue,
timestampFormatter,
timeGrain,
columnLabelToNameMap,
],
);
const toggleFilter = useCallback(

View File

@@ -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<string, string> = {};
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,
};
};

View File

@@ -124,6 +124,10 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
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<string, string>;
}
export default {};

View File

@@ -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(
<ProviderWrapper>
<TableChart {...baseProps} emitCrossFilters sticky={false} />
</ProviderWrapper>,
);
// 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']);
});
});
});