import { IApp, IObject } from 'interfaces' import { throttle } from 'lodash' import PropTypes from 'prop-types' import React from 'react' import { LayoutChangeEvent, StyleProp, StyleSheet, View, ViewStyle, } from 'react-native' import { BaseObjectProps } from '../BaseObject' import { TableContextProvider } from './context' import EmptyState from './EmptyState' import FixedColumn from './FixedColumn' import LoadingIndicator from './LoadingIndicator' import PaginatedFetchComponent, { connectPaginatedFetch, PaginatedFetchProps, } from './PaginatedFetchComponent' import PaginationControls from './PaginationControls' import TableHeader from './TableHeader' import TableRow, { Row } from './TableRow' import TableScroll from './TableScroll' import type { BindingDataItem, CellData } from './TableTypes' import { makeTheme, type TableTheme } from './theme' import { getBindingData, getShouldFreezeFirstColumn } from './utils' interface TableObjectProps extends BaseObjectProps { bindingData: BindingDataItem[] object: IObject getApp: () => IApp } type TableData = { id: number cells: CellData[] }[] const DEFAULT_PAGE_LIMIT = 10 const MAX_REQUESTS_PER_SECOND = 7 const NEXT_PAGE_THROTTLE = Math.floor(1000 / MAX_REQUESTS_PER_SECOND) type Props = TableObjectProps & PaginatedFetchProps class Table extends PaginatedFetchComponent { constructor(props: Props) { super(props) this.goToNextPage = throttle( this.goToNextPage.bind(this), NEXT_PAGE_THROTTLE ) this.goToPreviousPage = throttle( this.goToPreviousPage.bind(this), NEXT_PAGE_THROTTLE ) } static childContextTypes = { getBindings: PropTypes.func, getStore: PropTypes.func, getParams: PropTypes.func, } getPageSize() { const { object } = this.props const { attributes } = object const pageSize = attributes?.pageSize ?? DEFAULT_PAGE_LIMIT return pageSize } getCurrentPage() { return this.state.page } getTableColumns() { const { bindingData } = this.props if (bindingData) { const { object } = this.props const { columns } = getBindingData(object, bindingData) return columns } return [] } getTableDataRowsForPage(page: number, pageSize: number): TableData { const { bindingData } = this.props if (bindingData) { const { object } = this.props const { data } = getBindingData(object, bindingData) let pageData = data if (this.isManuallyPaginated()) { pageData = data.slice(page * pageSize, (page + 1) * pageSize) } return pageData } return [] } goToFirstPage(): void { this.setState({ page: 0, nextPageLoading: true }) const pageSize = this.getPageSize() this.fetchPageData(0, pageSize).then(() => { // When fetching data from the first page, the data in the store gets reset, // so we need to prefetch the next page of data to make sure we know if the next page is available. this.fetchPageData(1, pageSize).finally(() => this.setState({ nextPageLoading: false }) ) }) } getTableDataRows() { const { page } = this.state const pageSize = this.getPageSize() return this.getTableDataRowsForPage(page, pageSize) } goToPreviousPage() { const { page } = this.state if (page !== 0) { const previousPage = page - 1 const pageSize = this.getPageSize() this.setState({ page: previousPage }) // Re-fetch the current page, and fetch the next page again to keep "next" enabled this.fetchPageData(previousPage, pageSize).then(() => { // Fetching the next page should happen after fetching the current page is done this.fetchPageData(page, pageSize) }) } } goToNextPage() { const { page } = this.state const { object } = this.props const { dataBinding } = object if (!dataBinding) { throw new Error( `Tried to fetch data from Table without dataBinding. Object: ${object.id}` ) } const pageSize = this.getPageSize() const nextPage = page + 1 const nextPageDataLoaded = this.getTableDataRowsForPage(nextPage, pageSize) this.setState({ nextPageLoading: true }) if (nextPageDataLoaded.length === 0) { // Only re-fetch the current page if we don't have the data loaded in the state this.fetchPageData(nextPage, pageSize) } this.fetchPageData(nextPage + 1, pageSize) this.setState({ page: nextPage }) // Clear the loading state after a short delay. This is just an artificial delay to make the loading state visible for a short time. const clearNextPageLoading = () => this.setState({ nextPageLoading: false }) setTimeout(clearNextPageLoading, NEXT_PAGE_THROTTLE / 2) } renderPaginationControls(theme: TableTheme) { // Only render paginated controls for manually paginated tables (external collections aren't "manually" paginated) if (!this.isManuallyPaginated()) { return null } const { page, nextPageLoading } = this.state const pageSize = this.getPageSize() const nextPageData = this.getTableDataRowsForPage(page + 1, pageSize) const nextEnabled = !nextPageLoading && nextPageData.length > 0 return ( ) } renderEmptyState(data: TableData, theme: TableTheme) { // if no attempt has been made to populate bindingData // then it's undefined, meaning we can differentiate // between an empty table and a table that hasn't loaded yet const hasNoBindingData = typeof this.props.bindingData === 'undefined' if (hasNoBindingData === true) { return } else if (hasNoBindingData === false && data.length === 0) { const { object } = this.props return } return null } getDatasource = () => { const { object } = this.props const { dataBinding } = object return dataBinding?.source } onUpdateTableWidth = (ev: LayoutChangeEvent) => { const { width } = ev.nativeEvent.layout this.setState({ tableWidth: width }) } render() { const data = this.getTableDataRows() const columns = this.getTableColumns() const datasource = this.getDatasource() if (!datasource) { return null } const { object, isResponsiveComponent, getApp } = this.props const theme = makeTheme(getApp(), object) const emptyState = this.renderEmptyState(data, theme) const tableHasData = emptyState === null const fixedFirstColumn = getShouldFreezeFirstColumn(object) const borderlessTableWidth = this.state.tableWidth - theme.tableContainer.borderWidth * 2 const wrapperStyles: StyleProp = [ styles.tableContainer, theme.tableContainer, isResponsiveComponent === false && object.layout, ] const innerStyles: StyleProp = [ styles.tableContainerInner, theme.tableContainerInner, ] return ( {fixedFirstColumn && tableHasData && ( )} {tableHasData && data.map((rowData: Row) => ( ))} {emptyState} {this.renderPaginationControls(theme)} ) } } const styles = StyleSheet.create({ tableContainer: { display: 'flex', flexDirection: 'column', }, tableContainerInner: { width: '100%', overflow: 'hidden', }, }) // @ts-expect-error https://github.com/AdaloHQ/runner/pull/873/files#r1656132138 export default connectPaginatedFetch(Table)