Skip to content

Commit 2d4ab24

Browse files
fix(ListView): implement AntD pagination for ListView component
1 parent 0fce5ec commit 2d4ab24

5 files changed

Lines changed: 138 additions & 64 deletions

File tree

superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ interface TableCollectionProps<T extends object> {
4747
toggleAllRowsSelected?: (value?: boolean) => void;
4848
sticky?: boolean;
4949
size?: TableSize;
50+
usePagination?: boolean;
51+
pageIndex?: number;
52+
pageSize?: number;
53+
totalCount?: number;
54+
onPageChange?: (page: number, pageSize: number) => void;
5055
}
5156

5257
const StyledTable = styled(Table)`
@@ -56,6 +61,7 @@ const StyledTable = styled(Table)`
5661
text-overflow: ellipsis;
5762
white-space: nowrap;
5863
}
64+
5965
.actions {
6066
opacity: 0;
6167
font-size: ${theme.fontSizeXL}px;
@@ -72,15 +78,18 @@ const StyledTable = styled(Table)`
7278
}
7379
}
7480
}
81+
7582
.ant-table-column-title {
7683
line-height: initial;
7784
}
85+
7886
.ant-table-row:hover {
7987
.actions {
8088
opacity: 1;
8189
transition: opacity ease-in ${theme.motionDurationMid};
8290
}
8391
}
92+
8493
.ant-table-cell {
8594
max-width: 320px;
8695
font-feature-settings: 'tnum' 1;
@@ -91,10 +100,25 @@ const StyledTable = styled(Table)`
91100
padding-left: ${theme.sizeUnit * 4}px;
92101
white-space: nowrap;
93102
}
103+
94104
.ant-table-placeholder .ant-table-cell {
95105
border-bottom: 0;
96106
}
97107
108+
&.ant-table-wrapper .ant-table-pagination.ant-pagination {
109+
display: flex;
110+
justify-content: center;
111+
margin: ${theme.sizeUnit * 4}px 0 ${theme.sizeUnit * 14}px 0;
112+
position: relative;
113+
114+
.ant-pagination-total-text {
115+
color: ${theme.colorTextBase};
116+
margin-inline-end: 0;
117+
position: absolute;
118+
top: ${theme.sizeUnit * 12}px;
119+
}
120+
}
121+
98122
// Hotfix - antd doesn't apply background color to overflowing cells
99123
& table {
100124
background-color: ${theme.colorBgContainer};
@@ -116,6 +140,11 @@ function TableCollection<T extends object>({
116140
prepareRow,
117141
sticky,
118142
size = TableSize.Middle,
143+
usePagination = false,
144+
pageIndex = 0,
145+
pageSize = 25,
146+
totalCount = 0,
147+
onPageChange,
119148
}: TableCollectionProps<T>) {
120149
const mappedColumns = mapColumns<T>(
121150
columns,
@@ -147,6 +176,27 @@ function TableCollection<T extends object>({
147176
toggleRowSelected,
148177
toggleAllRowsSelected,
149178
]);
179+
180+
const paginationConfig = useMemo(() => {
181+
if (!usePagination) return false;
182+
183+
return {
184+
current: pageIndex + 1,
185+
pageSize,
186+
total: totalCount,
187+
size: 'default' as const,
188+
showSizeChanger: false,
189+
showQuickJumper: false,
190+
align: 'center' as const,
191+
showTotal: (total: number, range: [number, number]) =>
192+
`${range[0]}-${range[1]} of ${total}`,
193+
onChange: (page: number, size: number) => {
194+
const validPage = Math.max(0, (page || 1) - 1);
195+
const validSize = size || pageSize;
196+
onPageChange?.(validPage, validSize);
197+
},
198+
};
199+
}, [usePagination, pageIndex, pageSize, totalCount, onPageChange]);
150200
return (
151201
<StyledTable
152202
loading={loading}
@@ -155,7 +205,7 @@ function TableCollection<T extends object>({
155205
data={mappedRows}
156206
size={size}
157207
data-test="listview-table"
158-
pagination={false}
208+
pagination={paginationConfig}
159209
tableLayout="auto"
160210
rowKey="rowId"
161211
rowSelection={rowSelection}

superset-frontend/src/components/ListView/ListView.test.jsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { render, screen, within } from 'spec/helpers/testing-library';
19+
import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
2020
import userEvent from '@testing-library/user-event';
2121
import { QueryParamProvider } from 'use-query-params';
2222
import thunk from 'redux-thunk';
@@ -172,23 +172,28 @@ describe('ListView', () => {
172172
});
173173
});
174174

175-
// Update pagination control tests to use button role
175+
// Update pagination control tests for Ant Design pagination
176176
it('renders pagination controls', () => {
177-
expect(screen.getByRole('navigation')).toBeInTheDocument();
178-
expect(screen.getByRole('button', { name: '«' })).toBeInTheDocument();
179-
expect(screen.getByRole('button', { name: '»' })).toBeInTheDocument();
177+
const paginationList = screen.getByRole('list');
178+
expect(paginationList).toBeInTheDocument();
179+
180+
const pageOneItem = screen.getByRole('listitem', { name: '1' });
181+
expect(pageOneItem).toBeInTheDocument();
180182
});
181183

182184
it('calls fetchData on page change', async () => {
183-
const nextButton = screen.getByRole('button', { name: '»' });
184-
await userEvent.click(nextButton);
185+
const pageTwoItem = screen.getByRole('listitem', { name: '2' });
186+
await userEvent.click(pageTwoItem);
185187

186-
// Remove sortBy expectation since it's not part of the initial state
187-
expect(mockedProps.fetchData).toHaveBeenCalledWith({
188-
filters: [],
189-
pageIndex: 1,
190-
pageSize: 1,
191-
sortBy: [],
188+
await waitFor(() => {
189+
const { calls } = mockedProps.fetchData.mock;
190+
const pageChangeCall = calls.find(
191+
call =>
192+
call[0].pageIndex === 1 &&
193+
call[0].filters.length === 0 &&
194+
call[0].pageSize === 1,
195+
);
196+
expect(pageChangeCall).toBeDefined();
192197
});
193198
});
194199

superset-frontend/src/components/ListView/ListView.tsx

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import { t, styled } from '@superset-ui/core';
2020
import { useCallback, useEffect, useRef, useState, ReactNode } from 'react';
2121
import cx from 'classnames';
22-
import Pagination from '@superset-ui/core/components/Pagination';
2322
import TableCollection from '@superset-ui/core/components/TableCollection';
2423
import BulkTagModal from 'src/features/tags/BulkTagModal';
2524
import {
@@ -71,6 +70,7 @@ const ListViewStyles = styled.div`
7170
7271
.body {
7372
overflow-x: auto;
73+
overflow-y: hidden;
7474
}
7575
7676
.ant-empty {
@@ -462,6 +462,7 @@ export function ListView<T extends object = any>({
462462
</FullPageLoadingWrapper>
463463
) : (
464464
<TableCollection
465+
usePagination
465466
getTableProps={getTableProps}
466467
getTableBodyProps={getTableBodyProps}
467468
prepareRow={prepareRow}
@@ -482,6 +483,12 @@ export function ListView<T extends object = any>({
482483
}
483484
}}
484485
toggleAllRowsSelected={toggleAllRowsSelected}
486+
pageIndex={pageIndex}
487+
pageSize={pageSize}
488+
totalCount={count}
489+
onPageChange={newPageIndex => {
490+
gotoPage(newPageIndex);
491+
}}
485492
/>
486493
)}
487494
</>
@@ -509,25 +516,6 @@ export function ListView<T extends object = any>({
509516
)}
510517
</div>
511518
</div>
512-
{rows.length > 0 && (
513-
<div className="pagination-container">
514-
<Pagination
515-
totalPages={pageCount || 0}
516-
currentPage={pageCount && pageIndex < pageCount ? pageIndex + 1 : 0}
517-
onChange={(p: number) => gotoPage(p - 1)}
518-
hideFirstAndLastPageLinks
519-
/>
520-
<div className="row-count-container">
521-
{!loading &&
522-
t(
523-
'%s-%s of %s',
524-
pageSize * pageIndex + (rows.length && 1),
525-
pageSize * pageIndex + rows.length,
526-
count,
527-
)}
528-
</div>
529-
</div>
530-
)}
531519
</ListViewStyles>
532520
);
533521
}

superset-frontend/src/components/ListView/utils.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,22 @@ import {
4848
// Define custom RisonParam for proper encoding/decoding; note that
4949
// %, &, +, and # must be encoded to avoid breaking the url
5050
const RisonParam: QueryParamConfig<string, any> = {
51-
encode: (data?: any | null) =>
52-
data === undefined
53-
? undefined
54-
: rison
55-
.encode(data)
56-
.replace(/%/g, '%25')
57-
.replace(/&/g, '%26')
58-
.replace(/\+/g, '%2B')
59-
.replace(/#/g, '%23'),
51+
encode: (data?: any | null) => {
52+
if (data === undefined || data === null) return undefined;
53+
54+
const cleanData = JSON.parse(
55+
JSON.stringify(data, (key, value) =>
56+
value === undefined ? null : value,
57+
),
58+
);
59+
60+
return rison
61+
.encode(cleanData)
62+
.replace(/%/g, '%25')
63+
.replace(/&/g, '%26')
64+
.replace(/\+/g, '%2B')
65+
.replace(/#/g, '%23');
66+
},
6067
decode: (dataStr?: string | string[]) =>
6168
dataStr === undefined || Array.isArray(dataStr)
6269
? undefined
@@ -319,7 +326,7 @@ export function useListViewState({
319326
filters: Object.keys(filterObj).length ? filterObj : undefined,
320327
pageIndex,
321328
};
322-
if (sortBy[0]) {
329+
if (sortBy?.[0]?.id !== undefined && sortBy[0].id !== null) {
323330
queryParams.sortColumn = sortBy[0].id;
324331
queryParams.sortOrder = sortBy[0].desc ? 'desc' : 'asc';
325332
}

0 commit comments

Comments
 (0)