diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 892eabf9..e512fda8 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -3,6 +3,7 @@ import { useMemo, useRef, useState, useEffect, useCallback } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useThrottledCallback } from "use-debounce"; import { + RowSelectionState, createColumnHelper, getCoreRowModel, useReactTable, @@ -33,6 +34,7 @@ import { selectedCellAtom, tableSortsAtom, tableIdAtom, + serverDocCountAtom, } from "@src/atoms/tableScope"; import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; import { getFieldType, getFieldProp } from "@src/components/fields"; @@ -42,6 +44,7 @@ import { useSaveColumnSizing } from "./useSaveColumnSizing"; import useHotKeys from "./useHotKey"; import type { TableRow, ColumnConfig } from "@src/types/table"; import useStateWithRef from "./useStateWithRef"; // testing with useStateWithRef +import { Checkbox, FormControlLabel } from "@mui/material"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -75,6 +78,20 @@ export interface ITableProps { * Loading state handled by Suspense in parent component. */ emptyState?: React.ReactNode; + /** + * If defined, it will show a checkbox to select rows. The + * state is to be maintained by the parent component. + * + * Usage: + * + * const [selectedRows, setSelectedRows] = useState({}); + * const selectedRowsProp = useMemo(() => ({state: selectedRows, setState: setSelectedRows}), [selectedRows, setSelectedRows]) + * + */ + selectedRows?: { + state: RowSelectionState; + setState: React.Dispatch>; + }; } /** @@ -93,8 +110,10 @@ export default function Table({ canEditCells, hiddenColumns, emptyState, + selectedRows, }: ITableProps) { const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const [serverDocCount] = useAtom(serverDocCountAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const [tableRows] = useAtom(tableRowsAtom, tableScope); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); @@ -143,7 +162,57 @@ export default function Table({ } return _columns; - }, [tableColumnsOrdered, canAddColumns, canEditCells]); + }, [tableColumnsOrdered, canAddColumns, canEditCells, selectedRows]); + + columns.unshift( + ...useMemo(() => { + if (!selectedRows) return []; + + return [ + columnHelper.display({ + id: "_rowy_select", + size: 41.8, // TODO: We shouldn't have to change this often + header: ({ table }) => { + const checked = + Object.keys(selectedRows.state).length >= serverDocCount!; + const indeterminate = Object.keys(selectedRows.state).length > 0; + return ( + { + table.toggleAllRowsSelected( + !table.getIsAllRowsSelected() + ); + }} + /> + } + /> + ); + }, + cell: ({ row }) => { + return ( + + } + /> + ); + }, + }), + ]; + }, [selectedRows]) + ); // Get user’s hidden columns from props and memoize into a `VisibilityState` const columnVisibility: VisibilityState = useMemo(() => { @@ -151,14 +220,16 @@ export default function Table({ return hiddenColumns.reduce((a, c) => ({ ...a, [c]: false }), {}); }, [hiddenColumns]); - // Get frozen columns and memoize into a `ColumnPinningState` const columnPinning: ColumnPinningState = useMemo( () => ({ - left: columns - .filter( - (c) => c.meta?.fixed && c.id && columnVisibility[c.id] !== false - ) - .map((c) => c.id!), + left: [ + ...(selectedRows ? ["_rowy_select"] : []), + ...columns + .filter( + (c) => c.meta?.fixed && c.id && columnVisibility[c.id] !== false + ) + .map((c) => c.id!), + ], }), [columns, columnVisibility] ); @@ -172,6 +243,14 @@ export default function Table({ getCoreRowModel: getCoreRowModel(), getRowId, columnResizeMode: "onChange", + ...(selectedRows && { + enableRowSelection: true, + enableMultiRowSelection: true, + state: { + rowSelection: selectedRows.state, + }, + onRowSelectionChange: selectedRows.setState, + }), }); // Store local `columnSizing` state so we can save it to table schema @@ -213,11 +292,13 @@ export default function Table({ updateColumn({ key: result.draggableId, - index: result.destination.index, + index: selectedRows + ? result.destination.index - 1 + : result.destination.index, config: {}, }); }, - [updateColumn] + [updateColumn, selectedRows] ); const fetchMoreOnBottomReached = useThrottledCallback( diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index b50fcdf6..04918f18 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -1,6 +1,11 @@ import { memo } from "react"; import { useAtom } from "jotai"; -import type { Column, Row, ColumnSizingState } from "@tanstack/react-table"; +import { + Column, + Row, + ColumnSizingState, + flexRender, +} from "@tanstack/react-table"; import StyledRow from "./Styled/StyledRow"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; @@ -18,6 +23,7 @@ import { getFieldProp } from "@src/components/fields"; import type { TableRow } from "@src/types/table"; import useVirtualization from "./useVirtualization"; import { DEFAULT_ROW_HEIGHT, OUT_OF_ORDER_MARGIN } from "./Table"; +import StyledCell from "./Styled/StyledCell"; export interface ITableBodyProps { /** @@ -114,6 +120,14 @@ export const TableBody = memo(function TableBody({ const isReadOnlyCell = fieldTypeGroup === "Auditing" || fieldTypeGroup === "Metadata"; + if (cell.id.includes("_rowy_select")) { + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + } + return ( + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + // Render later, after the drag & drop placeholder if (header.id === "_rowy_column_actions") return ( diff --git a/src/components/Table/useVirtualization.tsx b/src/components/Table/useVirtualization.tsx index cd50acc6..eb423180 100644 --- a/src/components/Table/useVirtualization.tsx +++ b/src/components/Table/useVirtualization.tsx @@ -71,7 +71,8 @@ export function useVirtualization( const definedWidth = localWidth || schemaWidth; if (definedWidth === undefined) return DEFAULT_COL_WIDTH; - if (definedWidth < MIN_COL_WIDTH) return MIN_COL_WIDTH; + if (definedWidth < MIN_COL_WIDTH && columnDef.id !== "_rowy_select") + return MIN_COL_WIDTH; return definedWidth; }, [leafColumns, columnSizing] diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index 5845e185..77eb8749 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -1,7 +1,7 @@ import { lazy, Suspense } from "react"; import { useAtom, useSetAtom } from "jotai"; -import { Button, Stack } from "@mui/material"; +import { Button, Stack, Tooltip, Typography } from "@mui/material"; import WebhookIcon from "@mui/icons-material/Webhook"; import { Export as ExportIcon, @@ -33,10 +33,14 @@ import { tableSchemaAtom, tableModalAtom, tableSortsAtom, + serverDocCountAtom, + deleteRowAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; import { TableToolsType } from "@src/types/table"; import FilterIcon from "@mui/icons-material/FilterList"; +import DeleteIcon from "@mui/icons-material/DeleteOutlined"; +import { RowSelectionState } from "@tanstack/react-table"; // prettier-ignore const Sort = lazy(() => import("./Sort" /* webpackChunkName: "Filters" */)); @@ -51,10 +55,75 @@ const ReExecute = lazy(() => import("./ReExecute" /* webpackChunkName: "ReExecut export const TABLE_TOOLBAR_HEIGHT = 44; +const StyledStack = ({ children }: React.PropsWithChildren) => ( + `max(env(safe-area-inset-left), ${theme.spacing(2)})`, + pb: 1.5, + height: TABLE_TOOLBAR_HEIGHT, + scrollbarWidth: "thin", + overflowX: "auto", + "&": { overflowX: "overlay" }, + overflowY: "hidden", + "& > *": { flexShrink: 0 }, + + "& > .end-spacer": { + width: (theme) => + `max(env(safe-area-inset-right), ${theme.spacing(2)})`, + height: "100%", + ml: 0, + }, + }} + > + {children} + +); + +function RowSelectedToolBar({ + selectedRows, + resetSelectedRows, +}: { + selectedRows: RowSelectionState; + resetSelectedRows: () => void; +}) { + const [serverDocCount] = useAtom(serverDocCountAtom, tableScope); + const deleteRow = useSetAtom(deleteRowAtom, tableScope); + + const handleDelete = async () => { + await deleteRow({ path: Object.keys(selectedRows) }); + resetSelectedRows(); + }; + + return ( + + + {Object.values(selectedRows).length} of {serverDocCount} rows selected + + + + + + ); +} + export default function TableToolbar({ disabledTools, + selectedRows, + resetSelectedRows, }: { disabledTools?: TableToolsType[]; + selectedRows?: RowSelectionState; + resetSelectedRows?: () => void; }) { const [projectSettings] = useAtom(projectSettingsAtom, projectScope); const [userRoles] = useAtom(userRolesAtom, projectScope); @@ -77,29 +146,17 @@ export default function TableToolbar({ tableSchema.compiledExtension.replace(/\W/g, "")?.length > 0; disabledTools = disabledTools ?? []; - return ( - `max(env(safe-area-inset-left), ${theme.spacing(2)})`, - pb: 1.5, - height: TABLE_TOOLBAR_HEIGHT, - scrollbarWidth: "thin", - overflowX: "auto", - "&": { overflowX: "overlay" }, - overflowY: "hidden", - "& > *": { flexShrink: 0 }, - "& > .end-spacer": { - width: (theme) => - `max(env(safe-area-inset-right), ${theme.spacing(2)})`, - height: "100%", - ml: 0, - }, - }} - > + if (selectedRows && Object.keys(selectedRows).length > 0 && resetSelectedRows) + return ( + + ); + + return ( + {tableSettings.isCollection === false ? ( ) : ( @@ -202,6 +259,6 @@ export default function TableToolbar({ )}
- + ); } diff --git a/src/pages/Table/ProvidedArraySubTablePage.tsx b/src/pages/Table/ProvidedArraySubTablePage.tsx index 56efae4b..ee375154 100644 --- a/src/pages/Table/ProvidedArraySubTablePage.tsx +++ b/src/pages/Table/ProvidedArraySubTablePage.tsx @@ -139,6 +139,7 @@ export default function ProvidedArraySubTablePage() { - + diff --git a/src/pages/Table/ProvidedTablePage.tsx b/src/pages/Table/ProvidedTablePage.tsx index cdb7d19c..6adc676f 100644 --- a/src/pages/Table/ProvidedTablePage.tsx +++ b/src/pages/Table/ProvidedTablePage.tsx @@ -141,7 +141,7 @@ export default function ProvidedTablePage() { } >
- +
{outlet} diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 421be174..c9286821 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy } from "react"; +import { Suspense, lazy, useMemo, useState } from "react"; import { useAtom } from "jotai"; import { ErrorBoundary } from "react-error-boundary"; import { isEmpty, intersection } from "lodash-es"; @@ -33,7 +33,6 @@ import { tableSchemaAtom, columnModalAtom, tableModalAtom, - tableSortsAtom, } from "@src/atoms/tableScope"; import useBeforeUnload from "@src/hooks/useBeforeUnload"; import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider"; @@ -43,6 +42,7 @@ import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; import { DRAWER_COLLAPSED_WIDTH } from "@src/components/SideDrawer"; import { formatSubTableName } from "@src/utils/table"; import { TableToolsType } from "@src/types/table"; +import { RowSelectionState } from "@tanstack/react-table"; // prettier-ignore const BuildLogsSnack = lazy(() => import("@src/components/TableModals/CloudLogsModal/BuildLogs/BuildLogsSnack" /* webpackChunkName: "TableModals-BuildLogsSnack" */)); @@ -57,6 +57,8 @@ export interface ITablePageProps { disableSideDrawer?: boolean; /** list of table tools to be disabled */ disabledTools?: TableToolsType; + /** If true shows checkbox to select rows */ + enableRowSelection?: boolean; } /** @@ -76,6 +78,7 @@ export default function TablePage({ disableModals, disableSideDrawer, disabledTools, + enableRowSelection = false, }: ITablePageProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -101,6 +104,21 @@ export default function TablePage({ useBeforeUnload(columnModalAtom, tableScope); useBeforeUnload(tableModalAtom, tableScope); + const [selectedRows, setSelectedRows] = useState({}); + + // Without useMemo we'll be stuck in an infinite loop + const selectedRowsProp = useMemo( + () => ({ + state: selectedRows, + setState: setSelectedRows, + }), + [selectedRows, setSelectedRows] + ); + + const resetSelectedRows = () => { + setSelectedRows({}); + }; + if (!(tableSchema as any)._rowy_ref) return ( <> @@ -132,7 +150,11 @@ export default function TablePage({ }> - + @@ -160,6 +182,7 @@ export default function TablePage({ hiddenColumns={ userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields } + selectedRows={enableRowSelection ? selectedRowsProp : undefined} emptyState={