From 17369b47cfdcbdaeb290ec0a12d9cfc73fbba65b Mon Sep 17 00:00:00 2001 From: Vaibhav C Date: Sun, 11 Jun 2023 18:23:49 +0000 Subject: [PATCH 1/8] add support for bulk deletion of rows --- src/components/Table/Table.tsx | 65 +++++++++++- src/components/Table/TableBody.tsx | 20 +++- src/components/Table/TableHeader.tsx | 26 ++++- src/components/Table/useVirtualization.tsx | 3 +- src/components/TableToolbar/TableToolbar.tsx | 105 ++++++++++++++----- src/pages/Table/TablePage.tsx | 26 ++++- 6 files changed, 207 insertions(+), 38 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 892eabf9..92d42183 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, @@ -42,6 +43,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 +77,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,6 +109,7 @@ export default function Table({ canEditCells, hiddenColumns, emptyState, + selectedRows, }: ITableProps) { const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); @@ -142,8 +159,44 @@ export default function Table({ ); } + if (selectedRows) + _columns.unshift( + columnHelper.display({ + id: "_rowy_select", + size: 41.8, // TODO: We shouldn't have to change this often + header: ({ table }) => ( + + } + /> + ), + cell: ({ row }) => { + return ( + + } + /> + ); + }, + }) + ); + return _columns; - }, [tableColumnsOrdered, canAddColumns, canEditCells]); + }, [tableColumnsOrdered, canAddColumns, canEditCells, selectedRows]); // Get user’s hidden columns from props and memoize into a `VisibilityState` const columnVisibility: VisibilityState = useMemo(() => { @@ -172,6 +225,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 @@ -292,7 +353,7 @@ export default function Table({ }} > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + } + return ( ); -}); +}; export default TableBody; diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index f7187646..2064d538 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -2,7 +2,7 @@ import { memo, Fragment } from "react"; import { useAtom } from "jotai"; import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd"; -import type { ColumnSizingState, HeaderGroup } from "@tanstack/react-table"; +import { ColumnSizingState, Table, flexRender } from "@tanstack/react-table"; import type { TableRow } from "@src/types/table"; import StyledRow from "./Styled/StyledRow"; @@ -11,10 +11,11 @@ import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; import { tableScope, selectedCellAtom } from "@src/atoms/tableScope"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; +import StyledColumnHeader from "./Styled/StyledColumnHeader"; export interface ITableHeaderProps { /** Headers with context from TanStack Table state */ - headerGroups: HeaderGroup[]; + table: Table; /** Called when a header is dropped in a new position */ handleDropColumn: (result: DropResult) => void; /** Passed to `FinalColumnHeader` */ @@ -34,13 +35,14 @@ export interface ITableHeaderProps { * * - Renders drag & drop components */ -export const TableHeader = memo(function TableHeader({ - headerGroups, +export const TableHeader = function TableHeader({ + table, handleDropColumn, canAddColumns, canEditColumns, lastFrozen, }: ITableHeaderProps) { + const headerGroups = table.getHeaderGroups(); const [selectedCell] = useAtom(selectedCellAtom, tableScope); const focusInside = selectedCell?.focusInside ?? false; @@ -69,6 +71,20 @@ export const TableHeader = memo(function TableHeader({ const isLastHeader = i === headerGroup.headers.length - 1; + if (header.id === "_rowy_select") + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + // Render later, after the drag & drop placeholder if (header.id === "_rowy_column_actions") return ( @@ -129,6 +145,6 @@ export const TableHeader = memo(function TableHeader({ ))} ); -}); +}; export default TableHeader; 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/TablePage.tsx b/src/pages/Table/TablePage.tsx index 421be174..b61e65bd 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" */)); @@ -101,6 +101,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 +147,11 @@ export default function TablePage({ }> - + @@ -160,6 +179,7 @@ export default function TablePage({ hiddenColumns={ userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields } + selectedRows={selectedRowsProp} emptyState={ Date: Sun, 18 Jun 2023 11:39:40 +0000 Subject: [PATCH 2/8] re-add `memo` and `headerGroup` as per suggestion --- src/components/Table/Table.tsx | 2 +- src/components/Table/TableBody.tsx | 4 ++-- src/components/Table/TableHeader.tsx | 15 +++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 92d42183..f1931337 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -353,7 +353,7 @@ export default function Table({ }} > ); -}; +}); export default TableBody; diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index 2064d538..ae763a60 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -2,7 +2,11 @@ import { memo, Fragment } from "react"; import { useAtom } from "jotai"; import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd"; -import { ColumnSizingState, Table, flexRender } from "@tanstack/react-table"; +import { + ColumnSizingState, + HeaderGroup, + flexRender, +} from "@tanstack/react-table"; import type { TableRow } from "@src/types/table"; import StyledRow from "./Styled/StyledRow"; @@ -15,7 +19,7 @@ import StyledColumnHeader from "./Styled/StyledColumnHeader"; export interface ITableHeaderProps { /** Headers with context from TanStack Table state */ - table: Table; + headerGroups: HeaderGroup[]; /** Called when a header is dropped in a new position */ handleDropColumn: (result: DropResult) => void; /** Passed to `FinalColumnHeader` */ @@ -35,14 +39,13 @@ export interface ITableHeaderProps { * * - Renders drag & drop components */ -export const TableHeader = function TableHeader({ - table, +export const TableHeader = memo(function TableHeader({ + headerGroups, handleDropColumn, canAddColumns, canEditColumns, lastFrozen, }: ITableHeaderProps) { - const headerGroups = table.getHeaderGroups(); const [selectedCell] = useAtom(selectedCellAtom, tableScope); const focusInside = selectedCell?.focusInside ?? false; @@ -145,6 +148,6 @@ export const TableHeader = function TableHeader({ ))} ); -}; +}); export default TableHeader; From febe46c14b3e2482c7542136672ae93378ef6b9e Mon Sep 17 00:00:00 2001 From: il3ven Date: Tue, 4 Jul 2023 20:59:00 +0000 Subject: [PATCH 3/8] fix ui issue when a column is frozen --- src/components/Table/Table.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index f1931337..7b241e77 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -204,14 +204,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] ); From 9b5c2e34b80881471b5d8d6f014ca76d7e4fe500 Mon Sep 17 00:00:00 2001 From: il3ven Date: Tue, 4 Jul 2023 20:59:57 +0000 Subject: [PATCH 4/8] fix re-ordering of columns when row selection is enabled --- src/components/Table/Table.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 7b241e77..b9b5398a 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -276,11 +276,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( From 3d45e340ae28027e064118d92bc27e84e2876a8b Mon Sep 17 00:00:00 2001 From: il3ven Date: Wed, 5 Jul 2023 11:38:37 +0000 Subject: [PATCH 5/8] disable row selection for array sub table page Row selection has been disabled because bulk delete functionality cannot work for array sub table according to the current implementation. Bulk delete problem: Suppose we have an array with values `[a, b, c]`. Suppose, the indexes to be removed are [1, 2]. The UI asks firebase function to remove index 1. After 1, the UI asks index 2 to be deleted but index 2 doesn't exist. After deletion of index 1, the array is `[a, c]`. --- src/pages/Table/ProvidedArraySubTablePage.tsx | 1 + src/pages/Table/TablePage.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) 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() { Date: Wed, 19 Jul 2023 12:21:05 +0000 Subject: [PATCH 6/8] make `enableRowSelection` optional and false by default `enableRowSelection` was introduced in the last commit. As of this commit row selection is enabled for TablePage and SubTablePage. --- src/pages/Table/ProvidedSubTablePage.tsx | 2 +- src/pages/Table/ProvidedTablePage.tsx | 2 +- src/pages/Table/TablePage.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Table/ProvidedSubTablePage.tsx b/src/pages/Table/ProvidedSubTablePage.tsx index e4659d33..649e9e90 100644 --- a/src/pages/Table/ProvidedSubTablePage.tsx +++ b/src/pages/Table/ProvidedSubTablePage.tsx @@ -137,7 +137,7 @@ export default function ProvidedSubTablePage() { > - + 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 3180cf84..c9286821 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -58,7 +58,7 @@ export interface ITablePageProps { /** list of table tools to be disabled */ disabledTools?: TableToolsType; /** If true shows checkbox to select rows */ - enableRowSelection: boolean; + enableRowSelection?: boolean; } /** @@ -78,7 +78,7 @@ export default function TablePage({ disableModals, disableSideDrawer, disabledTools, - enableRowSelection = true, + enableRowSelection = false, }: ITablePageProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); From f08710589586de6f7522ca6395e6e3aab5488e15 Mon Sep 17 00:00:00 2001 From: il3ven Date: Wed, 23 Aug 2023 18:35:34 +0000 Subject: [PATCH 7/8] change logic to determine state of checkbox in header row If there are 1000 rows and we have rendered 100 then select all will only select 100. In this case, technically all rows in react-table are selected but we should not show the checked state. It leads to bad UX. Indermediate would be better. Also, split the additions of row selection column into a new useMemo. This ensures that if a row is selected we are only re-calculating the row selection column and not all. --- src/components/Table/Table.tsx | 56 ++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index b9b5398a..e512fda8 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -34,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"; @@ -112,6 +113,7 @@ export default function Table({ 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); @@ -159,24 +161,39 @@ export default function Table({ ); } - if (selectedRows) - _columns.unshift( + return _columns; + }, [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 }) => ( - - } - /> - ), + 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 ( ); }, - }) - ); - - return _columns; - }, [tableColumnsOrdered, canAddColumns, canEditCells, selectedRows]); + }), + ]; + }, [selectedRows]) + ); // Get user’s hidden columns from props and memoize into a `VisibilityState` const columnVisibility: VisibilityState = useMemo(() => { From 5936f1351ed605ece839de22db3ec5d89e3669a3 Mon Sep 17 00:00:00 2001 From: il3ven Date: Wed, 23 Aug 2023 18:36:26 +0000 Subject: [PATCH 8/8] fix css of CheckboxIndeterminateIcon Due to wrong CSS we were not able to see the icon. --- src/theme/CheckboxIndeterminateIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/CheckboxIndeterminateIcon.tsx b/src/theme/CheckboxIndeterminateIcon.tsx index 682384f7..c4af7c34 100644 --- a/src/theme/CheckboxIndeterminateIcon.tsx +++ b/src/theme/CheckboxIndeterminateIcon.tsx @@ -47,7 +47,7 @@ export default function CheckboxIndeterminateIcon() { boxShadow: 1, }, - ".Mui-checked &, [aria-selected='true'] &": { + ".MuiCheckbox-indeterminate &, [aria-selected='true'] &": { backgroundColor: "currentColor", borderColor: "currentColor",