fix(table chart): fix rerender bug that continuously cleared search box (#39707)

(cherry picked from commit 3395620b6e)
This commit is contained in:
Sam Firke
2026-04-28 07:40:56 -04:00
committed by Michael S. Molina
parent 61bcf578a6
commit 87e5450cbe
3 changed files with 248 additions and 12 deletions

View File

@@ -147,7 +147,25 @@ export default typedMemo(function DataTable<D extends object>({
hooks || [],
].flat();
const columnNames = Object.keys(data?.[0] || {});
const columnNames = columns.map((column, index) => {
const normalizedColumn = column as typeof column & {
accessor?: string | ((row: D) => unknown);
columnKey?: string;
id?: string;
};
const accessorName =
typeof normalizedColumn.accessor === 'string'
? normalizedColumn.accessor
: undefined;
return (
normalizedColumn.columnKey ??
normalizedColumn.id ??
accessorName ??
String(index)
);
});
const previousColumnNames = usePrevious(columnNames);
const resultsSize = serverPagination ? rowCount : data.length;
const sortByRef = useRef([]); // cache initial `sortby` so sorting doesn't trigger page reset
@@ -237,6 +255,7 @@ export default typedMemo(function DataTable<D extends object>({
getTableSize: defaultGetTableSize,
globalFilter: defaultGlobalFilter,
sortTypes,
autoResetGlobalFilter: !isEqual(columnNames, previousColumnNames),
autoResetSortBy: !isEqual(columnNames, previousColumnNames),
manualSortBy: !!serverPagination,
...moreUseTableOptions,

View File

@@ -36,6 +36,8 @@ import {
SMART_DATE_ID,
getTimeFormatterForGranularity,
} from '@superset-ui/core';
import { CellProps, Column, HeaderProps } from 'react-table';
import DataTable from '../src/DataTable/DataTable';
import TableChart, { sanitizeHeaderId } from '../src/TableChart';
import { GenericDataType } from '@apache-superset/core/common';
import transformProps from '../src/transformProps';
@@ -1980,6 +1982,222 @@ describe('plugin-chart-table', () => {
expect(totalCellAfter).toBeInTheDocument();
});
});
test('preserves client-side search text across temporal table rerenders', async () => {
const formDataWithSearch = {
...testData.basic.formData,
include_search: true,
server_pagination: false,
};
const renderChart = () => {
const props = transformProps({
...testData.basic,
formData: formDataWithSearch,
});
props.includeSearch = true;
return (
<ProviderWrapper>
<TableChart {...props} sticky={false} />
</ProviderWrapper>
);
};
const { rerender } = render(renderChart());
const searchInput = screen.getByRole('textbox');
fireEvent.change(searchInput, { target: { value: 'Michael' } });
await waitFor(() => {
expect(searchInput).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
rerender(renderChart());
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
});
test('preserves client-side search text when rerendered with empty data', async () => {
const formDataWithSearch = {
...testData.basic.formData,
include_search: true,
server_pagination: false,
};
const renderChart = (data = testData.basic.queriesData[0].data) => {
const props = transformProps({
...testData.basic,
formData: formDataWithSearch,
queriesData: [
{
...testData.basic.queriesData[0],
data,
},
],
});
props.includeSearch = true;
return (
<ProviderWrapper>
<TableChart {...props} sticky={false} />
</ProviderWrapper>
);
};
const { rerender } = render(renderChart());
const searchInput = screen.getByRole('textbox');
fireEvent.change(searchInput, { target: { value: 'Michael' } });
await waitFor(() => {
expect(searchInput).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
rerender(renderChart([]));
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('Michael');
expect(screen.getByLabelText('Search 0 records')).toHaveValue(
'Michael',
);
});
});
test('preserves client-side search text for function accessor columns', async () => {
type DataRow = {
city: string;
firstName: string;
};
const makeColumns = (): Column<DataRow>[] => [
{
Header: ({ column }: HeaderProps<DataRow>) => (
<th data-column-name={column.id}>First name</th>
),
Cell: ({ value }: CellProps<DataRow>) => <td>{value}</td>,
id: 'firstName',
accessor: ((row: DataRow) => row.firstName) as never,
},
{
Header: ({ column }: HeaderProps<DataRow>) => (
<th data-column-name={column.id}>City</th>
),
Cell: ({ value }: CellProps<DataRow>) => <td>{value}</td>,
id: 'city',
accessor: ((row: DataRow) => row.city) as never,
},
];
const data: DataRow[] = [
{ firstName: 'Michael', city: 'Paris' },
{ firstName: 'Jordan', city: 'London' },
];
const renderDataTable = () => (
<ProviderWrapper>
<DataTable<DataRow>
columns={makeColumns()}
data={data}
rowCount={data.length}
serverPagination={false}
serverPaginationData={{}}
onServerPaginationChange={jest.fn()}
handleSortByChange={jest.fn()}
sortByFromParent={[]}
onSearchColChange={jest.fn()}
searchOptions={[]}
sticky={false}
/>
</ProviderWrapper>
);
const { rerender } = render(renderDataTable());
const searchInput = screen.getByRole('textbox');
fireEvent.change(searchInput, { target: { value: 'Michael' } });
await waitFor(() => {
expect(searchInput).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
rerender(renderDataTable());
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
});
test('preserves client-side search text for string accessor columns without ids', async () => {
type DataRow = {
city: string;
firstName: string;
};
const makeColumns = (): Column<DataRow>[] => [
{
Header: ({ column }: HeaderProps<DataRow>) => (
<th data-column-name={column.id}>First name</th>
),
Cell: ({ value }: CellProps<DataRow>) => <td>{value}</td>,
accessor: 'firstName',
},
{
Header: ({ column }: HeaderProps<DataRow>) => (
<th data-column-name={column.id}>City</th>
),
Cell: ({ value }: CellProps<DataRow>) => <td>{value}</td>,
accessor: 'city',
},
];
const data: DataRow[] = [
{ firstName: 'Michael', city: 'Paris' },
{ firstName: 'Jordan', city: 'London' },
];
const renderDataTable = () => (
<ProviderWrapper>
<DataTable<DataRow>
columns={makeColumns()}
data={data}
rowCount={data.length}
serverPagination={false}
serverPaginationData={{}}
onServerPaginationChange={jest.fn()}
handleSortByChange={jest.fn()}
sortByFromParent={[]}
onSearchColChange={jest.fn()}
searchOptions={[]}
sticky={false}
/>
</ProviderWrapper>
);
const { rerender } = render(renderDataTable());
const searchInput = screen.getByRole('textbox');
fireEvent.change(searchInput, { target: { value: 'Michael' } });
await waitFor(() => {
expect(searchInput).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
rerender(renderDataTable());
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('Michael');
expect(screen.getByText('Michael')).toBeInTheDocument();
});
});
});
test('should build columnLabelToNameMap for adhoc columns with custom labels', () => {