import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { reduce } from "lodash"; import type { Row as ReactTableRowType } from "react-table"; import { useTable, usePagination, useBlockLayout, useResizeColumns, useRowSelect, } from "react-table"; import { useSticky } from "react-table-sticky"; import { TableWrapper, TableHeaderWrapper, TableHeaderInnerWrapper, } from "./TableStyledWrappers"; import TableHeader from "./header"; import { Classes } from "@blueprintjs/core"; import type { ReactTableColumnProps, ReactTableFilter, CompactMode, AddNewRowActions, StickyType, } from "./Constants"; import { TABLE_SIZES, CompactModeTypes, TABLE_SCROLLBAR_HEIGHT, } from "./Constants"; import { Colors } from "constants/Colors"; import type { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { EditableCell, TableVariant } from "../constants"; import SimpleBar from "simplebar-react"; import "simplebar-react/dist/simplebar.min.css"; import { createGlobalStyle } from "styled-components"; import { Classes as PopOver2Classes } from "@blueprintjs/popover2"; import StaticTable from "./StaticTable"; import VirtualTable from "./VirtualTable"; import fastdom from "fastdom"; const SCROLL_BAR_OFFSET = 2; const HEADER_MENU_PORTAL_CLASS = ".header-menu-portal"; const PopoverStyles = createGlobalStyle<{ widgetId: string; borderRadius: string; }>` ${HEADER_MENU_PORTAL_CLASS}-${({ widgetId }) => widgetId} { font-family: var(--wds-font-family) !important; & .${PopOver2Classes.POPOVER2}, .${PopOver2Classes.POPOVER2_CONTENT}, .bp3-menu { border-radius: ${({ borderRadius }) => borderRadius >= `1.5rem` ? `0.375rem` : borderRadius} !important; } } `; export interface TableProps { width: number; height: number; pageSize: number; widgetId: string; widgetName: string; searchKey: string; isLoading: boolean; columnWidthMap?: { [key: string]: number }; columns: ReactTableColumnProps[]; data: Array>; totalRecordsCount?: number; editMode: boolean; editableCell: EditableCell; sortTableColumn: (columnIndex: number, asc: boolean) => void; handleResizeColumn: (columnWidthMap: { [key: string]: number }) => void; handleReorderColumn: (columnOrder: string[]) => void; selectTableRow: (row: { original: Record; index: number; }) => void; pageNo: number; updatePageNo: (pageNo: number, event?: EventType) => void; multiRowSelection?: boolean; isSortable?: boolean; nextPageClick: () => void; prevPageClick: () => void; serverSidePaginationEnabled: boolean; selectedRowIndex: number; selectedRowIndices: number[]; disableDrag: () => void; enableDrag: () => void; toggleAllRowSelect: ( isSelect: boolean, pageData: ReactTableRowType>[], ) => void; triggerRowSelection: boolean; searchTableData: (searchKey: any) => void; filters?: ReactTableFilter[]; applyFilter: (filters: ReactTableFilter[]) => void; compactMode?: CompactMode; isVisibleDownload?: boolean; isVisibleFilters?: boolean; isVisiblePagination?: boolean; isVisibleSearch?: boolean; delimiter: string; accentColor: string; borderRadius: string; boxShadow: string; borderWidth?: number; borderColor?: string; onBulkEditDiscard: () => void; onBulkEditSave: () => void; variant?: TableVariant; primaryColumnId?: string; isAddRowInProgress: boolean; allowAddNewRow: boolean; onAddNewRow: () => void; onAddNewRowAction: ( type: AddNewRowActions, onActionComplete: () => void, ) => void; disabledAddNewRowSave: boolean; handleColumnFreeze?: (columnName: string, sticky?: StickyType) => void; canFreezeColumn?: boolean; } const defaultColumn = { minWidth: 30, width: 150, }; export type HeaderComponentProps = { enableDrag: () => void; disableDrag: () => void; multiRowSelection?: boolean; handleAllRowSelectClick: ( e: React.MouseEvent, ) => void; handleReorderColumn: (columnOrder: string[]) => void; columnOrder?: string[]; accentColor: string; borderRadius: string; headerGroups: any; canFreezeColumn?: boolean; editMode: boolean; handleColumnFreeze?: (columnName: string, sticky?: StickyType) => void; isResizingColumn: React.MutableRefObject; isSortable?: boolean; sortTableColumn: (columnIndex: number, asc: boolean) => void; columns: ReactTableColumnProps[]; width: number; subPage: ReactTableRowType>[]; prepareRow: any; headerWidth?: number; rowSelectionState: 0 | 1 | 2 | null; widgetId: string; }; const emptyArr: any = []; export function Table(props: TableProps) { const isResizingColumn = React.useRef(false); const handleResizeColumn = (columnWidths: Record) => { const columnWidthMap = { ...props.columnWidthMap, ...columnWidths, }; for (const i in columnWidthMap) { if (columnWidthMap[i] < 60) { columnWidthMap[i] = 60; } else if (columnWidthMap[i] === undefined) { const columnCounts = props.columns.filter( (column) => !column.isHidden, ).length; columnWidthMap[i] = props.width / columnCounts; } } props.handleResizeColumn(columnWidthMap); }; const { columns, data, multiRowSelection, toggleAllRowSelect } = props; const tableHeadercolumns = React.useMemo( () => columns.filter((column: ReactTableColumnProps) => { return column.alias !== "actions"; }), [columns], ); /* For serverSidePaginationEnabled we are taking props.data.length as the page size. As props.pageSize is being set by the visible number of rows in the table (without scrolling), it will not give the correct count of records in the current page when query limit is set higher/lower than the visible number of rows in the table */ const pageCount = props.serverSidePaginationEnabled && props.totalRecordsCount && props.data.length ? Math.ceil(props.totalRecordsCount / props.data.length) : Math.ceil(props.data.length / props.pageSize); const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0; const { getTableBodyProps, getTableProps, headerGroups, page, pageOptions, prepareRow, state, totalColumnsWidth, } = useTable( { //columns and data needs to be memoised as per useTable specs columns, data, defaultColumn, initialState: { pageIndex: currentPageIndex, pageSize: props.pageSize, }, manualPagination: true, pageCount, }, useBlockLayout, useResizeColumns, usePagination, useRowSelect, useSticky, ); //Set isResizingColumn as true when column is resizing using table state if (state.columnResizing.isResizingColumn) { isResizingColumn.current = true; } else { // We are updating column size since the drag is complete when we are changing value of isResizing from true to false if (isResizingColumn.current) { //clear timeout logic //update isResizingColumn in next event loop so that dragEnd event does not trigger click event. setTimeout(function () { isResizingColumn.current = false; handleResizeColumn(state.columnResizing.columnWidths); }, 0); } } let startIndex = currentPageIndex * props.pageSize; let endIndex = startIndex + props.pageSize; if (props.serverSidePaginationEnabled) { startIndex = 0; endIndex = props.data.length; } const subPage = useMemo( () => page.slice(startIndex, endIndex), [page, startIndex, endIndex], ); const selectedRowIndices = props.selectedRowIndices || emptyArr; const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT]; const tableWrapperRef = useRef(null); const scrollBarRef = useRef(null); const tableHeaderWrapperRef = React.createRef(); const rowSelectionState = React.useMemo(() => { // return : 0; no row selected | 1; all row selected | 2: some rows selected if (!multiRowSelection) return null; const selectedRowCount = reduce( page, (count, row) => { return selectedRowIndices.includes(row.index) ? count + 1 : count; }, 0, ); const result = selectedRowCount === 0 ? 0 : selectedRowCount === page.length ? 1 : 2; return result; }, [multiRowSelection, page, selectedRowIndices]); const handleAllRowSelectClick = useCallback( (e: React.MouseEvent) => { // if all / some rows are selected we remove selection on click // else select all rows toggleAllRowSelect(!Boolean(rowSelectionState), page); // loop over subPage rows and toggleRowSelected if required e.stopPropagation(); }, [page, rowSelectionState, toggleAllRowSelect], ); const isHeaderVisible = props.isVisibleSearch || props.isVisibleFilters || props.isVisibleDownload || props.isVisiblePagination || props.allowAddNewRow; const scrollContainerStyles = useMemo(() => { return { height: isHeaderVisible ? props.height - tableSizes.TABLE_HEADER_HEIGHT - TABLE_SCROLLBAR_HEIGHT - SCROLL_BAR_OFFSET : props.height - TABLE_SCROLLBAR_HEIGHT - SCROLL_BAR_OFFSET, }; }, [isHeaderVisible, props.height, tableSizes.TABLE_HEADER_HEIGHT]); const shouldUseVirtual = props.serverSidePaginationEnabled && !props.columns.some( (column) => !!column.columnProperties.allowCellWrapping, ); useEffect(() => { if (props.isAddRowInProgress) { fastdom.mutate(() => { if (scrollBarRef && scrollBarRef?.current) { scrollBarRef.current.getScrollElement().scrollTop = 0; } }); } }, [props.isAddRowInProgress]); return ( {isHeaderVisible && ( )}
{!shouldUseVirtual && ( )} {shouldUseVirtual && ( )}
); } export default Table;