React pagination page number icons not working

I’ve created a React frontend list page in my application. When I click on the pagination buttons, the contents of the page changes as it should, but the page number icon at the bottom of the screen does not correspond. The “1” button is always highlighted. And, because of this, I can’t go back to page 1 or use the left arrow to navigate back.

Here is page 1:

image

Here is page 2:

image

Note that the “1” button at the bottom of the page is still highlighted. It should be “2” now.

Here is page 9:

image

Last page of the group - yet button 1 is still highlighted.

Any suggestions on how to fix this?

Hi @eraskin,

Couldn’t reproduce it, could you please share the code?

Our basic homepage allows anonymous access. I have a Client Login menu item that references SecureArea.tsx, which enforces login. Then, based on the type of user account, I dispatch to various tabs. This is all a work in progress.

I let Cuba Studio generate the standard pagination, list and edit pages. Then I modified the pagination and list pages to pass along an internal user id loaded by SecureArea in the props (used for lookups in my database). That all seems to work OK after a lot of tweaking.

MergesManagement.tsx:

import * as React from "react";
import { RouteComponentProps } from "react-router";
import { observer } from "mobx-react";
import MergesEdit from "./MergesEdit";
import MergesList from "./MergesList";
import { PaginationConfig } from "antd/es/pagination";
import { action, observable } from "mobx";
import {
  addPagingParams,
  createPagingConfig,
  defaultPagingConfig
} from "@cuba-platform/react-ui";
import {Webusers} from "../../cuba/entities/pasweb_Webusers";

type Props = RouteComponentProps<{ entityId?: string }> & { webUser: Webusers };

@observer
export class MergesManagement extends React.Component<Props> {
  static PATH = "/mergesManagement";
  static NEW_SUBPATH = "new";

  @observable paginationConfig: PaginationConfig = { ...defaultPagingConfig };

  componentDidMount(): void {
    // to disable paging config pass 'true' as disabled param in function below
    this.paginationConfig = createPagingConfig(this.props.location.search);
  }

  render() {
    const { entityId } = this.props.match.params;
    return entityId ? <MergesEdit entityId={entityId} /> : <MergesList webUser={this.props.webUser}/>;
  }

  @action onPagingChange = (current: number, pageSize: number) => {
    this.props.history.push(
      addPagingParams("mergesManagement", current, pageSize)
    );
    this.paginationConfig = { ...this.paginationConfig, current, pageSize };
  };
}

MergesList.tsx:

import * as React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { action, observable } from "mobx";
import { Button, message } from "antd";
import Moment from 'moment';

import {
  collection,
  injectMainStore,
  MainStoreInjected
} from "@cuba-platform/react-core";
import { DataTable, Spinner } from "@cuba-platform/react-ui";

import { Orders } from "../../cuba/entities/pasweb_Orders";
import { SerializedEntity } from "@cuba-platform/rest";
import { MergesManagement } from "./MergesManagement";
import {
  FormattedMessage,
  injectIntl,
  WrappedComponentProps
} from "react-intl";
import {Webusers} from "../../cuba/entities/pasweb_Webusers";
import {DataCollectionStore} from "@cuba-platform/react-core/dist/data/Collection";
import Title from "antd/lib/typography/Title";
import {ReportsLink} from "../reportsLink/ReportsLink";

type Props = { webUser: Webusers } & MainStoreInjected & WrappedComponentProps;

@injectMainStore
@observer
class MergesListComponent extends React.Component<Props>
{

  @observable dataCollection: DataCollectionStore<Orders>;
  @observable performingOrderLookup = false;

  @observable selectedRowKey: string | undefined;

  url = "";
  download = "";

  @action
  doLoadOrders(customerId: string | null ) {
    this.performingOrderLookup = true;
    this.dataCollection = collection<Orders>(Orders.NAME, {
      view: "orders-view",
      sort: "-bkrnum",
      filter: {conditions: [
          {property: "ordType", operator: "=", value: "MPO"},
          {property: "cancelled", operator: "=", value:"1"},
          {property: "cus", operator: "=", value: customerId }
        ]},
      loadImmediately: false
    });

    this.dataCollection.load()
      .then(
        action(() => {
          this.performingOrderLookup = false;
        })
      )
      .catch(
        action(() => {
          this.performingOrderLookup = false;
          message.error(this.props.intl.formatMessage({ id: "mergesList.NoMergesFound" }));
        })
      );
  }

  render() {
    if (this.props.mainStore?.isEntityDataLoaded() !== true) return <Spinner />;
    if (this.props.webUser == null) return <Spinner/>;
    if (this.props.webUser.customer?.id == null) {
      return (<Title level={1}>Customer missing - internal error</Title>);
    }

    if (this.dataCollection == null && !this.performingOrderLookup) {
      this.doLoadOrders(this.props.webUser.customer.id);
      return <Spinner />;
    }

    if (this.dataCollection.status !== "DONE") {
      if (this.dataCollection.status == "ERROR") {
        return (<Title level={1}>Can't locate user - log in failed.</Title>);
      }
      return <Spinner/>;
    }

    const buttons = [
      <Link to={MergesManagement.PATH + "/" + this.selectedRowKey} key="edit">
        <Button
          htmlType="button"
          style={{ margin: "0 12px 12px 0" }}
          disabled={!this.selectedRowKey}
          type="default"
        >
          <FormattedMessage id="common.view" />
        </Button>
      </Link>
    ];

    return (
      <DataTable
        dataCollection={this.dataCollection}
        canSelectRowByClick={true}
        columnDefinitions={[
          {field: "bkrnum", columnProps: { title: "Bkr Num" }},
          {field: "id", columnProps: {title: "PAS Num"}},
          {field: "orddate", columnProps: { title: "Order Date", render: (text, record: any) => ( Moment(record.orddate).format("MM/DD/YYYY"))}},
          {field: "cutoffdate", columnProps: {title: "Cutoff Date", render: (text, record: any) => ( Moment(record.cutoffdate).format("MM/DD/YYYY"))}},
          {field: "wantdate", columnProps: {title: "Wanted By", render: (text, record: any) => ( Moment(record.wantdate).format("MM/DD/YYYY"))}},
          {field: "maildate", columnProps: {title: "Mail Date", render: (text, record: any) => ( Moment(record.maildate).format("MM/DD/YYYY"))}},
          {field: "shipdate", columnProps: {title: "Ship Date", render: (text, record: any) => ( Moment(record.shipdate).format("MM/DD/YYYY"))}},
          {field: "totQtyOrdered", columnProps: {title: "Qty Ordered", render: (text, record:any) => ( record.totQtyOrdered.toLocaleString('en-US', {maximumFractionDigits:0}))}},
          {field: "totQtyReceived", columnProps: {title: "Qty Received", render: (text, record:any) => ( record.totQtyReceived.toLocaleString('en-US', {maximumFractionDigits:0}))}},
          {field: "totQtyConverted", columnProps: {title: "Qty Converted", render: (text, record:any) => ( record.totQtyConverted.toLocaleString('en-US', {maximumFractionDigits:0}))}},
          {columnProps: {
            render: (text, record : any) => (
              <ReportsLink ordnum={record.id} bkrnum={record.bkrnum}/>
              )
            }
          }
        ]}
        onRowSelectionChange={this.handleRowSelectionChange}
        hideSelectionColumn={true}
        buttons={buttons}
      />
    );
  }

  getRecordById(id: string): SerializedEntity<Orders> {
    const record:
      | SerializedEntity<Orders>
      | undefined = this.dataCollection.items.find(record => record.id === id);

    if (!record) {
      throw new Error("Cannot find entity with id " + id);
    }

    return record;
  }

  handleRowSelectionChange = (selectedRowKeys: string[]) => {
    this.selectedRowKey = selectedRowKeys[0];
  };
}

const MergesList = injectIntl(MergesListComponent);

export default MergesList;

SecureArea.tsx (this calls MergesManagement):

import * as React from "react";

import {Divider, message, Tabs, Typography} from "antd";
import { observer } from "mobx-react";
import ModalLogin from "../login/ModalLogin";

import {
  collection, DataCollectionStore, DataInstanceStore,
  injectMainStore, instance,
  MainStoreInjected
} from "@cuba-platform/react-core";
import {
  injectIntl,
  WrappedComponentProps
} from "react-intl";
import {Webusers} from "../../cuba/entities/pasweb_Webusers";
import {Spinner} from "@cuba-platform/react-ui";
import {action, autorun, IReactionDisposer, observable, reaction} from "mobx";
import {WebUserTypes} from "../../cuba/enums/enums";
import MergesList from "../merges/MergesList";
import {MergesManagement} from "../merges/MergesManagement";
import {RouteComponentProps} from "react-router";

const { TabPane } = Tabs;
const { Title } = Typography;

type Props = RouteComponentProps<{ entityId?: string }> & MainStoreInjected & WrappedComponentProps;

@injectMainStore
@observer
class SecureArea extends React.Component<Props> {

  @observable performingWebuserLookup = false;
  @observable webUsers: DataCollectionStore<Webusers>;
  @observable webUser: Webusers;
  reactionDisposer: IReactionDisposer;

  @action
  doWebuserLookup(userName: string | undefined) {
    this.performingWebuserLookup = true;
    this.webUsers = collection<Webusers>(Webusers.NAME, {
      view: "webusers-view",
      filter: {
        conditions: [
          {property: "login", operator: "=", value: userName || null}
        ]
      },
      loadImmediately: false
    });
    this.webUsers.load()
      .then(
        action(() => {
          this.webUser = this.webUsers.items[0];
          this.performingWebuserLookup = false;
        })
      )
      .catch(
        action(() => {
          this.performingWebuserLookup = false;
          message.error(this.props.intl.formatMessage({ id: "secureArea.userNotFound" }));
        })
      );
  }

  render() {
    const mainStore = this.props.mainStore!;
    const {initialized, authenticated, userName, metadata, messages, enums, locale, loginRequired} = mainStore;

    if (loginRequired) {
      return (
        <ModalLogin title="PAS Client Access"/>
      );
    }

    if (mainStore == null || !mainStore.isEntityDataLoaded() || this.performingWebuserLookup) {
      return <Spinner />;
    }

    if (this.webUser == null && !this.performingWebuserLookup) {
      this.doWebuserLookup(userName);
      return <Spinner />;
    }

   if (this.webUsers.status !== "DONE") {
      if (this.webUsers.status == "ERROR") {
        return (<Title level={1}>Can't locate user - log in failed.</Title>);
      }
      return <Spinner/>;
    }

   if (this.webUser != null && this.webUser.customer == null) {
     return (<Title level={1}>Customer not assigned for this Web User</Title>);
   }

   if (this.webUser != undefined && this.webUser.usertype === WebUserTypes.MAILER) {
      return (
        <div>
          <Tabs onChange={onTabChange} type="card">
            <TabPane tab="Merges" key="Merges">
              <MergesList webUser={this.webUser}/>
            </TabPane>
          </Tabs>
        </div>
      );
    } else if (this.webUser != undefined && this.webUser.usertype === WebUserTypes.OWNER) {
      return (
        <div>
          <TabPane tab="Rentals" key="Rentals">
            Rentals Data
          </TabPane>
        </div>
      );
    } else if (this.webUser != undefined && this.webUser.usertype === WebUserTypes.MAILEROWNER) {
      return (
        <div>
          <Tabs onChange={onTabChange} type="card">
            <TabPane tab="Merges" key="Merges">
              <MergesManagement webUser={this.webUser} match={this.props.match} location={this.props.location} history={this.props.history}/>
            </TabPane>
            <TabPane tab="Rentals" key="Rentals">
              Rentals Data
            </TabPane>
          </Tabs>
        </div>
      );
    } else if (this.webUser != undefined && this.webUser.usertype === WebUserTypes.CONSULTANT) {
      return (
        <div>
          <Tabs onChange={onTabChange} type="card">
            <TabPane tab="Mailers" key="Mailers">
              Mailers Data
            </TabPane>
            <TabPane tab="Owners" key="Owners">
              Owners Data
            </TabPane>
          </Tabs>
        </div>
      );
    } else {
      return (
        <Title level={1}>Can't locate user - log in failed.</Title>
      );
    }

  }

  componentWillUnmount() {
    if (this.reactionDisposer != null) this.reactionDisposer();
  }

}

function onTabChange(key: string) {
  if (key == "Merges") {
    //TODO: Load Merges data
  } else if (key == "Rentals") {
    //TODO: Load Rentals data
  } else if (key == "Mailers") {
    //TODO: Load Mailers data
  } else if (key == "Owners") {
    //TODO: Load Owners data
  }

};

export default injectIntl(SecureArea);

By the way, I have another open Forum message because sorting only partially works. If I click on a column header, it sorts ascending. If I click again, it does NOT sort descending or disable the sort as you would expect. It just sorts ascending again. I mention it here in the hope that the two problems are related to an issue with state or something.

@eraskin Could you please post your project somewhere so that I can take a closer look.

I have just sent you a Github invitatation to my private repository (ericraskin/pasweb). The most recent commit is the current state of the project (errors and all).

Let me know what else I can provide.

I have done a lot of tracing, at it appears this @action onPagingChange (bottom of the code) is not firing. We do have paginationConfig as an @observable, so any idea why it doesn’t get executed?

Since this does not execute, the page and pageSize parameters do not get added to the url, so it does not keep track of its state. The paging code does get called, so the underlying table does update.

Clearly I’m missing something.

type Props = RouteComponentProps & { webUser: Webusers };

@observer
export class MergesManagement extends React.Component<Props> {
  static PATH = "/mergesManagement";

  @observable paginationConfig: PaginationConfig = { ...defaultPagingConfig };

  componentDidMount(): void {
    // to disable paging config pass 'true' as disabled param in function below
    this.paginationConfig = createPagingConfig(this.props.location.search);
  }

  render() {
    return <MergesList webUser={this.props.webUser}/>;
  }

  @action onPagingChange = (current: number, pageSize: number) => {
    this.props.history.push(
      addPagingParams("mergesManagement", current, pageSize)
    );
    this.paginationConfig = { ...this.paginationConfig, current, pageSize };
  };
}

This is located in the MergesManagement.tsx file.

I added the following code to MergesManagement.tsx and confirmed that paginationConfig is not changing when page buttons are clicked:

 constructor(props: any) {
    super(props);
    autorun(() => {
      console.log(this.paginationConfig);
    });
  }

This fired twice when the component got created but did not fire when clicking on any of the pagination buttons.

TL;DR

I am still trying to track this down. I can not find any place in @cuba-platform/react-ui/index.esm.js where the pagination onShowSizeChange or onChange properties are set to the onPagingChange action defined in MergesManagement.tsx. I have been told that paging works fine, but I have not been able to make it happen. I am running on Cuba Platform 7.2.10. Is there a newer version that I should be using?

My application does not need editing functionality, so I collapsed MergesManagement.tsx paging directly into MergesList.tsx and stopped using MergesManagement.tsx. As far as I can tell, the “…Management.tsx” is created by Studio to load either the browse or the edit page depending on whether an entityId is in the URL. Since I don’t need editing and the browse paging it adds does not work for me, it’s easier to just do away with it completely.

I modified to use this code now in MergesList.tsx:

type Props = { webUser: Webusers } & MainStoreInjected & WrappedComponentProps & RouteComponentProps;

@injectMainStore
@observer
class MergesListComponent extends React.Component<Props>
{

  dataCollection: DataCollectionStore<Orders>;
  @observable performingOrderLookup = false;

  @observable selectedRowKey: string | undefined;

  @observable paginationConfig: PaginationConfig = { ...defaultPagingConfig };

  url = "";
  download = "";

  @action
  doLoadOrders(customerId: string | null ) {
    this.performingOrderLookup = true;
    this.dataCollection = collection<Orders>(Orders.NAME, {
      view: "orders-view",
      sort: "-bkrnum",
      filter: {conditions: [
          {property: "ordType", operator: "=", value: "MPO"},
          {property: "cancelled", operator: "=", value:"1"},
          {property: "cus", operator: "=", value: customerId }
        ]},
      loadImmediately: false
    });

    this.dataCollection.load()
      .then(
        action(() => {
          this.performingOrderLookup = false;
        })
      )
      .catch(
        action(() => {
          this.performingOrderLookup = false;
          message.error(this.props.intl.formatMessage({ id: "mergesList.NoMergesFound" }));
        })
      );
  }

  render() {
    if (this.props.mainStore?.isEntityDataLoaded() !== true) return <Spinner />;
    if (this.props.webUser == null) return <Spinner/>;
    if (this.props.webUser.customer?.id == null) {
      return (<Title level={1}>Customer missing - internal error</Title>);
    }

    if (this.dataCollection == null && !this.performingOrderLookup) {
      this.doLoadOrders(this.props.webUser.customer.id);
      return <Spinner />;
    }

    if (this.dataCollection.status !== "DONE") {
      if (this.dataCollection.status == "ERROR") {
        return (<Title level={1}>Can't locate user - log in failed.</Title>);
      }
      return <Spinner/>;
    }

    const buttons = [
      <Link to={"/mergesView/" + this.selectedRowKey} key="edit">
        <Button
          htmlType="button"
          style={{ margin: "0 12px 12px 0" }}
          disabled={!this.selectedRowKey}
          type="default"
        >
          <FormattedMessage id="common.view" />
        </Button>
      </Link>
    ];

    return (
      <DataTable
        dataCollection={this.dataCollection}
        canSelectRowByClick={true}
        tableProps={{ pagination: this.paginationConfig }}
        columnDefinitions={[
          {field: "bkrnum", columnProps: { title: "Bkr Num" }},
          {field: "id", columnProps: {title: "PAS Num"}},
          {field: "orddate", columnProps: { title: "Order Date", render: (text, record: any) => ( Moment(record.orddate).format("MM/DD/YYYY"))}},
          {field: "cutoffdate", columnProps: {title: "Cutoff Date", render: (text, record: any) => ( Moment(record.cutoffdate).format("MM/DD/YYYY"))}},
          {field: "wantdate", columnProps: {title: "Wanted By", render: (text, record: any) => ( Moment(record.wantdate).format("MM/DD/YYYY"))}},
          {field: "maildate", columnProps: {title: "Mail Date", render: (text, record: any) => ( Moment(record.maildate).format("MM/DD/YYYY"))}},
          {field: "shipdate", columnProps: {title: "Ship Date", render: (text, record: any) => ( Moment(record.shipdate).format("MM/DD/YYYY"))}},
          {field: "totQtyOrdered", columnProps: {title: "Qty Ordered", render: (text, record:any) => ( record.totQtyOrdered.toLocaleString('en-US', {maximumFractionDigits:0}))}},
          {field: "totQtyReceived", columnProps: {title: "Qty Received", render: (text, record:any) => ( record.totQtyReceived.toLocaleString('en-US', {maximumFractionDigits:0}))}},
          {field: "totQtyConverted", columnProps: {title: "Qty Converted", render: (text, record:any) => ( record.totQtyConverted.toLocaleString('en-US', {maximumFractionDigits:0}))}},
          {columnProps: {
            render: (text, record : any) => (
              <ReportsLink ordnum={record.id} bkrnum={record.bkrnum}/>
              )
            }
          }
        ]}
        onRowSelectionChange={this.handleRowSelectionChange}
        hideSelectionColumn={true}
        buttons={buttons}
      />
    );
  }

  handleRowSelectionChange = (selectedRowKeys: string[]) => {
    this.selectedRowKey = selectedRowKeys[0];
  };

  componentDidMount(): void {

    // to disable paging config pass 'true' as disabled param in function below
    this.paginationConfig = createPagingConfig(this.props.location.search);
    this.paginationConfig.onShowSizeChange = this.onPagingChange;
    this.paginationConfig.onChange = this.onPagingChange;
  }

  @action onPagingChange = (current: number, pageSize: number) => {
    this.props.history.push(
      addPagingParams("mergesList", current, pageSize)
    );
    this.paginationConfig = { ...this.paginationConfig, current, pageSize };
  };

}

const MergesList = injectIntl(MergesListComponent);

export default MergesList;

The important changes are:

  1. moved paginationConfig into here:
@observable paginationConfig: PaginationConfig = { ...defaultPagingConfig };
  1. added componentDidMount and @action onPagingChange:
 componentDidMount(): void {

    // to disable paging config pass 'true' as disabled param in function below
    this.paginationConfig = createPagingConfig(this.props.location.search);
    this.paginationConfig.onShowSizeChange = this.onPagingChange;
    this.paginationConfig.onChange = this.onPagingChange;
  }

  @action onPagingChange = (current: number, pageSize: number) => {
    this.props.history.push(
      addPagingParams("mergesList", current, pageSize)
    );
    this.paginationConfig = { ...this.paginationConfig, current, pageSize };
  };

Note that componentDidMount now sets onShowSizeChange and onChange to the onPagingChange action. This was NOT generated by Cuba Studio.

  1. Added this pagingConfiguration to the table properties in my DataTable:
      <DataTable
        dataCollection={this.dataCollection}
        canSelectRowByClick={true}
        tableProps={{ pagination: this.paginationConfig }}

I can confirm that the onPagingChange action now gets called. I can also confirm that size changes now work.

HOWEVER, clicking on the direct page or the next/previous page buttons are still broken. While the data does update to the correct page, the pagination buttons disappear and it goes to 1 page (no back, no forward, box with 1 only). Also, the table load spinner stays visible. Somehow on the re-render of the Ant Design Pagination object, the total row count changes from the correct value to 10. It is correct on the first execution, but wrong after that.

The spinner staying visible is even stranger, since the data on the screen does update. I would guess the spinner appears when the status of the DataCollection is not “DONE”. I can confirm that it is “DONE”, so that spinner should no longer be displayed.

I will continue to work this. I’m documenting this process in the hope that it is helpful. If anybody has any suggestions, please let me know! This is taking me days to debug, whereas somebody with some experience can probably tell me what is wrong in a few minutes.

ReactJS is EXTREMELY frustrating to work with. I know that is the future direction of Cuba/Jmix, but this may make me rethink my whole strategy. My entire business is now running on Cuba using Backend UI and it is fine. But so far, this Frontend UI is a nightmare.

Pagination and buttons now work. The final piece of the puzzle was to change DataTable declaration to this:

      <DataTable
        dataCollection={this.dataCollection}
        canSelectRowByClick={true}
        tableProps={{ pagination: { ...this.paginationConfig, total: this.dataCollection.count }}}

You can see that I am inserting the total number of rows from the DataCollection object. I’m sure this is because I’m providing the pagination myself. If I could figure out how to let Cuba do it, then this shouldn’t be necessary.

I am down to one issue. After pressing any page button or next/previous, I get a spinner in the middle of the table that never goes away. Debugging shows that it is coming from the table filters. It gets displayed while the filters are being set up. For some reason, though, when the loading phase is complete and the Spin component is no longer rendered, React does not remove it from the DOM.

It’s this piece of code in react-ui/index.js:

image

I know that this.loading is set to true while the filters get set up and does change to false when completed. I have put a breakpoint here and watched the antd.Spin object get created and get skipped. But, when it gets skipped, the spinner does not leave the screen!

Does anybody know of a situation where this might happen?

The spinner mystery is now fixed, although I don’t understand why this matters. I had mixed a React Spinner in with the Ant Design Spin components. Changing all components to Ant Design fixed the problem. They now appear and disappear as required.

This problem is now solved for my application. Of course, it is WAY too hard to do this. I must be doing something wrong.

Now I need to fix column sorting (React Datatable column sorting?)

Hi @eraskin,

I was finally able to look closely into this, sorry for delay. The problem is caused by this part in your initial code:

if (this.dataCollection.status !== "DONE") {
      if (this.dataCollection.status == "ERROR") {
        return (<Title level={1}>Can't locate user - log in failed.</Title>);
      }
      return <Spinner/>;
}

The problem is that whenever status goes to LOADING, the DataTable (which is below this code) gets unmounted, therefore losing its state. This causes both the pagination and sorting issue.

In fact, you don’t need to manually check for LOADING state, DataTable will display the spinner automatically.

It should work correctly if you replace this code with:

if (this.dataCollection.status == "ERROR") {
     return (<Title level={1}>Can't locate user - log in failed.</Title>);
}

More generally speaking, to debug the issues in customized components I’d suggest the following approach (this is how I tracked down the issue):

  • Check whether the problem occurs in the unmodified (freshly generated) component (in this case it doesn’t)
  • Remove modifications from your customized component until you are able to isolate the modification that causes the issue

In the end this will either allow to uncover the root cause or give us a clear indication what to look for if the problem is on our side.

Also, it will enable us to answer much faster.

Thank you! So simple when you know what you’re doing, right? :slight_smile: