Przeglądaj źródła

Add support API-side pagination in FilterList and NumberedPagination

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
Mihaela Balutoiu 1 miesiąc temu
rodzic
commit
240c4a77ef

+ 103 - 0
src/components/ui/Lists/FilterList/FilterList.spec.tsx

@@ -87,6 +87,35 @@ const FilterListWrap = (options?: {
   />
   />
 );
 );
 
 
+type ApiPaginationOptions = {
+  currentPage?: number;
+  hasNextPage?: boolean;
+  itemsPerPage?: number;
+  onPageChange?: (page: number) => void;
+  onItemsPerPageChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
+  items?: typeof ITEMS;
+};
+
+const FilterListApiPaginationWrap = (opts: ApiPaginationOptions = {}) => (
+  <FilterList
+    items={opts.items ?? ITEMS}
+    filterItems={FILTER_ITEMS}
+    itemFilterFunction={itemFilterFunction}
+    loading={false}
+    onReloadButtonClick={() => {}}
+    onItemClick={() => {}}
+    selectionLabel="test item"
+    renderItemComponent={ItemComponent}
+    apiPagination={{
+      currentPage: opts.currentPage ?? 1,
+      hasNextPage: opts.hasNextPage ?? false,
+      itemsPerPage: opts.itemsPerPage ?? 2,
+      onPageChange: opts.onPageChange ?? (() => {}),
+      onItemsPerPageChange: opts.onItemsPerPageChange ?? (() => {}),
+    }}
+  />
+);
+
 describe("FilterList", () => {
 describe("FilterList", () => {
   beforeAll(() => {
   beforeAll(() => {
     window.HTMLElement.prototype.scrollTo = jest.fn();
     window.HTMLElement.prototype.scrollTo = jest.fn();
@@ -159,6 +188,80 @@ describe("FilterList", () => {
     expect(onItemClick).toHaveBeenCalledWith(ITEMS[1]);
     expect(onItemClick).toHaveBeenCalledWith(ITEMS[1]);
   });
   });
 
 
+  describe("API pagination mode", () => {
+    it("renders all items without client-side slicing", () => {
+      render(<FilterListApiPaginationWrap />);
+      const listItems = TestUtils.selectAll("FilterListspec__MainListItem-");
+      expect(listItems).toHaveLength(ITEMS.length);
+    });
+
+    it("shows 'Page X of X+1' when hasNextPage is true (at least one more page)", () => {
+      render(
+        <FilterListApiPaginationWrap currentPage={2} hasNextPage={true} />,
+      );
+      expect(
+        TestUtils.select("NumberedPagination__PageNumber")?.textContent,
+      ).toBe("Page 2 of 3");
+    });
+
+    it("shows 'Page X of X' when hasNextPage is false (current page is last)", () => {
+      render(
+        <FilterListApiPaginationWrap currentPage={2} hasNextPage={false} />,
+      );
+      expect(
+        TestUtils.select("NumberedPagination__PageNumber")?.textContent,
+      ).toBe("Page 2 of 2");
+    });
+
+    it("disables Next when hasNextPage is false", () => {
+      render(<FilterListApiPaginationWrap hasNextPage={false} />);
+      const nextButton = Array.from(document.querySelectorAll("button")).find(
+        btn => btn.textContent === "Next",
+      );
+      expect(nextButton).toHaveProperty("disabled", true);
+    });
+
+    it("enables Next when hasNextPage is true", () => {
+      render(<FilterListApiPaginationWrap hasNextPage={true} />);
+      const nextButton = Array.from(document.querySelectorAll("button")).find(
+        btn => btn.textContent === "Next",
+      );
+      expect(nextButton).not.toHaveProperty("disabled", true);
+    });
+
+    it("calls onPageChange with next page when Next is clicked", () => {
+      const onPageChange = jest.fn();
+      render(
+        <FilterListApiPaginationWrap
+          currentPage={1}
+          hasNextPage={true}
+          onPageChange={onPageChange}
+        />,
+      );
+      const nextButton = Array.from(document.querySelectorAll("button")).find(
+        btn => btn.textContent === "Next",
+      )!;
+      fireEvent.click(nextButton);
+      expect(onPageChange).toHaveBeenCalledWith(2);
+    });
+
+    it("calls onPageChange with previous page when Previous is clicked", () => {
+      const onPageChange = jest.fn();
+      render(
+        <FilterListApiPaginationWrap
+          currentPage={3}
+          hasNextPage={false}
+          onPageChange={onPageChange}
+        />,
+      );
+      const prevButton = Array.from(document.querySelectorAll("button")).find(
+        btn => btn.textContent === "Previous",
+      )!;
+      fireEvent.click(prevButton);
+      expect(onPageChange).toHaveBeenCalledWith(2);
+    });
+  });
+
   it("selects items", async () => {
   it("selects items", async () => {
     const onSelectedItemsChange = jest.fn();
     const onSelectedItemsChange = jest.fn();
     render(FilterListWrap({ onSelectedItemsChange }));
     render(FilterListWrap({ onSelectedItemsChange }));

+ 47 - 5
src/components/ui/Lists/FilterList/FilterList.tsx

@@ -40,6 +40,13 @@ const Footer = styled.div<any>`
 `;
 `;
 
 
 type DictItem = { value: string; label: string };
 type DictItem = { value: string; label: string };
+export type ApiPaginationProps = {
+  currentPage: number;
+  hasNextPage: boolean;
+  itemsPerPage: number;
+  onPageChange: (page: number) => void;
+  onItemsPerPageChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
+};
 type Props = {
 type Props = {
   items: any[];
   items: any[];
   dropdownActions?: DropdownAction[];
   dropdownActions?: DropdownAction[];
@@ -67,6 +74,7 @@ type Props = {
   listHeaderComponent?: React.ReactNode;
   listHeaderComponent?: React.ReactNode;
   itemsPerPageOptions?: number[];
   itemsPerPageOptions?: number[];
   initialItemsPerPage?: number;
   initialItemsPerPage?: number;
+  apiPagination?: ApiPaginationProps;
 };
 };
 type State = {
 type State = {
   items: any[];
   items: any[];
@@ -137,6 +145,9 @@ class FilterList extends React.Component<Props, State> {
   }
   }
 
 
   get paginatedItems() {
   get paginatedItems() {
+    if (this.props.apiPagination) {
+      return this.state.items;
+    }
     let paginatedItems = this.state.items;
     let paginatedItems = this.state.items;
     if (paginatedItems.length > this.state.itemsPerPage) {
     if (paginatedItems.length > this.state.itemsPerPage) {
       paginatedItems = this.state.items.filter(
       paginatedItems = this.state.items.filter(
@@ -259,12 +270,20 @@ class FilterList extends React.Component<Props, State> {
   }
   }
 
 
   handlePageClick = (page: number) => {
   handlePageClick = (page: number) => {
-    this.setPageAndItemsPerPage(page);
+    if (this.props.apiPagination) {
+      this.props.apiPagination.onPageChange(page);
+    } else {
+      this.setPageAndItemsPerPage(page);
+    }
   };
   };
 
 
   handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
   handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
-    const itemsPerPage = parseInt(event.target.value, 10);
-    this.setPageAndItemsPerPage(1, itemsPerPage);
+    if (this.props.apiPagination) {
+      this.props.apiPagination.onItemsPerPageChange(event);
+    } else {
+      const itemsPerPage = parseInt(event.target.value, 10);
+      this.setPageAndItemsPerPage(1, itemsPerPage);
+    }
   };
   };
 
 
   getFooterText() {
   getFooterText() {
@@ -278,6 +297,27 @@ class FilterList extends React.Component<Props, State> {
   }
   }
 
 
   renderPagination() {
   renderPagination() {
+    const { apiPagination } = this.props;
+
+    if (apiPagination) {
+      const { currentPage, hasNextPage, itemsPerPage } = apiPagination;
+      const apiItemsCount = hasNextPage
+        ? (currentPage + 1) * itemsPerPage
+        : currentPage * itemsPerPage;
+      return (
+        <NumberedPagination
+          itemsCount={apiItemsCount}
+          currentPage={currentPage}
+          itemsPerPage={itemsPerPage}
+          hasNextPage={hasNextPage}
+          style={{ margin: "0 5px" }}
+          onPageChange={this.handlePageClick}
+          onItemsPerPageChange={this.handleItemsPerPageChange}
+          itemsPerPageOptions={this.props.itemsPerPageOptions || [25, 50, 100]}
+        />
+      );
+    }
+
     if (this.state.items.length === 0) {
     if (this.state.items.length === 0) {
       return null;
       return null;
     }
     }
@@ -336,7 +376,9 @@ class FilterList extends React.Component<Props, State> {
           showEmptyList={
           showEmptyList={
             this.state.items.length === 0 &&
             this.state.items.length === 0 &&
             this.state.filterStatus === "all" &&
             this.state.filterStatus === "all" &&
-            this.state.filterText === ""
+            this.state.filterText === "" &&
+            (!this.props.apiPagination ||
+              this.props.apiPagination.currentPage === 1)
           }
           }
           emptyListImage={this.props.emptyListImage}
           emptyListImage={this.props.emptyListImage}
           emptyListMessage={this.props.emptyListMessage}
           emptyListMessage={this.props.emptyListMessage}
@@ -345,7 +387,7 @@ class FilterList extends React.Component<Props, State> {
           emptyListComponent={this.props.emptyListComponent}
           emptyListComponent={this.props.emptyListComponent}
           onEmptyListButtonClick={this.props.onEmptyListButtonClick}
           onEmptyListButtonClick={this.props.onEmptyListButtonClick}
         />
         />
-        {this.state.items.length > 0 && (
+        {(this.state.items.length > 0 || this.props.apiPagination) && (
           <Footer>
           <Footer>
             <div>{this.getFooterText()}</div>
             <div>{this.getFooterText()}</div>
             <div style={{ flex: 1, display: "flex", justifyContent: "center" }}>
             <div style={{ flex: 1, display: "flex", justifyContent: "center" }}>

+ 47 - 0
src/components/ui/Pagination/NumberedPagination/NumberedPagination.spec.tsx

@@ -29,6 +29,7 @@ const NumberedPaginationWithDefaultProps = (
     onPageChange={props.onPageChange || (() => {})}
     onPageChange={props.onPageChange || (() => {})}
     onItemsPerPageChange={props.onItemsPerPageChange || (() => {})}
     onItemsPerPageChange={props.onItemsPerPageChange || (() => {})}
     style={props.style}
     style={props.style}
+    hasNextPage={props.hasNextPage}
   />
   />
 );
 );
 
 
@@ -117,4 +118,50 @@ describe("NumberedPagination", () => {
       expect(options[index].textContent).toBe(option.toString());
       expect(options[index].textContent).toBe(option.toString());
     });
     });
   });
   });
+
+  describe("API pagination mode (hasNextPage prop)", () => {
+    it("shows 'of N' total derived from itemsCount even when hasNextPage prop is provided", () => {
+      render(
+        <NumberedPaginationWithDefaultProps
+          currentPage={1}
+          itemsCount={100}
+          itemsPerPage={10}
+          hasNextPage={true}
+        />,
+      );
+      expect(
+        TestUtils.select("NumberedPagination__PageNumber")?.textContent,
+      ).toBe("Page 1 of 10");
+    });
+
+    it("disables Next button when hasNextPage is false even if itemsCount suggests more pages", () => {
+      const onPageChange = jest.fn();
+      render(
+        <NumberedPaginationWithDefaultProps
+          currentPage={1}
+          itemsCount={100}
+          itemsPerPage={10}
+          hasNextPage={false}
+          onPageChange={onPageChange}
+        />,
+      );
+      const nextButton = screen.getByRole("button", { name: "Next" });
+      expect(nextButton).toHaveProperty("disabled", true);
+    });
+
+    it("enables Next button when hasNextPage is true", () => {
+      const onPageChange = jest.fn();
+      render(
+        <NumberedPaginationWithDefaultProps
+          currentPage={3}
+          itemsCount={30}
+          itemsPerPage={10}
+          hasNextPage={true}
+          onPageChange={onPageChange}
+        />,
+      );
+      const nextButton = screen.getByRole("button", { name: "Next" });
+      expect(nextButton).not.toHaveProperty("disabled", true);
+    });
+  });
 });
 });

+ 7 - 2
src/components/ui/Pagination/NumberedPagination/NumberedPagination.tsx

@@ -61,6 +61,7 @@ type Props = {
   itemsPerPageOptions: number[];
   itemsPerPageOptions: number[];
   onPageChange: (newPage: number) => void;
   onPageChange: (newPage: number) => void;
   onItemsPerPageChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
   onItemsPerPageChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
+  hasNextPage?: boolean;
 };
 };
 
 
 @observer
 @observer
@@ -68,7 +69,11 @@ class NumberedPagination extends React.Component<Props> {
   render() {
   render() {
     const { itemsCount, currentPage, itemsPerPage } = this.props;
     const { itemsCount, currentPage, itemsPerPage } = this.props;
     const totalPages = Math.max(1, Math.ceil(itemsCount / itemsPerPage));
     const totalPages = Math.max(1, Math.ceil(itemsCount / itemsPerPage));
-    const hasNextPage = currentPage * itemsPerPage < itemsCount;
+    const computedHasNextPage = currentPage * itemsPerPage < itemsCount;
+    const hasNextPage =
+      this.props.hasNextPage !== undefined
+        ? this.props.hasNextPage
+        : computedHasNextPage;
 
 
     return (
     return (
       <PaginationWrapper
       <PaginationWrapper
@@ -90,7 +95,7 @@ class NumberedPagination extends React.Component<Props> {
         </PageNumber>
         </PageNumber>
         <PageNext
         <PageNext
           onClick={() => {
           onClick={() => {
-            if (currentPage < totalPages) {
+            if (hasNextPage) {
               this.props.onPageChange(currentPage + 1);
               this.props.onPageChange(currentPage + 1);
             }
             }
           }}
           }}