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], ); const pageCount = props.serverSidePaginationEnabled && props.totalRecordsCount ? Math.ceil(props.totalRecordsCount / props.pageSize) : 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;