add support for bulk deletion of rows

This commit is contained in:
Vaibhav C
2023-06-11 18:23:49 +00:00
committed by il3ven
parent b2983f4db8
commit 17369b47cf
6 changed files with 207 additions and 38 deletions

View File

@@ -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<RowSelectionState>({});
* const selectedRowsProp = useMemo(() => ({state: selectedRows, setState: setSelectedRows}), [selectedRows, setSelectedRows])
* <Table selectedRows={selectedRowsProp} />
*/
selectedRows?: {
state: RowSelectionState;
setState: React.Dispatch<React.SetStateAction<{}>>;
};
}
/**
@@ -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 }) => (
<FormControlLabel
sx={{ margin: 0 }}
label=""
control={
<Checkbox
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
}
/>
),
cell: ({ row }) => {
return (
<FormControlLabel
label=""
sx={{ margin: 0 }}
control={
<Checkbox
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
}
/>
);
},
})
);
return _columns;
}, [tableColumnsOrdered, canAddColumns, canEditCells]);
}, [tableColumnsOrdered, canAddColumns, canEditCells, selectedRows]);
// Get users 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({
}}
>
<TableHeader
headerGroups={table.getHeaderGroups()}
table={table}
handleDropColumn={handleDropColumn}
canAddColumns={canAddColumns}
canEditColumns={canEditColumns}

View File

@@ -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 {
/**
@@ -48,7 +54,7 @@ export interface ITableBodyProps {
* - Renders row out of order indicator
* - Renders next page loading UI (`RowsSkeleton`)
*/
export const TableBody = memo(function TableBody({
export const TableBody = function TableBody({
containerRef,
leafColumns,
rows,
@@ -114,6 +120,14 @@ export const TableBody = memo(function TableBody({
const isReadOnlyCell =
fieldTypeGroup === "Auditing" || fieldTypeGroup === "Metadata";
if (cell.id.includes("_rowy_select")) {
return (
<StyledCell key={cell.id} role="gridcell">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</StyledCell>
);
}
return (
<TableCell
key={cell.id}
@@ -143,6 +157,6 @@ export const TableBody = memo(function TableBody({
)}
</div>
);
});
};
export default TableBody;

View File

@@ -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<TableRow>[];
table: Table<TableRow>;
/** 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 (
<StyledColumnHeader
key={header.id}
role="columnheader"
style={{ padding: 0 }}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</StyledColumnHeader>
);
// Render later, after the drag & drop placeholder
if (header.id === "_rowy_column_actions")
return (
@@ -129,6 +145,6 @@ export const TableHeader = memo(function TableHeader({
))}
</DragDropContext>
);
});
};
export default TableHeader;

View File

@@ -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]

View File

@@ -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) => (
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{
pl: (theme) => `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}
</Stack>
);
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 (
<StyledStack>
<Typography>
{Object.values(selectedRows).length} of {serverDocCount} rows selected
</Typography>
<Tooltip title="Delete row">
<Button
variant="outlined"
startIcon={<DeleteIcon fontSize="small" />}
color="error"
onClick={handleDelete}
>
Delete
</Button>
</Tooltip>
</StyledStack>
);
}
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 (
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{
pl: (theme) => `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 (
<RowSelectedToolBar
selectedRows={selectedRows}
resetSelectedRows={resetSelectedRows}
/>
);
return (
<StyledStack>
{tableSettings.isCollection === false ? (
<AddRowArraySubTable />
) : (
@@ -202,6 +259,6 @@ export default function TableToolbar({
)}
<TableInformation />
<div className="end-spacer" />
</Stack>
</StyledStack>
);
}

View File

@@ -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<RowSelectionState>({});
// 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({
<ActionParamsProvider>
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<Suspense fallback={<TableToolbarSkeleton />}>
<TableToolbar disabledTools={disabledTools} />
<TableToolbar
disabledTools={disabledTools}
selectedRows={selectedRows}
resetSelectedRows={resetSelectedRows}
/>
</Suspense>
</ErrorBoundary>
@@ -160,6 +179,7 @@ export default function TablePage({
hiddenColumns={
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields
}
selectedRows={selectedRowsProp}
emptyState={
<EmptyState
Icon={AddRowIcon}