From 0a97f070829735a90b6c3cbbd1dc44e45fbf5c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Thu, 6 Feb 2025 09:57:01 +0100 Subject: [PATCH 1/3] feat: components for new paginated tree https://github.com/gisce/webclient/issues/1258 --- .../PaginationHeader.stories.tsx | 208 ++++++++++++++++++ .../ui/PaginationHeader/PaginationHeader.tsx | 92 ++++++++ .../PaginationHeader.types.ts | 10 + src/components/ui/PaginationHeader/index.ts | 2 + .../SelectAllRecordsRow.stories.tsx | 36 +++ .../SelectAllRecordsRow.tsx | 90 ++++++++ .../SelectAllRecordsRow.types.ts | 7 + .../ui/SelectAllRecordsRow/index.ts | 2 + src/components/ui/index.ts | 2 + src/locales/ca_ES.json | 5 +- src/locales/en_US.json | 5 +- src/locales/es_ES.json | 5 +- 12 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 src/components/ui/PaginationHeader/PaginationHeader.stories.tsx create mode 100644 src/components/ui/PaginationHeader/PaginationHeader.tsx create mode 100644 src/components/ui/PaginationHeader/PaginationHeader.types.ts create mode 100644 src/components/ui/PaginationHeader/index.ts create mode 100644 src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx create mode 100644 src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx create mode 100644 src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts create mode 100644 src/components/ui/SelectAllRecordsRow/index.ts diff --git a/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx b/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx new file mode 100644 index 0000000..4e7a32d --- /dev/null +++ b/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx @@ -0,0 +1,208 @@ +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; + +const Template: ComponentStory = (args) => { + return ; +}; + +// 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, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 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, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 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, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 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, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 0, + ...commonHandlers, +}; +LargeDataset.parameters = { + docs: { + description: { + story: "Shows pagination with a large dataset and larger page size.", + }, + }, +}; + +export const WithPartialSelection = Template.bind({}); +WithPartialSelection.args = { + initialPage: 1, + initialPageSize: 10, + total: 100, + realSelectedRowsLength: 5, + visibleSelectedRowsLength: 5, + ...commonHandlers, +}; +WithPartialSelection.parameters = { + docs: { + description: { + story: + "Shows pagination with some records selected, displaying the selection summary.", + }, + }, +}; + +export const WithAllVisibleSelected = Template.bind({}); +WithAllVisibleSelected.args = { + initialPage: 1, + initialPageSize: 10, + total: 100, + realSelectedRowsLength: 10, + visibleSelectedRowsLength: 10, + ...commonHandlers, +}; +WithAllVisibleSelected.parameters = { + docs: { + description: { + story: + "Shows pagination when all visible records on the current page are selected.", + }, + }, +}; + +export const WithAllRecordsSelected = Template.bind({}); +WithAllRecordsSelected.args = { + initialPage: 1, + initialPageSize: 10, + total: 100, + realSelectedRowsLength: 100, + visibleSelectedRowsLength: 10, + ...commonHandlers, +}; +WithAllRecordsSelected.parameters = { + docs: { + description: { + story: "Shows pagination when all records across all pages are selected.", + }, + }, +}; + +export const CustomPageSize = Template.bind({}); +CustomPageSize.args = { + initialPage: 1, + initialPageSize: 50, + total: 200, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 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, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 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, + realSelectedRowsLength: 0, + visibleSelectedRowsLength: 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.", + }, + }, +}; diff --git a/src/components/ui/PaginationHeader/PaginationHeader.tsx b/src/components/ui/PaginationHeader/PaginationHeader.tsx new file mode 100644 index 0000000..6821960 --- /dev/null +++ b/src/components/ui/PaginationHeader/PaginationHeader.tsx @@ -0,0 +1,92 @@ +import { useLocale } from "@/context"; +import { Col, Pagination, Row } from "antd"; +import { useMemo, useState } from "react"; +import { SelectAllRecordsRow } from "../SelectAllRecordsRow/SelectAllRecordsRow"; +import type { PaginationHeaderProps } from "./PaginationHeader.types"; + +const PaginationHeaderComp = (props: PaginationHeaderProps) => { + const { + total, + initialPage, + onRequestPageChange, + onSelectAllGlobalRecords, + initialPageSize, + onPageSizeChange, + realSelectedRowsLength, + visibleSelectedRowsLength, + } = 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 = (newPage: number, newPageSize?: number) => { + setPage(newPage); + if (newPageSize !== undefined) { + setPageSize(newPageSize); + } + onRequestPageChange(newPage, newPageSize); + }; + + const handlePageSizeChange = (newPageSize: number, newPage: number) => { + setPageSize(newPageSize); + setPage(newPage); + onPageSizeChange(newPageSize); + onRequestPageChange(newPage, newPageSize); + }; + + const mustShowSelectAllGlobalRecordsButton = + onSelectAllGlobalRecords !== undefined; + + const summary = useMemo(() => { + return total === undefined + ? null + : total === 0 + ? t("no_results") + : t("summary") + .replace("{from}", from?.toString()) + .replace("{to}", to?.toString()) + .replace("{total}", total?.toString()); + }, [total, from, to, t]); + + return ( + + + + + {mustShowSelectAllGlobalRecordsButton && ( + + + + )} + + {summary} + + + ); +}; + +export const PaginationHeader = PaginationHeaderComp; diff --git a/src/components/ui/PaginationHeader/PaginationHeader.types.ts b/src/components/ui/PaginationHeader/PaginationHeader.types.ts new file mode 100644 index 0000000..c8920fc --- /dev/null +++ b/src/components/ui/PaginationHeader/PaginationHeader.types.ts @@ -0,0 +1,10 @@ +export type PaginationHeaderProps = { + initialPage: number; + initialPageSize: number; + total: number; + realSelectedRowsLength: number; + visibleSelectedRowsLength: number; + onRequestPageChange: (page: number, pageSize?: number) => void; + onPageSizeChange: (pageSize: number) => void; + onSelectAllGlobalRecords: () => Promise; +}; diff --git a/src/components/ui/PaginationHeader/index.ts b/src/components/ui/PaginationHeader/index.ts new file mode 100644 index 0000000..91506c5 --- /dev/null +++ b/src/components/ui/PaginationHeader/index.ts @@ -0,0 +1,2 @@ +export { PaginationHeader } from "./PaginationHeader"; +export type { PaginationHeaderProps } from "./PaginationHeader.types"; diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx new file mode 100644 index 0000000..bf1bd0b --- /dev/null +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import { SelectAllRecordsRow } from "./SelectAllRecordsRow"; + +export default { + title: "Components/UI/SelectAllRecordsRow", + component: SelectAllRecordsRow, +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + return ; +}; + +export const Basic = Template.bind({}); +Basic.args = { + numberOfVisibleSelectedRows: 10, + numberOfRealSelectedRows: 10, + numberOfTotalRows: 100, + totalRecords: 100, + onSelectAllRecords: async () => { + console.log("Select all records clicked"); + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, +}; + +export const AllSelected = Template.bind({}); +AllSelected.args = { + numberOfVisibleSelectedRows: 100, + numberOfRealSelectedRows: 100, + numberOfTotalRows: 100, + totalRecords: 100, + onSelectAllRecords: async () => { + console.log("Select all records clicked"); + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, +}; diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx new file mode 100644 index 0000000..abb81f8 --- /dev/null +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx @@ -0,0 +1,90 @@ +import styled from "styled-components"; +import { useState } from "react"; +import { Spin } from "antd"; +import Link from "antd/es/typography/Link"; +import { useLocale } from "@/context"; +import type { SelectAllRecordsRowProps } from "./SelectAllRecordsRow.types"; + +export const Container = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +export const SelectAllRecordsRow = (props: SelectAllRecordsRowProps) => { + const { + numberOfVisibleSelectedRows, + totalRecords, + numberOfTotalRows, + onSelectAllRecords, + numberOfRealSelectedRows, + } = props; + const [loading, setLoading] = useState(false); + const { t } = useLocale(); + + const translations = { + recordsSelected: t("recordsSelected"), + selectAllRecords: t("selectAllRecords"), + allRecordsSelected: t("allRecordsSelected"), + }; + + if (numberOfTotalRows === 0) { + return null; + } + + if ( + numberOfVisibleSelectedRows < numberOfTotalRows && + numberOfRealSelectedRows <= numberOfTotalRows + ) { + return null; + } + + if (totalRecords === numberOfVisibleSelectedRows) { + return null; + } + + const handleClick = async (event: any) => { + event.preventDefault(); // prevent the default action (navigation) from happening + event.stopPropagation(); + setLoading(true); + await onSelectAllRecords(); + setLoading(false); + }; + + const selectRowsComponent = ( + + {translations.recordsSelected.replace( + "{numberOfSelectedRows}", + numberOfVisibleSelectedRows.toString(), + ) + " "} + + {loading ? ( + + ) : ( + + {translations.selectAllRecords.replace( + "{totalRecords}", + totalRecords.toString(), + )} + + )} + + ); + + const allRowsAreSelected = ( + + {translations.allRecordsSelected.replace( + "{totalRecords}", + numberOfRealSelectedRows.toString(), + ) + " "} + + ); + + return ( + + {numberOfRealSelectedRows > numberOfTotalRows + ? allRowsAreSelected + : selectRowsComponent} + + ); +}; diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts new file mode 100644 index 0000000..3e639ac --- /dev/null +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts @@ -0,0 +1,7 @@ +export type SelectAllRecordsRowProps = { + numberOfVisibleSelectedRows: number; + totalRecords: number; + numberOfTotalRows: number; + numberOfRealSelectedRows: number; + onSelectAllRecords: () => Promise; +}; diff --git a/src/components/ui/SelectAllRecordsRow/index.ts b/src/components/ui/SelectAllRecordsRow/index.ts new file mode 100644 index 0000000..ac213b4 --- /dev/null +++ b/src/components/ui/SelectAllRecordsRow/index.ts @@ -0,0 +1,2 @@ +export { SelectAllRecordsRow } from "./SelectAllRecordsRow"; +export type { SelectAllRecordsRowProps } from "./SelectAllRecordsRow.types"; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 67eee60..eda3283 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -4,3 +4,5 @@ export * from "./Separator"; export * from "./FieldSet"; export * from "./Dropdown"; export * from "./FavouriteButton"; +export * from "./PaginationHeader"; +export * from "./SelectAllRecordsRow"; diff --git a/src/locales/ca_ES.json b/src/locales/ca_ES.json index a98e603..6f14211 100644 --- a/src/locales/ca_ES.json +++ b/src/locales/ca_ES.json @@ -99,5 +99,8 @@ "noMatches": "No s'han trobat coincidències", "noExportFieldFound": "No s'ha trobat el camp en el model. Pot ser que s'hagi migrat o eliminat?", "exportHasFieldsUnavailable": "L'exportació té els següents camps no disponibles. Pot ser que s'hagin migrat o eliminat.", - "continueAndIgnoreFieldsUnavailable": "Vols continuar i ignorar els camps no disponibles?" + "continueAndIgnoreFieldsUnavailable": "Vols continuar i ignorar els camps no disponibles?", + "recordsSelected": "Hi ha {numberOfSelectedRows} registres seleccionats en aquesta pàgina.", + "selectAllRecords": "Seleccionar tots els {totalRecords} registres.", + "allRecordsSelected": "Hi ha {totalRecords} registres seleccionats." } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index b7a8a01..5aed65d 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -99,5 +99,8 @@ "noMatches": "No matches found", "noExportFieldFound": "Field not found in the model. It may have been migrated or deleted?", "exportHasFieldsUnavailable": "The export has the following fields unavailable. It may have been migrated or deleted.", - "continueAndIgnoreFieldsUnavailable": "Do you want to continue and ignore the unavailable fields?" + "continueAndIgnoreFieldsUnavailable": "Do you want to continue and ignore the unavailable fields?", + "recordsSelected": "There are {numberOfSelectedRows} records selected on this page.", + "selectAllRecords": "Select all {totalRecords} records.", + "allRecordsSelected": "There are {totalRecords} records selected." } diff --git a/src/locales/es_ES.json b/src/locales/es_ES.json index 8194aaa..d26da2a 100644 --- a/src/locales/es_ES.json +++ b/src/locales/es_ES.json @@ -99,5 +99,8 @@ "noMatches": "No hay coincidencias", "noExportFieldFound": "Campo no encontrado en el modelo. Puede haber sido migrado o eliminado?", "exportHasFieldsUnavailable": "La exportación tiene los siguientes campos no disponibles. Puede ser que se hayan migrado o eliminado.", - "continueAndIgnoreFieldsUnavailable": "Quieres continuar y ignorar los campos no disponibles?" + "continueAndIgnoreFieldsUnavailable": "Quieres continuar y ignorar los campos no disponibles?", + "recordsSelected": "Hay {numberOfSelectedRows} registros seleccionados en ésta página.", + "selectAllRecords": "Seleccionar todos los {totalRecords} registros.", + "allRecordsSelected": "Hay {totalRecords} registros seleccionados." } From ff787baf3d2db122cc0ae5e0b29c7ba37407048d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Thu, 6 Feb 2025 10:28:51 +0100 Subject: [PATCH 2/3] feat: more work for components of paginated tree https://github.com/gisce/webclient/issues/1258 --- .../PaginationHeader.stories.tsx | 71 ++++------ .../ui/PaginationHeader/PaginationHeader.tsx | 121 +++++++++++------- .../PaginationHeader.types.ts | 16 ++- .../SelectAllRecordsRow.stories.tsx | 48 +++++-- .../SelectAllRecordsRow.tsx | 120 +++++++++-------- .../SelectAllRecordsRow.types.ts | 13 +- src/locales/ca_ES.json | 3 +- src/locales/en_US.json | 3 +- src/locales/es_ES.json | 3 +- 9 files changed, 229 insertions(+), 169 deletions(-) diff --git a/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx b/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx index 4e7a32d..b68a605 100644 --- a/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx +++ b/src/components/ui/PaginationHeader/PaginationHeader.stories.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import { PaginationHeader } from "./PaginationHeader"; @@ -37,8 +38,8 @@ NoResults.args = { initialPage: 1, initialPageSize: 10, total: 0, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, ...commonHandlers, }; NoResults.parameters = { @@ -54,8 +55,8 @@ SinglePage.args = { initialPage: 1, initialPageSize: 10, total: 5, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, ...commonHandlers, }; SinglePage.parameters = { @@ -71,8 +72,8 @@ MultiplePages.args = { initialPage: 2, initialPageSize: 10, total: 25, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, ...commonHandlers, }; MultiplePages.parameters = { @@ -88,8 +89,8 @@ LargeDataset.args = { initialPage: 5, initialPageSize: 20, total: 1000, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, ...commonHandlers, }; LargeDataset.parameters = { @@ -100,55 +101,37 @@ LargeDataset.parameters = { }, }; -export const WithPartialSelection = Template.bind({}); -WithPartialSelection.args = { +export const WithPartialPageSelected = Template.bind({}); +WithPartialPageSelected.args = { initialPage: 1, initialPageSize: 10, total: 100, - realSelectedRowsLength: 5, - visibleSelectedRowsLength: 5, + currentPageSelectedCount: 10, + totalSelectedCount: 10, ...commonHandlers, }; -WithPartialSelection.parameters = { +WithPartialPageSelected.parameters = { docs: { description: { story: - "Shows pagination with some records selected, displaying the selection summary.", + "Shows when current page is fully selected but there are more records available to select globally.", }, }, }; -export const WithAllVisibleSelected = Template.bind({}); -WithAllVisibleSelected.args = { +export const WithAllPagesSelected = Template.bind({}); +WithAllPagesSelected.args = { initialPage: 1, initialPageSize: 10, total: 100, - realSelectedRowsLength: 10, - visibleSelectedRowsLength: 10, + currentPageSelectedCount: 10, + totalSelectedCount: 100, ...commonHandlers, }; -WithAllVisibleSelected.parameters = { +WithAllPagesSelected.parameters = { docs: { description: { - story: - "Shows pagination when all visible records on the current page are selected.", - }, - }, -}; - -export const WithAllRecordsSelected = Template.bind({}); -WithAllRecordsSelected.args = { - initialPage: 1, - initialPageSize: 10, - total: 100, - realSelectedRowsLength: 100, - visibleSelectedRowsLength: 10, - ...commonHandlers, -}; -WithAllRecordsSelected.parameters = { - docs: { - description: { - story: "Shows pagination when all records across all pages are selected.", + story: "Shows when all records across all pages are selected.", }, }, }; @@ -158,8 +141,8 @@ CustomPageSize.args = { initialPage: 1, initialPageSize: 50, total: 200, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, ...commonHandlers, }; CustomPageSize.parameters = { @@ -175,8 +158,8 @@ LastPage.args = { initialPage: 10, initialPageSize: 10, total: 100, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, ...commonHandlers, }; LastPage.parameters = { @@ -192,8 +175,8 @@ WithoutSelectionControls.args = { initialPage: 1, initialPageSize: 10, total: 100, - realSelectedRowsLength: 0, - visibleSelectedRowsLength: 0, + currentPageSelectedCount: 0, + totalSelectedCount: 0, onRequestPageChange: commonHandlers.onRequestPageChange, onPageSizeChange: commonHandlers.onPageSizeChange, // Omitting onSelectAllGlobalRecords to hide selection controls diff --git a/src/components/ui/PaginationHeader/PaginationHeader.tsx b/src/components/ui/PaginationHeader/PaginationHeader.tsx index 6821960..2c3329b 100644 --- a/src/components/ui/PaginationHeader/PaginationHeader.tsx +++ b/src/components/ui/PaginationHeader/PaginationHeader.tsx @@ -1,19 +1,21 @@ import { useLocale } from "@/context"; import { Col, Pagination, Row } from "antd"; -import { useMemo, useState } from "react"; +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 PaginationHeaderComp = (props: PaginationHeaderProps) => { +const PaginationHeaderComponent = (props: PaginationHeaderProps) => { const { total, initialPage, - onRequestPageChange, - onSelectAllGlobalRecords, initialPageSize, + currentPageSelectedCount, + totalSelectedCount, + onRequestPageChange, onPageSizeChange, - realSelectedRowsLength, - visibleSelectedRowsLength, + onSelectAllGlobalRecords, } = props; const { t } = useLocale(); @@ -27,56 +29,83 @@ const PaginationHeaderComp = (props: PaginationHeaderProps) => { [page, pageSize, total], ); - const handlePageChange = (newPage: number, newPageSize?: number) => { - setPage(newPage); - if (newPageSize !== undefined) { - setPageSize(newPageSize); - } - onRequestPageChange(newPage, newPageSize); - }; + const handlePageChange = useCallback( + (newPage: number, newPageSize?: number) => { + setPage(newPage); + if (newPageSize !== undefined) { + setPageSize(newPageSize); + } + onRequestPageChange(newPage, newPageSize); + }, + [onRequestPageChange], + ); - const handlePageSizeChange = (newPageSize: number, newPage: number) => { - setPageSize(newPageSize); - setPage(newPage); - onPageSizeChange(newPageSize); - onRequestPageChange(newPage, newPageSize); - }; + const handlePageSizeChange = useCallback( + (newPageSize: number, newPage: number) => { + setPageSize(newPageSize); + setPage(newPage); + onPageSizeChange(newPageSize); + onRequestPageChange(newPage, newPageSize); + }, + [onPageSizeChange, onRequestPageChange], + ); - const mustShowSelectAllGlobalRecordsButton = - onSelectAllGlobalRecords !== undefined; + const mustShowSelectAllGlobalRecordsButton = useMemo( + () => onSelectAllGlobalRecords !== undefined, + [onSelectAllGlobalRecords], + ); const summary = useMemo(() => { - return total === undefined - ? null - : total === 0 - ? t("no_results") - : t("summary") - .replace("{from}", from?.toString()) - .replace("{to}", to?.toString()) - .replace("{total}", total?.toString()); + 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, + currentPageTotalCount: pageSize, + totalRecordsCount: total, + totalSelectedCount, + onSelectAllRecords: onSelectAllGlobalRecords!, + }), + [ + currentPageSelectedCount, + pageSize, + total, + totalSelectedCount, + onSelectAllGlobalRecords, + ], + ); + return ( - + - + {mustShowSelectAllGlobalRecordsButton && ( - + )} { ); }; -export const PaginationHeader = PaginationHeaderComp; +export const PaginationHeader = memo(PaginationHeaderComponent); diff --git a/src/components/ui/PaginationHeader/PaginationHeader.types.ts b/src/components/ui/PaginationHeader/PaginationHeader.types.ts index c8920fc..dadf844 100644 --- a/src/components/ui/PaginationHeader/PaginationHeader.types.ts +++ b/src/components/ui/PaginationHeader/PaginationHeader.types.ts @@ -1,10 +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; - total: number; - realSelectedRowsLength: number; - visibleSelectedRowsLength: 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; - onSelectAllGlobalRecords: () => Promise; + /** Optional callback to select all records across all pages */ + onSelectAllGlobalRecords?: () => Promise; }; diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx index bf1bd0b..4cf0708 100644 --- a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.stories.tsx @@ -5,32 +5,56 @@ 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; const Template: ComponentStory = (args) => { return ; }; -export const Basic = Template.bind({}); -Basic.args = { - numberOfVisibleSelectedRows: 10, - numberOfRealSelectedRows: 10, - numberOfTotalRows: 100, - totalRecords: 100, +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 AllSelected = Template.bind({}); -AllSelected.args = { - numberOfVisibleSelectedRows: 100, - numberOfRealSelectedRows: 100, - numberOfTotalRows: 100, - totalRecords: 100, +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.", + }, + }, +}; diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx index abb81f8..55cb445 100644 --- a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { useState } from "react"; +import { useState, useCallback, memo, useMemo } from "react"; import { Spin } from "antd"; import Link from "antd/es/typography/Link"; import { useLocale } from "@/context"; @@ -11,80 +11,88 @@ export const Container = styled.div` justify-content: center; `; -export const SelectAllRecordsRow = (props: SelectAllRecordsRowProps) => { +const SelectAllRecordsRowComponent = (props: SelectAllRecordsRowProps) => { const { - numberOfVisibleSelectedRows, - totalRecords, - numberOfTotalRows, + currentPageSelectedCount, + currentPageTotalCount, + totalRecordsCount, + totalSelectedCount, onSelectAllRecords, - numberOfRealSelectedRows, } = props; + const [loading, setLoading] = useState(false); const { t } = useLocale(); - const translations = { - recordsSelected: t("recordsSelected"), - selectAllRecords: t("selectAllRecords"), - allRecordsSelected: t("allRecordsSelected"), - }; - - if (numberOfTotalRows === 0) { - return null; - } + // Memoize translations to avoid recalculating on every render + const translations = useMemo( + () => ({ + recordsSelected: t("recordsSelected"), + selectAllRecords: t("selectAllRecords"), + allRecordsSelected: t("allRecordsSelected"), + }), + [t], + ); - if ( - numberOfVisibleSelectedRows < numberOfTotalRows && - numberOfRealSelectedRows <= numberOfTotalRows - ) { + // Don't show anything if the current page is not fully selected + if (currentPageSelectedCount < currentPageTotalCount) { return null; } - if (totalRecords === numberOfVisibleSelectedRows) { + // Don't show anything if we don't have more records than the current page + if (totalRecordsCount <= currentPageTotalCount) { return null; } - const handleClick = async (event: any) => { - event.preventDefault(); // prevent the default action (navigation) from happening - event.stopPropagation(); - setLoading(true); - await onSelectAllRecords(); - setLoading(false); - }; - - const selectRowsComponent = ( - - {translations.recordsSelected.replace( - "{numberOfSelectedRows}", - numberOfVisibleSelectedRows.toString(), - ) + " "} + const handleClick = useCallback( + async (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setLoading(true); + try { + await onSelectAllRecords(); + } finally { + setLoading(false); + } + }, + [onSelectAllRecords], + ); - {loading ? ( - - ) : ( - - {translations.selectAllRecords.replace( + // If all records across all pages are selected, show the total count + if (totalSelectedCount === totalRecordsCount) { + return ( + + + {translations.allRecordsSelected.replace( "{totalRecords}", - totalRecords.toString(), + totalSelectedCount.toString(), )} - - )} - - ); - - const allRowsAreSelected = ( - - {translations.allRecordsSelected.replace( - "{totalRecords}", - numberOfRealSelectedRows.toString(), - ) + " "} - - ); + + + ); + } + // Show option to select all records when current page is selected but not all pages return ( - {numberOfRealSelectedRows > numberOfTotalRows - ? allRowsAreSelected - : selectRowsComponent} + + {translations.recordsSelected.replace( + "{numberOfSelectedRows}", + currentPageSelectedCount.toString(), + ) + " "} + + {loading ? ( + + ) : ( + + {translations.selectAllRecords.replace( + "{totalRecords}", + totalRecordsCount.toString(), + )} + + )} + ); }; + +export const SelectAllRecordsRow = memo(SelectAllRecordsRowComponent); diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts index 3e639ac..9254563 100644 --- a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts @@ -1,7 +1,12 @@ export type SelectAllRecordsRowProps = { - numberOfVisibleSelectedRows: number; - totalRecords: number; - numberOfTotalRows: number; - numberOfRealSelectedRows: number; + /** Number of records selected in the current page */ + currentPageSelectedCount: number; + /** Total number of records in the current page */ + currentPageTotalCount: number; + /** Total number of records across all pages */ + totalRecordsCount: number; + /** Total number of records selected across all pages */ + totalSelectedCount: number; + /** Callback to select all records across all pages */ onSelectAllRecords: () => Promise; }; diff --git a/src/locales/ca_ES.json b/src/locales/ca_ES.json index 6f14211..c850627 100644 --- a/src/locales/ca_ES.json +++ b/src/locales/ca_ES.json @@ -102,5 +102,6 @@ "continueAndIgnoreFieldsUnavailable": "Vols continuar i ignorar els camps no disponibles?", "recordsSelected": "Hi ha {numberOfSelectedRows} registres seleccionats en aquesta pàgina.", "selectAllRecords": "Seleccionar tots els {totalRecords} registres.", - "allRecordsSelected": "Hi ha {totalRecords} registres seleccionats." + "allRecordsSelected": "Hi ha {totalRecords} registres seleccionats.", + "items_per_page": "/ pàg." } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 5aed65d..991dd6e 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -102,5 +102,6 @@ "continueAndIgnoreFieldsUnavailable": "Do you want to continue and ignore the unavailable fields?", "recordsSelected": "There are {numberOfSelectedRows} records selected on this page.", "selectAllRecords": "Select all {totalRecords} records.", - "allRecordsSelected": "There are {totalRecords} records selected." + "allRecordsSelected": "There are {totalRecords} records selected.", + "items_per_page": "/ page" } diff --git a/src/locales/es_ES.json b/src/locales/es_ES.json index d26da2a..c495419 100644 --- a/src/locales/es_ES.json +++ b/src/locales/es_ES.json @@ -102,5 +102,6 @@ "continueAndIgnoreFieldsUnavailable": "Quieres continuar y ignorar los campos no disponibles?", "recordsSelected": "Hay {numberOfSelectedRows} registros seleccionados en ésta página.", "selectAllRecords": "Seleccionar todos los {totalRecords} registros.", - "allRecordsSelected": "Hay {totalRecords} registros seleccionados." + "allRecordsSelected": "Hay {totalRecords} registros seleccionados.", + "items_per_page": "/ pág." } From 5c17b3c90112b4c14b57a08a690d642d871561df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Thu, 6 Feb 2025 10:35:01 +0100 Subject: [PATCH 3/3] feat: adjustments --- .../ui/PaginationHeader/PaginationHeader.tsx | 19 +++--- .../SelectAllRecordsRow.tsx | 61 ++++++++----------- .../SelectAllRecordsRow.types.ts | 6 +- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/src/components/ui/PaginationHeader/PaginationHeader.tsx b/src/components/ui/PaginationHeader/PaginationHeader.tsx index 2c3329b..8325501 100644 --- a/src/components/ui/PaginationHeader/PaginationHeader.tsx +++ b/src/components/ui/PaginationHeader/PaginationHeader.tsx @@ -50,11 +50,6 @@ const PaginationHeaderComponent = (props: PaginationHeaderProps) => { [onPageSizeChange, onRequestPageChange], ); - const mustShowSelectAllGlobalRecordsButton = useMemo( - () => onSelectAllGlobalRecords !== undefined, - [onSelectAllGlobalRecords], - ); - const summary = useMemo(() => { if (total === undefined) return null; if (total === 0) return t("no_results"); @@ -84,7 +79,7 @@ const PaginationHeaderComponent = (props: PaginationHeaderProps) => { const selectAllRecordsProps: SelectAllRecordsRowProps = useMemo( () => ({ currentPageSelectedCount, - currentPageTotalCount: pageSize, + currentPageSize: pageSize, totalRecordsCount: total, totalSelectedCount, onSelectAllRecords: onSelectAllGlobalRecords!, @@ -98,20 +93,20 @@ const PaginationHeaderComponent = (props: PaginationHeaderProps) => { ], ); + const hasSelectionFeature = onSelectAllGlobalRecords !== undefined; + const columnSpan = hasSelectionFeature ? 8 : 12; + return ( - + - {mustShowSelectAllGlobalRecordsButton && ( + {hasSelectionFeature && ( )} - + {summary} diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx index 55cb445..93be305 100644 --- a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.tsx @@ -11,38 +11,16 @@ export const Container = styled.div` justify-content: center; `; -const SelectAllRecordsRowComponent = (props: SelectAllRecordsRowProps) => { - const { - currentPageSelectedCount, - currentPageTotalCount, - totalRecordsCount, - totalSelectedCount, - onSelectAllRecords, - } = props; - +const SelectAllRecordsRowComponent = ({ + currentPageSelectedCount, + currentPageSize, + totalRecordsCount, + totalSelectedCount, + onSelectAllRecords, +}: SelectAllRecordsRowProps) => { const [loading, setLoading] = useState(false); const { t } = useLocale(); - // Memoize translations to avoid recalculating on every render - const translations = useMemo( - () => ({ - recordsSelected: t("recordsSelected"), - selectAllRecords: t("selectAllRecords"), - allRecordsSelected: t("allRecordsSelected"), - }), - [t], - ); - - // Don't show anything if the current page is not fully selected - if (currentPageSelectedCount < currentPageTotalCount) { - return null; - } - - // Don't show anything if we don't have more records than the current page - if (totalRecordsCount <= currentPageTotalCount) { - return null; - } - const handleClick = useCallback( async (event: React.MouseEvent) => { event.preventDefault(); @@ -57,12 +35,27 @@ const SelectAllRecordsRowComponent = (props: SelectAllRecordsRowProps) => { [onSelectAllRecords], ); - // If all records across all pages are selected, show the total count + // Don't render anything if there are no records + if (totalRecordsCount === 0) { + return null; + } + + // Don't render if current page is not fully selected + if (currentPageSelectedCount < currentPageSize) { + return null; + } + + // Don't render if we don't have more records than current page + if (totalRecordsCount <= currentPageSize) { + return null; + } + + // If all records are selected, show the total count message if (totalSelectedCount === totalRecordsCount) { return ( - {translations.allRecordsSelected.replace( + {t("allRecordsSelected").replace( "{totalRecords}", totalSelectedCount.toString(), )} @@ -71,11 +64,11 @@ const SelectAllRecordsRowComponent = (props: SelectAllRecordsRowProps) => { ); } - // Show option to select all records when current page is selected but not all pages + // Show the select all option return ( - {translations.recordsSelected.replace( + {t("recordsSelected").replace( "{numberOfSelectedRows}", currentPageSelectedCount.toString(), ) + " "} @@ -84,7 +77,7 @@ const SelectAllRecordsRowComponent = (props: SelectAllRecordsRowProps) => { ) : ( - {translations.selectAllRecords.replace( + {t("selectAllRecords").replace( "{totalRecords}", totalRecordsCount.toString(), )} diff --git a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts index 9254563..529e109 100644 --- a/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts +++ b/src/components/ui/SelectAllRecordsRow/SelectAllRecordsRow.types.ts @@ -1,11 +1,11 @@ export type SelectAllRecordsRowProps = { /** Number of records selected in the current page */ currentPageSelectedCount: number; - /** Total number of records in the current page */ - currentPageTotalCount: number; + /** Size of the current page */ + currentPageSize: number; /** Total number of records across all pages */ totalRecordsCount: number; - /** Total number of records selected across all pages */ + /** Total number of selected records across all pages */ totalSelectedCount: number; /** Callback to select all records across all pages */ onSelectAllRecords: () => Promise;