mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
add support for bulk deletion of rows
This commit is contained in:
@@ -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 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({
|
||||
}}
|
||||
>
|
||||
<TableHeader
|
||||
headerGroups={table.getHeaderGroups()}
|
||||
table={table}
|
||||
handleDropColumn={handleDropColumn}
|
||||
canAddColumns={canAddColumns}
|
||||
canEditColumns={canEditColumns}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user