import { useMemo, useRef, useCallback, useState, useEffect } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useDebounce, useDebouncedCallback, useThrottledCallback, } from "use-debounce"; import { useSnackbar } from "notistack"; import useMemoValue from "use-memo-value"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { isEmpty, isEqual } from "lodash-es"; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { useVirtual } from "react-virtual"; import { StyledTable } from "./Styled/StyledTable"; import { StyledRow } from "./Styled/StyledRow"; import { StyledResizer } from "./Styled/StyledResizer"; import ColumnHeaderComponent from "./Column"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; import { IconButton } from "@mui/material"; import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader"; import FinalColumnHeader from "./FinalColumnHeader"; import FinalColumn from "./formatters/FinalColumn"; // import TableRow from "./TableRow"; import EmptyState from "@src/components/EmptyState"; // import BulkActions from "./BulkActions"; import AddRow from "@src/components/TableToolbar/AddRow"; import { AddRow as AddRowIcon } from "@src/assets/icons"; import Loading from "@src/components/Loading"; import ContextMenu from "./ContextMenu"; import { projectScope, userRolesAtom, userSettingsAtom, } from "@src/atoms/projectScope"; import { tableScope, tableIdAtom, tableSettingsAtom, tableSchemaAtom, tableColumnsOrderedAtom, tableRowsAtom, tableNextPageAtom, tablePageAtom, updateColumnAtom, updateFieldAtom, selectedCellAtom, SelectedCell, } from "@src/atoms/tableScope"; import { COLLECTION_PAGE_SIZE } from "@src/config/db"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { FieldType } from "@src/constants/fields"; import { formatSubTableName } from "@src/utils/table"; import { TableRow, ColumnConfig } from "@src/types/table"; import { StyledCell } from "./Styled/StyledCell"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; export const MIN_COL_WIDTH = 80; export const TABLE_PADDING = 16; export const OUT_OF_ORDER_MARGIN = 8; export const DEBOUNCE_DELAY = 500; declare module "@tanstack/table-core" { interface ColumnMeta extends ColumnConfig {} } const columnHelper = createColumnHelper(); const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id; export default function TableComponent() { const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); const [tableId] = useAtom(tableIdAtom, tableScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const [tableRows] = useAtom(tableRowsAtom, tableScope); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope); const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); const updateColumn = useSetAtom(updateColumnAtom, tableScope); const updateField = useSetAtom(updateFieldAtom, tableScope); const containerRef = useRef(null); const gridRef = useRef(null); const canAddColumn = userRoles.includes("ADMIN"); const canEditColumn = userRoles.includes("ADMIN"); // Get column defs from table schema // Also add end column for admins const columns = useMemo(() => { const _columns = tableColumnsOrdered // Hide column for all users using table schema .filter((column) => !column.hidden) .map((columnConfig) => columnHelper.accessor(columnConfig.fieldName, { id: columnConfig.fieldName, meta: columnConfig, size: columnConfig.width, enableResizing: columnConfig.resizable !== false, minSize: MIN_COL_WIDTH, // draggable: true, // resizable: true, // frozen: columnConfig.fixed, // headerRenderer: ColumnHeader, // formatter: // getFieldProp("TableCell", getFieldType(columnConfig)) ?? // function InDev() { // return null; // }, // editor: // getFieldProp("TableEditor", getFieldType(columnConfig)) ?? // function InDev() { // return null; // }, // ...columnConfig, // editable: // tableSettings.readOnly && !userRoles.includes("ADMIN") // ? false // : columnConfig.editable ?? true, // width: columnConfig.width ?? DEFAULT_COL_WIDTH, }) ); if (canAddColumn || !tableSettings.readOnly) { _columns.push( columnHelper.display({ id: "_rowy_column_actions", header: () => "Actions", cell: () => ( <> M D X ), }) ); } return _columns; }, [tableColumnsOrdered, canAddColumn, tableSettings.readOnly]); // Get user’s hidden columns from user document const userDocHiddenFields = userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields; // Memoize into a VisibilityState const columnVisibility = useMemo(() => { if (!Array.isArray(userDocHiddenFields)) return {}; return userDocHiddenFields.reduce((a, c) => ({ ...a, [c]: false }), {}); }, [userDocHiddenFields]); // Get frozen columns const columnPinning = useMemo( () => ({ left: columns.filter((c) => c.meta?.fixed && c.id).map((c) => c.id!), }), [columns] ); const lastFrozen: string | undefined = columnPinning.left[columnPinning.left.length - 1]; // Call TanStack Table const table = useReactTable({ data: tableRows, columns, getCoreRowModel: getCoreRowModel(), getRowId, columnResizeMode: "onChange", }); const [columnSizing, setColumnSizing] = useState( table.initialState.columnSizing ); table.setOptions((prev) => ({ ...prev, state: { ...prev.state, columnVisibility, columnPinning, columnSizing }, onColumnSizingChange: setColumnSizing, })); useSaveColumnSizing(columnSizing, canEditColumn); const { rows } = table.getRowModel(); const leafColumns = table.getVisibleLeafColumns(); // console.log(table, selectedCell); const { virtualItems: virtualRows, totalSize: totalHeight, scrollToIndex: scrollToRowIndex, } = useVirtual({ parentRef: containerRef, size: tableRows.length, overscan: 10, paddingEnd: TABLE_PADDING, estimateSize: useCallback( (index: number) => (tableSchema.rowHeight || DEFAULT_ROW_HEIGHT) + (tableRows[index]._rowy_outOfOrder ? OUT_OF_ORDER_MARGIN : 0), [tableSchema.rowHeight, tableRows] ), }); const { virtualItems: virtualCols, totalSize: totalWidth, scrollToIndex: scrollToColIndex, } = useVirtual({ parentRef: containerRef, horizontal: true, size: leafColumns.length, overscan: 10, paddingStart: TABLE_PADDING, paddingEnd: TABLE_PADDING, estimateSize: useCallback( (index: number) => leafColumns[index].columnDef.size || DEFAULT_COL_WIDTH, [leafColumns] ), }); useEffect(() => { if (!selectedCell) return; if (selectedCell.path) { const rowIndex = tableRows.findIndex( (row) => row._rowy_ref.path === selectedCell.path ); if (rowIndex > -1) scrollToRowIndex(rowIndex); } if (selectedCell.columnKey) { const colIndex = leafColumns.findIndex( (col) => col.id === selectedCell.columnKey ); if (colIndex > -1) scrollToColIndex(colIndex); } }, [ selectedCell, tableRows, leafColumns, scrollToRowIndex, scrollToColIndex, ]); const { handleKeyDown, focusInsideCell } = useKeyboardNavigation({ gridRef, tableRows, leafColumns, }); const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0; const paddingBottom = virtualRows.length > 0 ? totalHeight - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0; const paddingLeft = virtualCols.length > 0 ? virtualCols?.[0]?.start || 0 : 0; const paddingRight = virtualCols.length > 0 ? totalWidth - (virtualCols?.[virtualCols.length - 1]?.end || 0) : 0; const fetchMoreOnBottomReached = useThrottledCallback( (containerElement?: HTMLDivElement | null) => { if (!containerElement) return; const { scrollHeight, scrollTop, clientHeight } = containerElement; if (scrollHeight - scrollTop - clientHeight < 300) { setTablePage((p) => p + 1); } }, DEBOUNCE_DELAY ); // Check on mount and after fetch to see if the table is at the bottom // for large screen heights useEffect(() => { fetchMoreOnBottomReached(containerRef.current); }, [fetchMoreOnBottomReached, tablePage, tableNextPage.loading]); return (
fetchMoreOnBottomReached(e.target as HTMLDivElement)} style={{ overflow: "auto", width: "100%", height: "100%" }} >
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const isSelectedCell = (!selectedCell && header.index === 0) || (selectedCell?.path === "_rowy_header" && selectedCell?.columnKey === header.id); return ( { setSelectedCell({ path: "_rowy_header", columnKey: header.id, }); (e.target as HTMLDivElement).focus(); }} > {header.column.getCanResize() && ( )} ); })} ))}
{paddingTop > 0 && (
)} {virtualRows.map((virtualRow) => { const row = rows[virtualRow.index]; const outOfOrder = row.original._rowy_outOfOrder; return ( {paddingLeft > 0 && (
)} {outOfOrder && } {virtualCols.map((virtualCell) => { const cellIndex = virtualCell.index; const cell = row.getVisibleCells()[cellIndex]; const isSelectedCell = selectedCell?.path === row.original._rowy_ref.path && selectedCell?.columnKey === cell.column.id; return ( { setSelectedCell({ path: row.original._rowy_ref.path, columnKey: cell.column.id, }); (e.target as HTMLDivElement).focus(); }} >
{flexRender( cell.column.columnDef.cell, cell.getContext() )}
{/* */}
); })} {paddingRight > 0 && (
)} ); })} {paddingBottom > 0 && (
)}
); }