import React, { useState, useCallback, useRef } from "react"; import classnames from "classnames"; import { Row } from "react-table"; import ReactTooltip from "react-tooltip"; import useDeepEffect from "hooks/useDeepEffect"; import SearchField from "components/forms/fields/SearchField"; // @ts-ignore import Pagination from "components/Pagination"; import Button from "components/buttons/Button"; import { ButtonVariant } from "components/buttons/Button/Button"; import DataTable from "./DataTable/DataTable"; import TableContainerUtils from "./TableContainerUtils"; import { IActionButtonProps } from "./DataTable/ActionButton"; export interface ITableQueryData { pageIndex: number; pageSize: number; searchQuery: string; sortHeader: string; sortDirection: string; } interface ITableContainerProps { columns: any; // TODO: Figure out type data: any; // TODO: Figure out type isLoading: boolean; manualSortBy?: boolean; defaultSortHeader?: string; defaultSortDirection?: string; actionButtonText?: string; actionButtonIcon?: string; actionButtonVariant?: ButtonVariant; hideActionButton?: boolean; inputPlaceHolder?: string; disableActionButton?: boolean; disableMultiRowSelect?: boolean; resultsTitle: string; resultsHtml?: JSX.Element; additionalQueries?: string; emptyComponent: React.ElementType; className?: string; showMarkAllPages: boolean; isAllPagesSelected: boolean; // TODO: make dependent on showMarkAllPages toggleAllPagesSelected?: any; // TODO: an event type and make it dependent on showMarkAllPages searchable?: boolean; wideSearch?: boolean; disablePagination?: boolean; disableNextPage?: boolean; // disableNextPage is a temporary workaround for the case // where the number of items on the last page is equal to the page size. // The old page controls for server-side pagination render a no results screen // with a back button. This fix instead disables the next button in that case. disableCount?: boolean; primarySelectActionButtonVariant?: ButtonVariant; primarySelectActionButtonIcon?: string; primarySelectActionButtonText?: string | ((targetIds: number[]) => string); secondarySelectActions?: IActionButtonProps[]; // TODO create table actions interface filteredCount?: number; searchToolTipText?: string; searchQueryColumn?: string; selectedDropdownFilter?: string; isClientSidePagination?: boolean; isClientSideFilter?: boolean; isMultiColumnFilter?: boolean; // isMultiColumnFilter is used to preserve the table headers // in lieu of displaying the empty component when client-side filtering yields zero results highlightOnHover?: boolean; pageSize?: number; onActionButtonClick?: () => void; onQueryChange?: (queryData: ITableQueryData) => void; onPrimarySelectActionClick?: (selectedItemIds: number[]) => void; customControl?: () => JSX.Element; stackControls?: boolean; onSelectSingleRow?: (value: Row) => void; filters?: Record; renderCount?: () => JSX.Element | null; renderFooter?: () => JSX.Element | null; setExportRows?: (rows: Row[]) => void; } const baseClass = "table-container"; const DEFAULT_PAGE_SIZE = 100; const DEFAULT_PAGE_INDEX = 0; const TableContainer = ({ columns, data, filters, isLoading, manualSortBy = false, defaultSortHeader = "name", defaultSortDirection = "asc", inputPlaceHolder = "Search", additionalQueries, resultsTitle, resultsHtml, emptyComponent, className, disableActionButton, disableMultiRowSelect = false, actionButtonText, actionButtonIcon, actionButtonVariant = "brand", hideActionButton, showMarkAllPages, isAllPagesSelected, toggleAllPagesSelected, searchable, wideSearch, disablePagination, disableNextPage, disableCount, primarySelectActionButtonVariant = "brand", primarySelectActionButtonIcon, primarySelectActionButtonText, secondarySelectActions, filteredCount, searchToolTipText, isClientSidePagination, isClientSideFilter, isMultiColumnFilter, highlightOnHover, pageSize = DEFAULT_PAGE_SIZE, selectedDropdownFilter, searchQueryColumn, onActionButtonClick, onQueryChange, onPrimarySelectActionClick, customControl, stackControls, onSelectSingleRow, renderCount, renderFooter, setExportRows, }: ITableContainerProps): JSX.Element => { const [searchQuery, setSearchQuery] = useState(""); const [sortHeader, setSortHeader] = useState(defaultSortHeader || ""); const [sortDirection, setSortDirection] = useState( defaultSortDirection || "" ); const [pageIndex, setPageIndex] = useState(DEFAULT_PAGE_INDEX); const [clientFilterCount, setClientFilterCount] = useState(); const prevPageIndex = useRef(0); const wrapperClasses = classnames(baseClass, className); const EmptyComponent = emptyComponent; const onSortChange = useCallback( (id?: string, isDesc?: boolean) => { if (id === undefined) { setSortHeader(defaultSortHeader || ""); setSortDirection(defaultSortDirection || ""); } else { setSortHeader(id); const direction = isDesc ? "desc" : "asc"; setSortDirection(direction); } }, [defaultSortHeader, defaultSortDirection, setSortHeader, setSortDirection] ); const onSearchQueryChange = (value: string) => { setSearchQuery(value); }; const hasPageIndexChangedRef = useRef(false); const onPaginationChange = useCallback( (newPage: number) => { setPageIndex(newPage); hasPageIndexChangedRef.current = true; }, [hasPageIndexChangedRef] ); const onResultsCountChange = (resultsCount: number) => { setClientFilterCount(resultsCount); }; useDeepEffect(() => { if (!onQueryChange) { return; } const queryData = { searchQuery, sortHeader, sortDirection, pageSize, pageIndex, }; if (prevPageIndex.current === pageIndex) { setPageIndex(0); } onQueryChange(queryData); prevPageIndex.current = pageIndex; }, [ searchQuery, sortHeader, sortDirection, pageSize, pageIndex, additionalQueries, ]); // TODO: refactor existing components relying on displayCount to use renderCount pattern const displayCount = useCallback((): any => { if (typeof filteredCount === "number") { return filteredCount; } else if (typeof clientFilterCount === "number") { return clientFilterCount; } return data?.length || 0; }, [filteredCount, clientFilterCount, data]); const renderPagination = useCallback(() => { if (disablePagination || isClientSidePagination) { return null; } return ( ); }, [ data, disablePagination, isClientSidePagination, disableNextPage, pageIndex, pageSize, onPaginationChange, ]); const opacity = isLoading ? { opacity: 0.4 } : { opacity: 1 }; return (
{wideSearch && searchable && (
)}
{renderCount && (
{renderCount()}
)} {!renderCount && !disableCount && (isMultiColumnFilter || displayCount()) ? (
{TableContainerUtils.generateResultsCountText( resultsTitle, displayCount() )} {resultsHtml}
) : (
)} {!hideActionButton && actionButtonText && ( )} {customControl && customControl()}
{/* Render search bar only if not empty component */} {searchable && !wideSearch && ( <>
{searchToolTipText} )}
{/* No entities for this result. */} {(!isLoading && data.length === 0 && !isMultiColumnFilter) || (searchQuery.length && data.length === 0 && !isMultiColumnFilter) ? ( <> {pageIndex !== 0 && (
)} ) : ( <> {/* TODO: Fix this hacky solution to clientside search being 0 rendering emptycomponent but no longer accesses rows.length because DataTable is not rendered */} {clientFilterCount === 0 && !isMultiColumnFilter && ( )}
)}
); }; export default TableContainer;