Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions src/components/tables/BulkEditTable/BulkEditTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import React, { useEffect } from "react";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Checkbox from "@mui/material/Checkbox";
import Toolbar from "./components/Toolbar";
import Heading from "./components/Heading";
import Row from "./components/Row";
import useRowSelection from "./hooks/useRowSelection";
import { SORT_ASCENDING, SORT_DESCENDING } from "../../../utils/constants";
import styles from "./BulkEditTable.module.less";

const defaults = {
sortFunc: (a, b) => {
if (a < b) {
return SORT_DESCENDING;
}
if (a > b) {
return SORT_ASCENDING;
}
return 0;
},
sortable: false,
sortCol: 0,
sortDir: 1,
colWidth: ""
};

function BulkEditTable(props) {
const {
options,
columns,
currentSummit,
page,
data,
handleSort,
updateData,
handleDeleteRow,
formattingFunction,
afterUpdate = []
} = props;

const {
selectedRows,
isSelected,
toggleRow,
isAllSelected,
toggleAll,
editField,
editEnabled,
enterEditMode,
cancel,
reset
} = useRowSelection();

// reset selection/edit state on data changes/pagination
useEffect(() => {
reset();
}, [page]);

const getSortDir = (columnKey, columnIndex, sortCol, sortDir) => {
if (columnKey && columnKey === sortCol) {
return sortDir;
}
if (sortCol === columnIndex) {
return sortDir;
}
return null;
};

const handledAfterUpdateData = () => {
const actionsAfterUpdate = [];
if (afterUpdate.length > 0) {
afterUpdate.forEach(({ action, propertyName }) => {
selectedRows.forEach((row) => {
if (Array.isArray(row[propertyName])) {
row[propertyName].forEach((e) => {
actionsAfterUpdate.push(action(e));
});
} else {
actionsAfterUpdate.push(action(row[propertyName]));
}
});
});
}
return Promise.all(actionsAfterUpdate);
};

const onUpdateEvents = (evt) => {
evt.stopPropagation();
evt.preventDefault();
updateData(currentSummit.id, selectedRows)
.then(() => handledAfterUpdateData())
.then(() => reset())
.catch((error) => {
console.error("Error updating events:", error);
});
};

return (
<Box sx={{ width: "100%" }}>
<Toolbar
editEnabled={editEnabled}
hasSelection={selectedRows.length > 0}
onEdit={enterEditMode}
onApply={onUpdateEvents}
onCancel={cancel}
/>
<Paper elevation={0} sx={{ width: "100%", mb: 2 }}>
<TableContainer
component={Paper}
className={styles.tableWrapper}
sx={{ borderRadius: 0, boxShadow: "none" }}
>
<Table>
<TableHead sx={{ backgroundColor: "#EAEDF4" }}>
<TableRow>
<TableCell
align="center"
className={styles.checkColumn}
sx={{ backgroundColor: "#EAEDF4" }}
>
<Checkbox
checked={isAllSelected(data)}
onChange={() => toggleAll(data)}
inputProps={{ "aria-label": "select all" }}
/>
</TableCell>
{columns.map((col, i) => {
const sortCol =
typeof options.sortCol !== "undefined"
? options.sortCol
: defaults.sortCol;
const sortDir =
typeof options.sortDir !== "undefined"
? options.sortDir
: defaults.sortDir;
const sortFunc =
typeof options.sortFunc !== "undefined"
? options.sortFunc
: defaults.sortFunc;
const sortable =
typeof col.sortable !== "undefined"
? col.sortable
: defaults.sortable;
const colWidth =
typeof col.width !== "undefined"
? col.width
: defaults.colWidth;

return (
<Heading
editEnabled={editEnabled}
onSort={handleSort}
sortDir={getSortDir(col.columnKey, i, sortCol, sortDir)}
sortable={sortable}
sortFunc={sortFunc}
columnIndex={i}
columnKey={col.columnKey}
width={colWidth}
key={`heading_${col.columnKey}_${col.value}`}
>
{col.value}
</Heading>
);
})}
{options.actions && (
<TableCell
align="center"
className={styles.actionColumn}
sx={{ backgroundColor: "#EAEDF4" }}
>
{options.actionsHeader || " "}
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{columns.length > 0 &&
data.map((row, i) => {
if (Array.isArray(row) && row.length !== columns.length) {
console.warn(
`Data at row ${i} is ${row.length}. It should be ${columns.length}.`
);
return <TableRow key={`row_${row.id}`} />;
}

return (
<Row
key={`row_${row.id}`}
row={row}
currentSummit={currentSummit}
editEnabled={editEnabled}
isSelected={isSelected(row.id)}
editRow={selectedRows.find((r) => r.id === row.id) || row}
onToggle={() => toggleRow(row)}
onFieldChange={(key, value) =>
editField(row.id, key, value)
}
deleteRow={handleDeleteRow}
columns={columns}
actions={options.actions}
formattingFunction={formattingFunction}
/>
);
})}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Box>
);
}

export default BulkEditTable;
57 changes: 57 additions & 0 deletions src/components/tables/BulkEditTable/BulkEditTable.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.tableWrapper {
width: 100%;
overflow-x: auto;
position: relative;

td {
max-width: 150px;
text-overflow: ellipsis;
vertical-align: middle;

&.dataColumn {
min-width: 150px;
}
}

// shared by header (th) and body (td) cells so the checkbox/action columns
// stay pinned and aligned in both rows while the data columns scroll
// horizontally. Background color is intentionally NOT set here: header
// cells need the header's grey, body cells need white, set via sx where
// each is rendered (an explicit color is required, `inherit` resolves to
// transparent here and lets the scrolling columns show through).
.checkColumn {
text-align: center;
position: sticky;
z-index: 5;
left: 0;
}

.actionColumn {
text-align: center;
position: sticky;
z-index: 5;
right: 0;
width: 60px;
min-width: 60px;
max-width: 60px;
}

.bulkEditCol {
min-width: 250px;
}
}

.dottedBorderLeft {
position: relative;
border-left: none;
&::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
border-left: 1px dashed #e0e0e0;
height: 60%;
align-self: center;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
import userEvent from "@testing-library/user-event";
import { act, render, screen, waitFor } from "@testing-library/react";
import EditableTable from "../EditableTable";
import BulkEditTable from "../BulkEditTable";

describe("EditableTable", () => {
describe("BulkEditTable", () => {
const baseProps = {
options: {
className: "test-table",
Expand Down Expand Up @@ -40,7 +40,7 @@ describe("EditableTable", () => {
const user = userEvent.setup();
const updateData = jest.fn(() => Promise.resolve());

render(<EditableTable {...baseProps} updateData={updateData} />);
render(<BulkEditTable {...baseProps} updateData={updateData} />);

const checkboxes = screen.getAllByRole("checkbox");

Expand All @@ -65,7 +65,7 @@ describe("EditableTable", () => {
const afterUpdateAction = jest.fn(() => Promise.resolve());

render(
<EditableTable
<BulkEditTable
{...baseProps}
updateData={updateData}
afterUpdate={[
Expand Down
70 changes: 70 additions & 0 deletions src/components/tables/BulkEditTable/components/Cell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import PropTypes from "prop-types";
import T from "i18n-react/dist/i18n-react";
import TextField from "@mui/material/TextField";

function Cell({
col,
row,
editRow,
isEditingRow,
onChange,
onRemoveOption,
formattedData
}) {
if (isEditingRow && col.editableField === true) {
return (
<TextField
id={col.columnKey}
placeholder={T.translate(
`bulk_actions_page.placeholders.${col.columnKey}`
)}
multiline
minRows={2}
fullWidth
size="small"
onChange={onChange}
value={editRow[col.columnKey] || ""}
/>
);
}

if (isEditingRow && col.editableField) {
// editableField functions may short-circuit (e.g. `cond && <Input />`) and
// return `undefined` rather than `false`, which React rejects as a component return value.
return (
col.editableField({
value:
editRow[col.columnKey]?.id ||
editRow[col.columnKey]?.value ||
editRow[col.columnKey],
onChange,
row: editRow,
rowData: editRow[col.columnKey],
onRemoveOption
}) ?? null
);
}

if (col.render) {
return col.render(row[col.columnKey], row) ?? null;
}

return (
<span style={{ fontWeight: "normal" }}>
{formattedData[col.columnKey] ?? null}
</span>
);
}

Cell.propTypes = {
col: PropTypes.object.isRequired,
row: PropTypes.object.isRequired,
editRow: PropTypes.object.isRequired,
isEditingRow: PropTypes.bool,
onChange: PropTypes.func,
onRemoveOption: PropTypes.func,
formattedData: PropTypes.object
};

export default Cell;
Loading
Loading