-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feat/paginated-table-components' into alpha
- Loading branch information
Showing
12 changed files
with
509 additions
and
3 deletions.
There are no files selected for viewing
191 changes: 191 additions & 0 deletions
191
src/components/ui/PaginationHeader/PaginationHeader.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import React from "react"; | ||
import { ComponentStory, ComponentMeta } from "@storybook/react"; | ||
import { PaginationHeader } from "./PaginationHeader"; | ||
|
||
export default { | ||
title: "Components/UI/PaginationHeader", | ||
component: PaginationHeader, | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: | ||
"A pagination component that combines Ant Design Pagination with selection summary and controls.", | ||
}, | ||
}, | ||
}, | ||
} as ComponentMeta<typeof PaginationHeader>; | ||
|
||
const Template: ComponentStory<typeof PaginationHeader> = (args) => { | ||
return <PaginationHeader {...args} />; | ||
}; | ||
|
||
// Common handlers for all stories | ||
const commonHandlers = { | ||
onRequestPageChange: (page: number, pageSize?: number) => { | ||
console.log(`Page changed to ${page} with size ${pageSize}`); | ||
}, | ||
onPageSizeChange: (pageSize: number) => { | ||
console.log(`Page size changed to ${pageSize}`); | ||
}, | ||
onSelectAllGlobalRecords: async () => { | ||
console.log("Select all records clicked"); | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
}, | ||
}; | ||
|
||
export const NoResults = Template.bind({}); | ||
NoResults.args = { | ||
initialPage: 1, | ||
initialPageSize: 10, | ||
total: 0, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
...commonHandlers, | ||
}; | ||
NoResults.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows the pagination state when there are no results.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const SinglePage = Template.bind({}); | ||
SinglePage.args = { | ||
initialPage: 1, | ||
initialPageSize: 10, | ||
total: 5, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
...commonHandlers, | ||
}; | ||
SinglePage.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows the pagination when all items fit in a single page.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const MultiplePages = Template.bind({}); | ||
MultiplePages.args = { | ||
initialPage: 2, | ||
initialPageSize: 10, | ||
total: 25, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
...commonHandlers, | ||
}; | ||
MultiplePages.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows pagination with multiple pages, currently on page 2.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const LargeDataset = Template.bind({}); | ||
LargeDataset.args = { | ||
initialPage: 5, | ||
initialPageSize: 20, | ||
total: 1000, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
...commonHandlers, | ||
}; | ||
LargeDataset.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows pagination with a large dataset and larger page size.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const WithPartialPageSelected = Template.bind({}); | ||
WithPartialPageSelected.args = { | ||
initialPage: 1, | ||
initialPageSize: 10, | ||
total: 100, | ||
currentPageSelectedCount: 10, | ||
totalSelectedCount: 10, | ||
...commonHandlers, | ||
}; | ||
WithPartialPageSelected.parameters = { | ||
docs: { | ||
description: { | ||
story: | ||
"Shows when current page is fully selected but there are more records available to select globally.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const WithAllPagesSelected = Template.bind({}); | ||
WithAllPagesSelected.args = { | ||
initialPage: 1, | ||
initialPageSize: 10, | ||
total: 100, | ||
currentPageSelectedCount: 10, | ||
totalSelectedCount: 100, | ||
...commonHandlers, | ||
}; | ||
WithAllPagesSelected.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows when all records across all pages are selected.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const CustomPageSize = Template.bind({}); | ||
CustomPageSize.args = { | ||
initialPage: 1, | ||
initialPageSize: 50, | ||
total: 200, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
...commonHandlers, | ||
}; | ||
CustomPageSize.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows pagination with a custom page size of 50 items.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const LastPage = Template.bind({}); | ||
LastPage.args = { | ||
initialPage: 10, | ||
initialPageSize: 10, | ||
total: 100, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
...commonHandlers, | ||
}; | ||
LastPage.parameters = { | ||
docs: { | ||
description: { | ||
story: "Shows pagination when viewing the last page of results.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const WithoutSelectionControls = Template.bind({}); | ||
WithoutSelectionControls.args = { | ||
initialPage: 1, | ||
initialPageSize: 10, | ||
total: 100, | ||
currentPageSelectedCount: 0, | ||
totalSelectedCount: 0, | ||
onRequestPageChange: commonHandlers.onRequestPageChange, | ||
onPageSizeChange: commonHandlers.onPageSizeChange, | ||
// Omitting onSelectAllGlobalRecords to hide selection controls | ||
}; | ||
WithoutSelectionControls.parameters = { | ||
docs: { | ||
description: { | ||
story: | ||
"Shows pagination without the selection controls by omitting the onSelectAllGlobalRecords handler.", | ||
}, | ||
}, | ||
}; |
116 changes: 116 additions & 0 deletions
116
src/components/ui/PaginationHeader/PaginationHeader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { useLocale } from "@/context"; | ||
import { Col, Pagination, Row } from "antd"; | ||
import type { PaginationProps } from "antd"; | ||
import { useMemo, useState, useCallback, memo } from "react"; | ||
import { SelectAllRecordsRow } from "../SelectAllRecordsRow/SelectAllRecordsRow"; | ||
import type { PaginationHeaderProps } from "./PaginationHeader.types"; | ||
import type { SelectAllRecordsRowProps } from "../SelectAllRecordsRow/SelectAllRecordsRow.types"; | ||
|
||
const PaginationHeaderComponent = (props: PaginationHeaderProps) => { | ||
const { | ||
total, | ||
initialPage, | ||
initialPageSize, | ||
currentPageSelectedCount, | ||
totalSelectedCount, | ||
onRequestPageChange, | ||
onPageSizeChange, | ||
onSelectAllGlobalRecords, | ||
} = props; | ||
|
||
const { t } = useLocale(); | ||
|
||
const [page, setPage] = useState(initialPage); | ||
const [pageSize, setPageSize] = useState(initialPageSize); | ||
|
||
const from = useMemo(() => (page - 1) * pageSize + 1, [page, pageSize]); | ||
const to = useMemo( | ||
() => Math.min(page * pageSize, total), | ||
[page, pageSize, total], | ||
); | ||
|
||
const handlePageChange = useCallback( | ||
(newPage: number, newPageSize?: number) => { | ||
setPage(newPage); | ||
if (newPageSize !== undefined) { | ||
setPageSize(newPageSize); | ||
} | ||
onRequestPageChange(newPage, newPageSize); | ||
}, | ||
[onRequestPageChange], | ||
); | ||
|
||
const handlePageSizeChange = useCallback( | ||
(newPageSize: number, newPage: number) => { | ||
setPageSize(newPageSize); | ||
setPage(newPage); | ||
onPageSizeChange(newPageSize); | ||
onRequestPageChange(newPage, newPageSize); | ||
}, | ||
[onPageSizeChange, onRequestPageChange], | ||
); | ||
|
||
const summary = useMemo(() => { | ||
if (total === undefined) return null; | ||
if (total === 0) return t("no_results"); | ||
|
||
return t("summary") | ||
.replace("{from}", from?.toString()) | ||
.replace("{to}", to?.toString()) | ||
.replace("{total}", total?.toString()); | ||
}, [total, from, to, t]); | ||
|
||
const paginationProps: PaginationProps = useMemo( | ||
() => ({ | ||
total, | ||
pageSize, | ||
current: page, | ||
onChange: handlePageChange, | ||
showSizeChanger: true, | ||
onShowSizeChange: handlePageSizeChange, | ||
showLessItems: true, | ||
locale: { | ||
items_per_page: t("items_per_page"), | ||
}, | ||
}), | ||
[total, pageSize, page, handlePageChange, handlePageSizeChange, t], | ||
); | ||
|
||
const selectAllRecordsProps: SelectAllRecordsRowProps = useMemo( | ||
() => ({ | ||
currentPageSelectedCount, | ||
currentPageSize: pageSize, | ||
totalRecordsCount: total, | ||
totalSelectedCount, | ||
onSelectAllRecords: onSelectAllGlobalRecords!, | ||
}), | ||
[ | ||
currentPageSelectedCount, | ||
pageSize, | ||
total, | ||
totalSelectedCount, | ||
onSelectAllGlobalRecords, | ||
], | ||
); | ||
|
||
const hasSelectionFeature = onSelectAllGlobalRecords !== undefined; | ||
const columnSpan = hasSelectionFeature ? 8 : 12; | ||
|
||
return ( | ||
<Row align="bottom" className="pb-4" wrap={false}> | ||
<Col span={columnSpan}> | ||
<Pagination {...paginationProps} /> | ||
</Col> | ||
{hasSelectionFeature && ( | ||
<Col span={8} className="text-center"> | ||
<SelectAllRecordsRow {...selectAllRecordsProps} /> | ||
</Col> | ||
)} | ||
<Col span={columnSpan} className="text-right"> | ||
{summary} | ||
</Col> | ||
</Row> | ||
); | ||
}; | ||
|
||
export const PaginationHeader = memo(PaginationHeaderComponent); |
18 changes: 18 additions & 0 deletions
18
src/components/ui/PaginationHeader/PaginationHeader.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export type PaginationHeaderProps = { | ||
/** Total number of records across all pages */ | ||
total: number; | ||
/** Initial page number */ | ||
initialPage: number; | ||
/** Initial number of items per page */ | ||
initialPageSize: number; | ||
/** Number of selected records in the current page */ | ||
currentPageSelectedCount: number; | ||
/** Total number of selected records across all pages */ | ||
totalSelectedCount: number; | ||
/** Callback when page or page size changes */ | ||
onRequestPageChange: (page: number, pageSize?: number) => void; | ||
/** Callback when page size changes */ | ||
onPageSizeChange: (pageSize: number) => void; | ||
/** Optional callback to select all records across all pages */ | ||
onSelectAllGlobalRecords?: () => Promise<void>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { PaginationHeader } from "./PaginationHeader"; | ||
export type { PaginationHeaderProps } from "./PaginationHeader.types"; |
60 changes: 60 additions & 0 deletions
60
src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import React from "react"; | ||
import { ComponentStory, ComponentMeta } from "@storybook/react"; | ||
import { SelectAllRecordsRow } from "./SelectAllRecordsRow"; | ||
|
||
export default { | ||
title: "Components/UI/SelectAllRecordsRow", | ||
component: SelectAllRecordsRow, | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: | ||
"A component that appears when all records in the current page are selected, offering the option to select all records across all pages.", | ||
}, | ||
}, | ||
}, | ||
} as ComponentMeta<typeof SelectAllRecordsRow>; | ||
|
||
const Template: ComponentStory<typeof SelectAllRecordsRow> = (args) => { | ||
return <SelectAllRecordsRow {...args} />; | ||
}; | ||
|
||
export const CurrentPageSelected = Template.bind({}); | ||
CurrentPageSelected.args = { | ||
currentPageSelectedCount: 10, // All 10 records in current page are selected | ||
currentPageTotalCount: 10, // Current page has 10 records | ||
totalRecordsCount: 100, // We have 100 records in total | ||
totalSelectedCount: 10, // Only the current page (10 records) are selected | ||
onSelectAllRecords: async () => { | ||
console.log("Select all records clicked"); | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
}, | ||
}; | ||
CurrentPageSelected.parameters = { | ||
docs: { | ||
description: { | ||
story: | ||
"Shows when all records in the current page are selected, offering the option to select all records across all pages.", | ||
}, | ||
}, | ||
}; | ||
|
||
export const AllPagesSelected = Template.bind({}); | ||
AllPagesSelected.args = { | ||
currentPageSelectedCount: 10, // All 10 records in current page are selected | ||
currentPageTotalCount: 10, // Current page has 10 records | ||
totalRecordsCount: 100, // We have 100 records in total | ||
totalSelectedCount: 100, // All records across all pages are selected | ||
onSelectAllRecords: async () => { | ||
console.log("Select all records clicked"); | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
}, | ||
}; | ||
AllPagesSelected.parameters = { | ||
docs: { | ||
description: { | ||
story: | ||
"Shows when all records across all pages are selected, displaying the total count.", | ||
}, | ||
}, | ||
}; |
Oops, something went wrong.