All Downloads are FREE. Search and download functionalities are using the official Maven repository.

components.table.LargeTable.tsx Maven / Gradle / Ivy

import _                               from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useNotifier }                 from 'hooks/notification'
import { useCustomRef }                from 'hooks/customref'
import { useTranslator }               from 'hooks/translator'
import Report                          from 'helpers/report'
import { useDebouncedState }           from 'utils/utils'
import { toRegex }                     from 'utils/option-utils'
import { Box, Typography }             from '@mui/material'
import PageStats                       from 'components/table/page/PageStats'
import MenuBar                         from 'components/table/page/MenuBar'
import LoadingTable                    from 'components/table/LoadingTable'

type LargeTableProps = {
  description: string
  // rendering table data
  columns: { headerName: string, field: string, cellProps: any }[]
  dataLoader: DataLoader
  // styling
  containerSx?: any
  tableSx?: any
  largeTableSx?: any
  // configurations
  pageSize?: number
  filterColumns?: string[]
  filterType?: FilterType
  disablePaging?: boolean
  disableFilter?: boolean
  disableReload?: boolean
  disableToolbar?: boolean
}

type DataLoader = (
  page: Page,
  errorCallback: (message: string, reason: string) => void,
  dataCallback: (data: Data) => void,
) => Cancellable

type Data = {
  rows: any[]             // the filtered rows
  count: number           // the total number of rows
  page: Page
  //pageCount: number // TODO misleading name; this apparently is the number of rows before local filtering
}

type Page = {
  pageNum: number         // the current page number
  pageSize: number        // the number of rows per page
  filter: string
  filterType: FilterType
  action: Action
}

type FilterType = 'remote' | 'local'

type Action = 'main' | 'reload' | 'first' | 'previous' | 'next' | 'last' | 'filter'

type Status = {
  busy: boolean
  with?: Action
  request?: Cancellable
}

type Cancellable = {
  cancel: () => void
}

type Setter = (value: T) => void

const LargeTable = ({
  description,
  // rendering table data
  columns = [],
  dataLoader,
  // styling
  containerSx = {},
  tableSx     = {},
  largeTableSx = {},
  // configurations
  pageSize: givenPageSize = 100,
  filterType = 'remote',
  filterColumns = [],
  disableFilter,
  disablePaging,
  disableReload,
  disableToolbar
}: LargeTableProps) => {
  const pageSize = disablePaging && 999999 || givenPageSize
  const {translator} = useTranslator()
  const notifier = useNotifier()
  const [page, setPage] = useState({ pageNum: 0, pageSize, filter: "", filterType, action: 'main' })
  const [data, setData] = useState({ rows: [], count: 0, page: {...page, pageNum: 0 }, /*pageCount: -1*/ })
  const [pageable, setPageable] = useState(false)
  const [filter, setFilter, setCurrentFilter] = useDebouncedState('', 500)
  const tableRef = useRef(null)
  const [status, setStatus] = useState({ busy: false })
  const busyWith = (action: Action): boolean => status.busy && status.with === action

  const activeFilterColumns = filterColumns.length === 0 ? columns.filter(column => column.headerName).map(column => column.field) : filterColumns
  const toolbarFunctions = createToolbarFunctions(data, status, setStatus, setPage)
  const showToolbar = !disableToolbar && ((!disablePaging && pageable) || !disableFilter)

  // add functions to be visible from outside of this component
  const customRef = useCustomRef()
  customRef?.setCurrent({
    deleteLine: (id: string) => setData(data => ({...data, rows: data.rows.filter((row: any) => row.id != id)}))
  })

  // perform a request when the request info changes
  useEffect(() => {
    const pageChanged = data.page !== page
    if (pageChanged || page.action === 'reload') {
      const loadingDone = () => {
        setStatus({ busy: false, with: undefined })
      }
      const errorHandler = (message: string, reason: string) => {
        loadingDone()
        console.error(message, reason)
        Report.from(message, translator, { category: Report.backend }).addToNotifier(notifier)
      }
      const dataHandler = (result: any) => {
        loadingDone()
        console.log("LargeTable: handle data for %s: result=%o", description, result)
        const rows = filterType === 'local' ? filterRows(result.rows, activeFilterColumns, page.filter) : result.rows
        setData({ ...result, rows, page })
      }

      status.request?.cancel() // cancel a running request, if any
      console.debug("LargeTable: loading data for page=%o, data.page=%o", page, data.page)
      const request = dataLoader(page, errorHandler, dataHandler)
      setStatus({ busy: true, with: page.action, request })
    }
  },[page])

  // instigate a page change when the filter changes
  useEffect( () => {
    if (filter != page.filter) {
      setPage({...page, pageNum: 0, filter, action: 'filter' })
    }
  }, [filter])

  // active pagination buttons
  useEffect( () => {
    const hasCount = Boolean(data.count >= 0)
    const enablePaging = data.page.pageNum > 0 ||
      (hasCount && data.count > data.page.pageSize) /*||
      (!hasCount && data.pageCount === data.page.pageSize*)*/

    if (!pageable && enablePaging && !disablePaging)
      setPageable(enablePaging)

    // scroll to top
    if (tableRef.current) {
      // @ts-ignore
      tableRef.current.scrollTo({top: 0, left: 0})
    }
  }, [data])

  // TODO ref={tableRef} on LoadingTable
  return (
    
      
      
          
      
      
      
    
  )
}

const EmptyTableMessage = ({message, enable}: {message?: string, enable: boolean}) => {
  const {t} = useTranslator()
  if (!enable) return null
  return (
    
      
        {message || t('table.empty')}
      
    
  )
}

function filterRows(rows: any[], filterColumns: string[], filter: string) {
  if (!filter) return rows

  const regex = toRegex(filter)
  const check = (value: string) => regex.test(value)
  const filterRow = (row: any)  => filterColumns.some(column   => check(_.get(row.cells, column)))

  return rows.filter(filterRow)
}

const createToolbarFunctions = (data: any, status: Status, setStatus: Setter, setPage: Setter) => {
  const enabled  = data.page.pageNum >= 0
  const hasCount = data?.count >= 0
  const lastPage = Math.floor(Number(data.count) / Number(data.page.pageSize)) + (Number(data.count) % Number(data.page.pageSize) === 0 ? -1 : 0)

  return {
    reload: data.page.pageNum >= 0
      ? () => {
        console.log("Reloaded page: %o", data.page)
        setPage({...data.page, action: 'reload'})
      }
      : undefined,

    filter: () => {
      if (status.busy) setStatus({ busy: false, with: undefined })
    },

    first: enabled && data.page.pageNum > 0
      ? () => {
          setPage({...data.page, pageNum: 0, action: 'first'})
        }
      : undefined,

    last: enabled && hasCount && lastPage > data.page.pageNum
      ? () => {
          setPage({...data.page, pageNum: lastPage, action: 'last'})
        }
      : undefined,

    previous: enabled && data.page.pageNum > 0
      ? () => {
          setPage({...data.page, pageNum: data.page.pageNum - 1, action: 'previous'})
        }
      : undefined,

    next: enabled && (
        (hasCount  && data.count > (data.rows.length + data.page.pageNum * data.page.pageSize) ||
        (!hasCount && data.rows.length % data.page.pageSize === 0))
      )
      ? () => {
          setPage({...data.page, pageNum: data.page.pageNum + 1, action: 'next'})
        }
      : undefined,
  }
}

export default LargeTable




© 2015 - 2024 Weber Informatics LLC | Privacy Policy