mirror of
https://github.com/apache/superset.git
synced 2026-04-23 10:04:45 +00:00
fix(table): fix cross-filter not clearing on second click in Interactive Table (#39253)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
44e77fdf2b
commit
c2d96e0dce
@@ -40,7 +40,7 @@ import {
|
||||
GridReadyEvent,
|
||||
GridState,
|
||||
CellClickedEvent,
|
||||
IMenuActionParams,
|
||||
SelectionChangedEvent,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
@@ -96,8 +96,9 @@ export interface AgGridTableProps {
|
||||
percentMetrics: string[];
|
||||
serverPageLength: number;
|
||||
hasServerPageLengthChanged: boolean;
|
||||
handleCrossFilter: (event: CellClickedEvent | IMenuActionParams) => void;
|
||||
isActiveFilterValue: (key: string, val: DataRecordValue) => boolean;
|
||||
handleCellClicked: (event: CellClickedEvent) => void;
|
||||
handleSelectionChanged: (event: SelectionChangedEvent) => void;
|
||||
filters?: Record<string, DataRecordValue[]> | null;
|
||||
renderTimeComparisonDropdown: () => JSX.Element | null;
|
||||
cleanedTotals: DataRecord;
|
||||
showTotals: boolean;
|
||||
@@ -135,8 +136,9 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
percentMetrics,
|
||||
serverPageLength,
|
||||
hasServerPageLengthChanged,
|
||||
handleCrossFilter,
|
||||
isActiveFilterValue,
|
||||
handleCellClicked,
|
||||
handleSelectionChanged,
|
||||
filters,
|
||||
renderTimeComparisonDropdown,
|
||||
cleanedTotals,
|
||||
showTotals,
|
||||
@@ -422,6 +424,15 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
}
|
||||
}, [width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(!filters || Object.keys(filters).length === 0) &&
|
||||
gridRef.current?.api?.getSelectedRows().length
|
||||
) {
|
||||
gridRef.current.api.deselectAll();
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
const onGridReady = (params: GridReadyEvent) => {
|
||||
// This will make columns fill the grid width
|
||||
params.api.sizeColumnsToFit();
|
||||
@@ -500,7 +511,8 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
|
||||
rowSelection="multiple"
|
||||
animateRows
|
||||
onCellClicked={handleCrossFilter}
|
||||
onCellClicked={handleCellClicked}
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onStateUpdated={handleGridStateChange}
|
||||
initialState={gridInitialState}
|
||||
@@ -592,7 +604,6 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
initialSortState: getInitialSortState(
|
||||
serverPaginationData?.sortBy || [],
|
||||
),
|
||||
isActiveFilterValue,
|
||||
lastFilteredColumn: serverPaginationData?.lastFilteredColumn,
|
||||
lastFilteredInputPosition:
|
||||
serverPaginationData?.lastFilteredInputPosition,
|
||||
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
getTimeFormatterForGranularity,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
CellClickedEvent,
|
||||
IMenuActionParams,
|
||||
SelectionChangedEvent,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import {
|
||||
AgGridTableChartTransformedProps,
|
||||
@@ -40,7 +40,7 @@ import AgGridDataTable from './AgGridTable';
|
||||
import { updateTableOwnState } from './utils/externalAPIs';
|
||||
import TimeComparisonVisibility from './AgGridTable/components/TimeComparisonVisibility';
|
||||
import { useColDefs } from './utils/useColDefs';
|
||||
import { getCrossFilterDataMask } from './utils/getCrossFilterDataMask';
|
||||
import { buildSelectionCrossFilterDataMask } from './utils/getCrossFilterDataMask';
|
||||
import { StyledChartContainer } from './styles';
|
||||
import type { FilterState } from './utils/filterStateManager';
|
||||
|
||||
@@ -248,7 +248,14 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
|
||||
const isActiveFilterValue = useCallback(
|
||||
function isActiveFilterValue(key: string, val: DataRecordValue) {
|
||||
return !!filters && filters[key]?.includes(val);
|
||||
if (!filters || !filters[key]) return false;
|
||||
return filters[key].some(filterVal => {
|
||||
if (filterVal === val) return true;
|
||||
if (filterVal instanceof Date && val instanceof Date) {
|
||||
return filterVal.getTime() === val.getTime();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
[filters],
|
||||
);
|
||||
@@ -263,37 +270,68 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[timeGrain, isRawRecords],
|
||||
);
|
||||
|
||||
const toggleFilter = useCallback(
|
||||
(event: CellClickedEvent | IMenuActionParams) => {
|
||||
const activeColumnRef = useRef<string | null>(null);
|
||||
|
||||
const handleCellClicked = useCallback(
|
||||
(event: CellClickedEvent) => {
|
||||
if (!emitCrossFilters || !event.column) return;
|
||||
const colDef = event.column.getColDef();
|
||||
if (colDef.context?.isMetric || colDef.context?.isPercentMetric) return;
|
||||
|
||||
const key = event.column.getColId();
|
||||
activeColumnRef.current = key;
|
||||
|
||||
// Re-click on already-filtered single selection → untoggle
|
||||
// AG Grid doesn't change selection when re-clicking the same row,
|
||||
// so onSelectionChanged won't fire — handle clear directly here
|
||||
const selectedNodes = event.api.getSelectedNodes();
|
||||
if (
|
||||
emitCrossFilters &&
|
||||
event.column &&
|
||||
!(
|
||||
event.column.getColDef().context?.isMetric ||
|
||||
event.column.getColDef().context?.isPercentMetric
|
||||
)
|
||||
selectedNodes.length === 1 &&
|
||||
selectedNodes[0] === event.node &&
|
||||
isActiveFilterValue(key, event.value)
|
||||
) {
|
||||
const crossFilterProps = {
|
||||
key: event.column.getColId(),
|
||||
value: event.value,
|
||||
filters,
|
||||
timeGrain,
|
||||
isActiveFilterValue,
|
||||
timestampFormatter,
|
||||
};
|
||||
setDataMask(getCrossFilterDataMask(crossFilterProps).dataMask);
|
||||
event.node.setSelected(false);
|
||||
setDataMask(
|
||||
buildSelectionCrossFilterDataMask({
|
||||
key,
|
||||
values: [],
|
||||
timeGrain,
|
||||
timestampFormatter,
|
||||
}).dataMask,
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
emitCrossFilters,
|
||||
setDataMask,
|
||||
filters,
|
||||
timeGrain,
|
||||
isActiveFilterValue,
|
||||
setDataMask,
|
||||
timeGrain,
|
||||
timestampFormatter,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSelectionChanged = useCallback(
|
||||
(event: SelectionChangedEvent) => {
|
||||
if (!emitCrossFilters || !activeColumnRef.current) return;
|
||||
|
||||
const key = activeColumnRef.current;
|
||||
const selectedRows = event.api.getSelectedRows();
|
||||
const values = selectedRows
|
||||
.map(row => row[key] as DataRecordValue)
|
||||
.filter(v => v != null);
|
||||
|
||||
setDataMask(
|
||||
buildSelectionCrossFilterDataMask({
|
||||
key,
|
||||
values,
|
||||
timeGrain,
|
||||
timestampFormatter,
|
||||
}).dataMask,
|
||||
);
|
||||
},
|
||||
[emitCrossFilters, setDataMask, timeGrain, timestampFormatter],
|
||||
);
|
||||
|
||||
const handleServerPaginationChange = useCallback(
|
||||
(pageNumber: number, pageSize: number) => {
|
||||
const modifiedOwnState = {
|
||||
@@ -395,11 +433,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
onFilterChanged={handleFilterChanged}
|
||||
metricColumns={metricColumns}
|
||||
id={slice_id}
|
||||
handleCrossFilter={toggleFilter}
|
||||
handleCellClicked={handleCellClicked}
|
||||
handleSelectionChanged={handleSelectionChanged}
|
||||
filters={filters}
|
||||
percentMetrics={percentMetrics}
|
||||
serverPageLength={serverPageLength}
|
||||
hasServerPageLengthChanged={hasServerPageLengthChanged}
|
||||
isActiveFilterValue={isActiveFilterValue}
|
||||
renderTimeComparisonDropdown={
|
||||
isUsingTimeComparison ? renderTimeComparisonVisibility : () => null
|
||||
}
|
||||
|
||||
@@ -27,15 +27,11 @@ type GetCellClassParams = CellClassParams & {
|
||||
|
||||
const getCellClass = (params: GetCellClassParams) => {
|
||||
const { col, emitCrossFilters } = params;
|
||||
const isActiveFilterValue = params?.context?.isActiveFilterValue;
|
||||
let className = '';
|
||||
if (emitCrossFilters) {
|
||||
if (!col?.isMetric) {
|
||||
className += ' dt-is-filter';
|
||||
}
|
||||
if (isActiveFilterValue?.(col?.key, params?.value)) {
|
||||
className += ' dt-is-active-filter';
|
||||
}
|
||||
if (col?.config?.truncateLongCells) {
|
||||
className += ' dt-truncate-cell';
|
||||
}
|
||||
|
||||
@@ -34,6 +34,55 @@ type GetCrossFilterDataMaskProps = {
|
||||
timestampFormatter: (value: DataRecordValue) => string;
|
||||
};
|
||||
|
||||
type BuildSelectionCrossFilterProps = {
|
||||
key: string;
|
||||
values: DataRecordValue[];
|
||||
timeGrain?: TimeGranularity;
|
||||
timestampFormatter: (value: DataRecordValue) => string;
|
||||
};
|
||||
|
||||
export const buildSelectionCrossFilterDataMask = ({
|
||||
key,
|
||||
values,
|
||||
timeGrain,
|
||||
timestampFormatter,
|
||||
}: BuildSelectionCrossFilterProps) => {
|
||||
if (values.length === 0) {
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: { filters: [] },
|
||||
filterState: { label: null, value: null, filters: null },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const updatedFilters: DataRecordFilters = { [key]: values };
|
||||
const isTimestamp = key === DTTM_ALIAS;
|
||||
const label = values
|
||||
.map(v => (isTimestamp ? timestampFormatter(v) : v))
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: key,
|
||||
op: 'IN' as const,
|
||||
val: values.map(el => (el instanceof Date ? el.getTime() : el!)),
|
||||
grain: isTimestamp ? timeGrain : undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
label,
|
||||
value: [values],
|
||||
filters: updatedFilters,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getCrossFilterDataMask = ({
|
||||
key,
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user