-
Notifications
You must be signed in to change notification settings - Fork 4
Feature/grid filter continued #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
47e69af
df4fa23
15b581c
7ff283b
75760ee
63dd468
826bb4c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,160 @@ | ||||||||||||||||||||||||||||||||||||||
| import React, { useEffect } from "react"; | ||||||||||||||||||||||||||||||||||||||
| import PropTypes from "prop-types"; | ||||||||||||||||||||||||||||||||||||||
| 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 styles from "./BulkEditTable.module.less"; | ||||||||||||||||||||||||||||||||||||||
| import CustomTablePagination from "../table/CustomTablePagination"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const BulkEditTable = ({ options, columns, data, onSort, onUpdate, totalRows, perPage, currentPage, onPageChange, onPerPageChange }) => { | ||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||
| selectedRows, | ||||||||||||||||||||||||||||||||||||||
| isSelected, | ||||||||||||||||||||||||||||||||||||||
| toggleRow, | ||||||||||||||||||||||||||||||||||||||
| isAllSelected, | ||||||||||||||||||||||||||||||||||||||
| toggleAll, | ||||||||||||||||||||||||||||||||||||||
| editField, | ||||||||||||||||||||||||||||||||||||||
| editEnabled, | ||||||||||||||||||||||||||||||||||||||
| enterEditMode, | ||||||||||||||||||||||||||||||||||||||
| cancel, | ||||||||||||||||||||||||||||||||||||||
| reset | ||||||||||||||||||||||||||||||||||||||
| } = useRowSelection(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const dataIds = data.map((row) => row.id).join(","); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // reset selection/edit state whenever the set of rows shown changes | ||||||||||||||||||||||||||||||||||||||
| // (pagination, filtering, sorting, search, etc.) | ||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| reset(); | ||||||||||||||||||||||||||||||||||||||
| }, [dataIds]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const getSortDir = (columnKey) => | ||||||||||||||||||||||||||||||||||||||
| columnKey === options.sortCol ? options.sortDir : null; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const handleUpdateEvents = (evt) => { | ||||||||||||||||||||||||||||||||||||||
| evt.stopPropagation(); | ||||||||||||||||||||||||||||||||||||||
| evt.preventDefault(); | ||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PropTypes declaration ( One-line fix that preserves the current behavior for async consumers and doesn't crash for sync ones: Promise.resolve(onUpdate(selectedRows))
.then(() => reset())
.catch((error) => { console.error('Error updating events:', error); }); |
||||||||||||||||||||||||||||||||||||||
| onUpdate(selectedRows) | ||||||||||||||||||||||||||||||||||||||
| .then(() => reset()) | ||||||||||||||||||||||||||||||||||||||
| .catch((error) => { | ||||||||||||||||||||||||||||||||||||||
| console.error("Error updating events:", error); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Normalize This assumes Suggested fix const handleUpdateEvents = (evt) => {
evt.stopPropagation();
evt.preventDefault();
- onUpdate(selectedRows)
+ Promise.resolve(onUpdate(selectedRows))
.then(() => reset())
.catch((error) => {
console.error("Error updating events:", error);
});
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed valid — also flagged independently in our review. A synchronous There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <Box sx={{ width: "100%" }}> | ||||||||||||||||||||||||||||||||||||||
| <Toolbar | ||||||||||||||||||||||||||||||||||||||
| editEnabled={editEnabled} | ||||||||||||||||||||||||||||||||||||||
| hasSelection={selectedRows.length > 0} | ||||||||||||||||||||||||||||||||||||||
| onEdit={enterEditMode} | ||||||||||||||||||||||||||||||||||||||
| onApply={handleUpdateEvents} | ||||||||||||||||||||||||||||||||||||||
| 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)} | ||||||||||||||||||||||||||||||||||||||
| slotProps={{ input: { "aria-label": "select all" } }} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| {columns.map((col, i) => { | ||||||||||||||||||||||||||||||||||||||
| const sortable = !!col.sortable; | ||||||||||||||||||||||||||||||||||||||
| const colWidth = col.width ?? ""; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <Heading | ||||||||||||||||||||||||||||||||||||||
| editEnabled={editEnabled} | ||||||||||||||||||||||||||||||||||||||
| onSort={onSort} | ||||||||||||||||||||||||||||||||||||||
| sortDir={getSortDir(col.columnKey)} | ||||||||||||||||||||||||||||||||||||||
| sortable={sortable} | ||||||||||||||||||||||||||||||||||||||
| columnIndex={i} | ||||||||||||||||||||||||||||||||||||||
| columnKey={col.columnKey} | ||||||||||||||||||||||||||||||||||||||
| width={colWidth} | ||||||||||||||||||||||||||||||||||||||
| key={`heading_${col.columnKey}`} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {col.label} | ||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @santipalenque |
||||||||||||||||||||||||||||||||||||||
| </Heading> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||
| {options.actions && ( | ||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @santipalenque This condition checks only |
||||||||||||||||||||||||||||||||||||||
| <TableCell | ||||||||||||||||||||||||||||||||||||||
| align="center" | ||||||||||||||||||||||||||||||||||||||
| className={styles.actionColumn} | ||||||||||||||||||||||||||||||||||||||
| sx={{ backgroundColor: "#EAEDF4" }} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {options.actionsHeader || " "} | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||
| </TableRow> | ||||||||||||||||||||||||||||||||||||||
| </TableHead> | ||||||||||||||||||||||||||||||||||||||
| <TableBody> | ||||||||||||||||||||||||||||||||||||||
| {columns.length > 0 && | ||||||||||||||||||||||||||||||||||||||
| data.map((row) => ( | ||||||||||||||||||||||||||||||||||||||
| <Row | ||||||||||||||||||||||||||||||||||||||
| key={`row_${row.id}`} | ||||||||||||||||||||||||||||||||||||||
| row={row} | ||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| columns={columns} | ||||||||||||||||||||||||||||||||||||||
| actions={options.actions} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||
| </TableBody> | ||||||||||||||||||||||||||||||||||||||
| </Table> | ||||||||||||||||||||||||||||||||||||||
| </TableContainer> | ||||||||||||||||||||||||||||||||||||||
| {perPage && currentPage && onPageChange && ( | ||||||||||||||||||||||||||||||||||||||
| <CustomTablePagination | ||||||||||||||||||||||||||||||||||||||
| totalRows={totalRows} | ||||||||||||||||||||||||||||||||||||||
| perPage={perPage} | ||||||||||||||||||||||||||||||||||||||
| currentPage={currentPage} | ||||||||||||||||||||||||||||||||||||||
| onPageChange={onPageChange} | ||||||||||||||||||||||||||||||||||||||
| onPerPageChange={onPerPageChange} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||
| </Paper> | ||||||||||||||||||||||||||||||||||||||
| </Box> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| BulkEditTable.propTypes = { | ||||||||||||||||||||||||||||||||||||||
| options: PropTypes.object.isRequired, | ||||||||||||||||||||||||||||||||||||||
| columns: PropTypes.array.isRequired, | ||||||||||||||||||||||||||||||||||||||
| data: PropTypes.array.isRequired, | ||||||||||||||||||||||||||||||||||||||
| onSort: PropTypes.func.isRequired, | ||||||||||||||||||||||||||||||||||||||
| onUpdate: PropTypes.func.isRequired, | ||||||||||||||||||||||||||||||||||||||
| totalRows: PropTypes.number, | ||||||||||||||||||||||||||||||||||||||
| perPage: PropTypes.number, | ||||||||||||||||||||||||||||||||||||||
| currentPage: PropTypes.number, | ||||||||||||||||||||||||||||||||||||||
| onPageChange: PropTypes.func, | ||||||||||||||||||||||||||||||||||||||
| onPerPageChange: PropTypes.func | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export default BulkEditTable; | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| .tableWrapper { | ||
| width: 100%; | ||
| overflow-x: auto; | ||
| position: relative; | ||
|
|
||
| td { | ||
| max-width: 150px; | ||
| text-overflow: ellipsis; | ||
| overflow-wrap: break-word; | ||
| 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 |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import React from "react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| import { act, render, screen, waitFor } from "@testing-library/react"; | ||
| import BulkEditTable from "../BulkEditTable"; | ||
|
|
||
|
Comment on lines
+1
to
+5
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed valid. The test clicks by raw translation keys ( |
||
| describe("BulkEditTable", () => { | ||
| const baseProps = { | ||
| options: { | ||
| className: "test-table", | ||
| actions: {} | ||
| }, | ||
| columns: [ | ||
| { columnKey: "id", value: "id", sortable: true }, | ||
| { columnKey: "title", value: "title", sortable: true } | ||
| ], | ||
| onSort: jest.fn(), | ||
| data: [ | ||
| { id: 1, title: "Event 1" }, | ||
| { id: 2, title: "Event 2" } | ||
| ] | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| test("applies bulk updates to selected rows", async () => { | ||
| const user = userEvent.setup(); | ||
| const onUpdate = jest.fn(() => Promise.resolve()); | ||
|
|
||
| render(<BulkEditTable {...baseProps} onUpdate={onUpdate} />); | ||
|
|
||
| const checkboxes = screen.getAllByRole("checkbox"); | ||
|
|
||
| await user.click(checkboxes[1]); | ||
| await user.click(screen.getByText("event_list.edit_selected")); | ||
| await act(async () => { | ||
| await user.click(screen.getByText("bulk_actions_page.btn_apply_changes")); | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(onUpdate).toHaveBeenCalledTimes(1); | ||
| expect(onUpdate).toHaveBeenCalledWith( | ||
| expect.arrayContaining([expect.objectContaining({ id: 1 })]) | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||
| import React from "react"; | ||||||
| import PropTypes from "prop-types"; | ||||||
| import TextField from "@mui/material/TextField"; | ||||||
|
|
||||||
| const DEFAULT_PLACEHOLDER = "Enter text..."; | ||||||
|
smarcet marked this conversation as resolved.
|
||||||
|
|
||||||
| const Cell = ({ col, row, editRow, isEditingRow, onChange }) => { | ||||||
| if (isEditingRow && col.editableField === true) { | ||||||
| return ( | ||||||
| <TextField | ||||||
| id={col.columnKey} | ||||||
| placeholder={col.placeholder || DEFAULT_PLACEHOLDER} | ||||||
| multiline | ||||||
| minRows={2} | ||||||
| fullWidth | ||||||
| size="small" | ||||||
| onChange={onChange} | ||||||
| value={editRow[col.columnKey] || ""} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Do not coerce valid falsy cell values to empty string in edit mode. Using Suggested fix- value={editRow[col.columnKey] || ""}
+ value={editRow[col.columnKey] ?? ""}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed valid — this one was missed in our review. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| /> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| 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], | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hidden
const onRowChange = (ev) => {
const { value, id } = ev.target; // expects ev.target.id === col.columnKey
onFieldChange(id, value);
};The built-in This contract ("your synthetic event must carry onChange: (ev) => onChange({ target: { value: ev.target.value, id: col.columnKey } })or simplify to |
||||||
| onChange, | ||||||
| row: editRow, | ||||||
| rowData: editRow[col.columnKey] | ||||||
| }) ?? null | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| if (col.render) { | ||||||
| return col.render(row[col.columnKey], row) ?? null; | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <span style={{ fontWeight: "normal" }}>{row[col.columnKey] ?? null}</span> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| Cell.propTypes = { | ||||||
| col: PropTypes.object.isRequired, | ||||||
| row: PropTypes.object.isRequired, | ||||||
| editRow: PropTypes.object.isRequired, | ||||||
| isEditingRow: PropTypes.bool, | ||||||
| onChange: PropTypes.func | ||||||
| }; | ||||||
|
|
||||||
| export default Cell; | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed valid — consistency issue. The new
GridFiltervalue input files (AsyncSelectInput.jsx,DateTimeInput.jsx,NumberInput.jsx, etc.) all include the Apache 2.0 header, but none of the newBulkEditTablesource files do. As a published library this matters for downstream licensing clarity. @santipalenque please add the standard header to all newBulkEditTablemodules.