import { fetch } from 'ducks/data' import { getFormInputs } from 'ducks/formInputs' import { IObject } from 'interfaces' import { connect } from 'react-redux' import { LIST } from '@adalo/constants' import { ContextConsumerComponent } from 'components/ContextConsumerComponent' import { getDependencies } from 'utils/dependencies' import { isDependencyForObject } from 'utils/dependencies/dataBindings' import { getInputsDifferent } from 'utils/dependencies/formInputs' const DEFAULT_PAGE_SIZE = 20 const PREFETCH_SECOND_PAGE = true type PaginatedObject = IObject & { dataBinding?: { options?: { manualPagination?: boolean; pageSize?: number } } } export type PaginatedFetchProps = { component: IObject object: PaginatedObject globalState: any formInputs: any fetch: undefined | ((...args: any[]) => Promise) active: boolean } type PaginatedFetchState = { currentPage: number page: number nextPageLoading: boolean tableWidth: number } const mapStateToProps = (state: any) => ({ globalState: state, formInputs: getFormInputs(state), }) export const connectPaginatedFetch = connect(mapStateToProps, { fetch }) class PaginatedFetchComponent< P extends PaginatedFetchProps > extends ContextConsumerComponent { state: Readonly = { currentPage: 0, page: 0, nextPageLoading: false, tableWidth: 0, } componentDidMount() { if (!this.isManuallyPaginated()) { return } const pageSize = this.getPageSize() this.fetchPageData(this.state.currentPage ?? 0, pageSize).then(() => { if (PREFETCH_SECOND_PAGE) { this.fetchPageData(1, pageSize) } }) } dependencyFiltersChanged(otherFormInputs: any) { const { component, formInputs } = this.props const inputsDifferent = getInputsDifferent( component, formInputs, otherFormInputs ) return inputsDifferent } componentDidUpdate(prevProps: P) { // If the filters on which the table depends are updated, we need to go to the first page and fetch the data again const filtersUpdated = this.dependencyFiltersChanged(prevProps.formInputs) if (filtersUpdated) { this.goToFirstPage() } else if (!prevProps.active && this.props.active) { // If the screen was inactive and now is active (like when navigating to a different screen and coming back to this one), // re-fetch the current page data this.fetchCurrentPageData() } } getPageSize() { const { object } = this.props const { pageSize } = object.dataBinding?.options || {} return pageSize || DEFAULT_PAGE_SIZE } getCurrentPage() { return this.state.currentPage ?? 0 } isOptionEnabled() { const { object } = this.props return Boolean(object.dataBinding?.options?.manualPagination) } isManuallyPaginated() { const { object, component, globalState } = this.props const { getApp, getDatasources, getParams } = this.context const deps = getDependencies(component, { getApp, getDatasources, getParams, state: globalState, }) const objectDependency = deps.find(dep => isDependencyForObject(object.id, dep) ) // External collections data is not manually paginated. // It should still be loaded and handled by the Screen, same as Lists const isExternalCollection = objectDependency?.isExternalCollection === true const isManuallyPaginated = !isExternalCollection const isOptionEnabled = this.isOptionEnabled() return object.type === LIST ? isOptionEnabled && isManuallyPaginated : isManuallyPaginated } goToFirstPage() { this.setState({ currentPage: 0 }) this.fetchCurrentPageData() } fetchPageData( page: number, limit = DEFAULT_PAGE_SIZE ): Promise { if (!this.isManuallyPaginated()) { // No manual fetching if the data is not manually paginated return Promise.resolve() } const { component, globalState, fetch, object } = this.props const { getBaseURL, getApp, getDatasources, getParams } = this.context if (!fetch) { throw new Error( `PaginatedFetchComponent is missing 'fetch' prop, make sure to use connectPaginatedFetch HOC` ) } const url = getBaseURL() const componentDependencies = getDependencies(component, { getDatasources, getApp, state: globalState, getParams, }) // Filter the dependencies to only the ones that are for the current object const objectDependencies = componentDependencies.filter(dep => isDependencyForObject(object.id, dep) ) const paginationOptions = { offset: page * limit, limit } const fetchPayloads = objectDependencies.map(dep => ({ ...dep, ...paginationOptions, })) return Promise.all(fetchPayloads.map(async payload => fetch(url, payload))) } fetchCurrentPageData() { const page = this.getCurrentPage() const pageSize = this.getPageSize() return this.fetchPageData(page, pageSize).then(() => { // Next page data needs to be re-fetched too to know if we should show the "next" button this.fetchPageData(page + 1, pageSize) }) } } export default PaginatedFetchComponent