Skip to content

Commit

Permalink
chore: add server side pagination with totalItemCount & `pagination…
Browse files Browse the repository at this point in the history
…Type` (#2001)
  • Loading branch information
chaitanyadeorukhkar authored Feb 2, 2024
1 parent 03a37be commit b21e3ae
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-crabs-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@razorpay/blade": minor
---

chore: add server-side pagination with `totalItemCount` & `paginationType`
25 changes: 19 additions & 6 deletions packages/blade/src/components/Table/Table.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
tableBackgroundColor,
tablePagination,
} from './tokens';
import type { TableProps, TableNode, Identifier } from './types';
import type { TableProps, TableNode, Identifier, TablePaginationType } from './types';
import { makeBorderSize, makeMotionTime } from '~utils';
import { getComponentId, isValidAllowedChildren } from '~utils/isValidAllowedChildren';
import { throwBladeError } from '~utils/logger';
Expand Down Expand Up @@ -131,6 +131,9 @@ const _Table = <Item,>({
const [selectedRows, setSelectedRows] = React.useState<TableNode<unknown>['id'][]>([]);
const [disabledRows, setDisabledRows] = React.useState<TableNode<unknown>['id'][]>([]);
const [totalItems, setTotalItems] = React.useState(data.nodes.length || 0);
const [paginationType, setPaginationType] = React.useState<NonNullable<TablePaginationType>>(
'client',
);
// Need to make header is sticky if first column is sticky otherwise the first header cell will not be sticky
const shouldHeaderBeSticky = isHeaderSticky ?? isFirstColumnSticky;
const backgroundColor = tableBackgroundColor;
Expand Down Expand Up @@ -318,12 +321,18 @@ const _Table = <Item,>({

const hasPagination = Boolean(pagination);

const paginationConfig = usePagination(data, {
state: {
page: 0,
size: tablePagination.defaultPageSize,
const paginationConfig = usePagination(
data,
{
state: {
page: 0,
size: tablePagination.defaultPageSize,
},
},
});
{
isServer: paginationType === 'server',
},
);

const currentPaginationState = useMemo(() => {
return hasPagination
Expand Down Expand Up @@ -376,6 +385,8 @@ const _Table = <Item,>({
showStripedRows,
disabledRows,
setDisabledRows,
paginationType,
setPaginationType,
backgroundColor,
}),
[
Expand All @@ -394,6 +405,8 @@ const _Table = <Item,>({
showStripedRows,
disabledRows,
setDisabledRows,
paginationType,
setPaginationType,
backgroundColor,
],
);
Expand Down
6 changes: 5 additions & 1 deletion packages/blade/src/components/Table/TableContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// eslint-disable-next-line @typescript-eslint/no-empty-function
import React from 'react';
import type { TableNode } from '@table-library/react-table-library/table';
import type { TableBackgroundColors, TableProps } from './types';
import type { TableBackgroundColors, TableProps, TablePaginationType } from './types';

export type TableContextType = {
selectionType?: TableProps<unknown>['selectionType'];
Expand All @@ -27,6 +27,8 @@ export type TableContextType = {
showStripedRows?: boolean;
disabledRows: TableNode['id'][];
setDisabledRows: React.Dispatch<React.SetStateAction<TableNode['id'][]>>;
paginationType: NonNullable<TablePaginationType>;
setPaginationType: React.Dispatch<React.SetStateAction<NonNullable<TablePaginationType>>>;
backgroundColor: TableBackgroundColors;
};

Expand All @@ -47,6 +49,8 @@ const TableContext = React.createContext<TableContextType>({
setPaginationRowSize: () => {},
disabledRows: [],
setDisabledRows: () => {},
paginationType: 'client',
setPaginationType: () => {},
backgroundColor: 'surface.background.gray.intense',
});

Expand Down
23 changes: 20 additions & 3 deletions packages/blade/src/components/Table/TablePagination.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from 'styled-components';
import { useTableContext } from './TableContext';
import { ComponentIds } from './componentIds';
import { tablePagination } from './tokens';
import type { TablePaginationProps } from './types';
import type { TablePaginationCommonProps, TablePaginationProps } from './types';
import isUndefined from '~utils/lodashButBetter/isUndefined';
import getIn from '~utils/lodashButBetter/get';
import BaseBox from '~components/Box/BaseBox';
Expand All @@ -23,9 +23,10 @@ import { Button } from '~components/Button';
import { makeAccessible } from '~utils/makeAccessible';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { useTheme } from '~components/BladeProvider';
import { throwBladeError } from '~utils/logger';
import { getFocusRingStyles } from '~utils/getFocusRingStyles';

const pageSizeOptions: NonNullable<TablePaginationProps['defaultPageSize']>[] = [10, 25, 50];
const pageSizeOptions: NonNullable<TablePaginationCommonProps['defaultPageSize']>[] = [10, 25, 50];

const PageSelectionButton = styled.button<{ isSelected?: boolean }>(({ theme, isSelected }) => ({
backgroundColor: isSelected
Expand Down Expand Up @@ -145,12 +146,15 @@ const _TablePagination = ({
showPageNumberSelector = false,
showLabel,
label,
totalItemCount,
paginationType = 'client',
}: TablePaginationProps): React.ReactElement => {
const {
setPaginationPage,
currentPaginationState,
totalItems,
setPaginationRowSize,
setPaginationType,
} = useTableContext();
const [currentPageSize, setCurrentPageSize] = React.useState<number>(defaultPageSize);
const [currentPage, setCurrentPage] = React.useState<number>(
Expand All @@ -170,6 +174,7 @@ const _TablePagination = ({
const onMobile = platform === 'onMobile';
useEffect(() => {
setPaginationRowSize(currentPageSize);
setPaginationType(paginationType);
}, []);

useEffect(() => {
Expand All @@ -178,7 +183,9 @@ const _TablePagination = ({
}
}, [currentPage, currentPaginationState?.page, setPaginationPage]);

const totalPages = Math.ceil(totalItems / currentPageSize);
const totalPages = isUndefined(totalItemCount)
? Math.ceil(totalItems / currentPageSize)
: Math.ceil(totalItemCount / currentPageSize);

const handlePageChange = useCallback(
(page: number): void => {
Expand Down Expand Up @@ -206,6 +213,16 @@ const _TablePagination = ({
}
}, [controlledCurrentPage, currentPage, handlePageChange, onPageChange]);

if (__DEV__) {
if (paginationType === 'server' && (isUndefined(totalItemCount) || isUndefined(onPageChange))) {
throwBladeError({
message:
'`onPageChange` and `totalItemCount` props are required when paginationType is server.',
moduleName: 'TablePagination',
});
}
}

const handlePageSizeChange = (pageSize: number): void => {
onPageSizeChange?.({ pageSize });
setPaginationRowSize(pageSize);
Expand Down
123 changes: 122 additions & 1 deletion packages/blade/src/components/Table/__tests__/Table.web.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { Table } from '../Table';
import { TableBody, TableCell, TableRow } from '../TableBody';
Expand Down Expand Up @@ -752,7 +753,7 @@ describe('<Table />', () => {
expect(onSelectionChange).toHaveBeenCalledWith({ values: [] });
});

it('should render table with pagination', async () => {
it('should render table with client side pagination', async () => {
const onPageChange = jest.fn();
const onPageSizeChange = jest.fn();
const user = userEvent.setup();
Expand Down Expand Up @@ -840,4 +841,124 @@ describe('<Table />', () => {
fireEvent.click(goBack5PagesButton);
expect(onPageChange).toHaveBeenLastCalledWith({ page: 0 });
}, 10000);

it('should render table with server side pagination', () => {
const ServerPaginatedTable = (): React.ReactElement => {
const [apiData, setAPIData] = useState({ nodes: nodes.slice(0, 10) });
const onPageChange = ({ page }: { page: number }): void => {
setAPIData({ nodes: nodes.slice(page * 10, page * 10 + 10) });
};

return (
<Table
data={apiData}
pagination={
<TablePagination
onPageChange={onPageChange}
paginationType="server"
totalItemCount={nodes.length}
showPageSizePicker
showPageNumberSelector
/>
}
>
{(tableData) => (
<>
<TableHeader>
<TableHeaderRow>
<TableHeaderCell>Payment ID</TableHeaderCell>
<TableHeaderCell>Amount</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Method</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
</TableHeaderRow>
</TableHeader>
<TableBody>
{tableData.map((tableItem, index) => (
<TableRow item={tableItem} key={index}>
<TableCell>{tableItem.paymentId}</TableCell>
<TableCell>{tableItem.amount}</TableCell>
<TableCell>{tableItem.status}</TableCell>
<TableCell>{tableItem.type}</TableCell>
<TableCell>{tableItem.method}</TableCell>
<TableCell>{tableItem.name}</TableCell>
</TableRow>
))}
</TableBody>
</>
)}
</Table>
);
};
const { getByLabelText, queryByText } = renderWithTheme(<ServerPaginatedTable />);
const nextPageButton = getByLabelText('Next Page');
const previousPageButton = getByLabelText('Previous Page');
// Check if pagination buttons work
expect(nextPageButton).toBeInTheDocument();
expect(previousPageButton).toBeInTheDocument();
expect(queryByText('rzp01')).toBeInTheDocument();
// Go to next page
fireEvent.click(nextPageButton);
expect(queryByText('rzp01')).not.toBeInTheDocument();
expect(queryByText('rzp11')).toBeInTheDocument();
// Go to previous page
fireEvent.click(previousPageButton);
expect(queryByText('rzp01')).toBeInTheDocument();
});

beforeAll(() => jest.spyOn(console, 'error').mockImplementation());
afterAll(() => jest.restoreAllMocks());

it('should throw error for missing props in server side pagination', () => {
const ServerPaginatedTable = (): React.ReactElement => {
const [apiData] = useState({ nodes: nodes.slice(0, 10) });

return (
<Table
data={apiData}
pagination={
// @ts-expect-error onPageChange and totalItemCount are missing intentionally
<TablePagination paginationType="server" showPageSizePicker showPageNumberSelector />
}
>
{(tableData) => (
<>
<TableHeader>
<TableHeaderRow>
<TableHeaderCell>Payment ID</TableHeaderCell>
<TableHeaderCell>Amount</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Method</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
</TableHeaderRow>
</TableHeader>
<TableBody>
{tableData.map((tableItem, index) => (
<TableRow item={tableItem} key={index}>
<TableCell>{tableItem.paymentId}</TableCell>
<TableCell>{tableItem.amount}</TableCell>
<TableCell>{tableItem.status}</TableCell>
<TableCell>{tableItem.type}</TableCell>
<TableCell>{tableItem.method}</TableCell>
<TableCell>{tableItem.name}</TableCell>
</TableRow>
))}
</TableBody>
</>
)}
</Table>
);
};
try {
renderWithTheme(<ServerPaginatedTable />);
} catch (error: unknown) {
if (error instanceof Error) {
expect(error.message).toEqual(
'[Blade: TablePagination]: `onPageChange` and `totalItemCount` props are required when paginationType is server.',
);
}
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
MultiSelectableWithZebraStripesStory,
TableWithStickyHeaderAndFooterStory,
TableWithStickyFirstColumnStory,
TableWithPaginationStory,
TableWithDisabledRowsStory,
TableWithBackgroundColorStory,
TableWithIsLoadingStory,
Expand Down Expand Up @@ -101,14 +100,6 @@ export const MultiSelectableWithZebraStripes = (): React.ReactElement => {
);
};

export const TableWithPagination = (): React.ReactElement => {
return (
<Sandbox padding="spacing.0" editorHeight="90vh">
{TableWithPaginationStory}
</Sandbox>
);
};

export const TableWithDisabledRows = (): React.ReactElement => {
return (
<Sandbox padding="spacing.0" editorHeight="90vh">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import type { Meta } from '@storybook/react';
import { Table } from '../Table';
import { TableWithClientSidePaginationStory, TableWithServerSidePaginationStory } from './stories';
import { Sandbox } from '~utils/storybook/Sandbox';

const TableMeta: Meta = {
title: 'Components/Table/Examples/Pagination',
component: Table,
parameters: {
viewMode: 'story',
options: {
showPanel: false,
},
previewTabs: {
'storybook/docs/panel': {
hidden: true,
},
},
chromatic: { disableSnapshot: true },
},
};

export const TableWithClientSidePagination = (): React.ReactElement => {
return (
<Sandbox padding="spacing.0" editorHeight="90vh">
{TableWithClientSidePaginationStory}
</Sandbox>
);
};

export const TableWithServerSidePagination = (): React.ReactElement => {
return (
<Sandbox padding="spacing.0" editorHeight="90vh">
{TableWithServerSidePaginationStory}
</Sandbox>
);
};

export default TableMeta;
Loading

0 comments on commit b21e3ae

Please sign in to comment.