diff --git a/src/components/mui/BulkEditTable/BulkEditTable.js b/src/components/mui/BulkEditTable/BulkEditTable.js
index 925c6d4b..2d19111f 100644
--- a/src/components/mui/BulkEditTable/BulkEditTable.js
+++ b/src/components/mui/BulkEditTable/BulkEditTable.js
@@ -31,10 +31,7 @@ import styles from "./BulkEditTable.module.less";
import CustomTablePagination from "../tables/components/CustomTablePagination";
import parsePaginationPosition from "../tables/components/pagination-position";
import showConfirmDialog from "../showConfirmDialog";
-import {
- RESPONSIVE_TABLE_SX,
- getActionsMenuBreakpoint
-} from "../tables/components/table-styles";
+import { RESPONSIVE_TABLE_SX } from "../tables/components/table-styles";
const BulkEditTable = ({
options,
@@ -52,6 +49,7 @@ const BulkEditTable = ({
idKey,
onEdit,
onDelete,
+ getRowActions,
getName,
deleteDialogTitle,
deleteDialogBody,
@@ -71,9 +69,6 @@ const BulkEditTable = ({
reset
} = useRowSelection(idKey);
- const collapseActions = (onEdit ? 1 : 0) + (onDelete ? 1 : 0) >= 2;
- const actionsBreakpoint = getActionsMenuBreakpoint(columns.length);
-
const dataIds = data.map((row) => row[idKey]).join(",");
// reset selection/edit state whenever the set of rows shown changes
@@ -196,7 +191,7 @@ const BulkEditTable = ({
);
})}
- {(onEdit || onDelete) && (
+ {(onEdit || onDelete || getRowActions) && (
))}
@@ -253,6 +247,7 @@ BulkEditTable.propTypes = {
pageSliderVisible: PropTypes.bool,
onEdit: PropTypes.func,
onDelete: PropTypes.func,
+ getRowActions: PropTypes.func,
getName: PropTypes.func,
deleteDialogTitle: PropTypes.string,
deleteDialogBody: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
@@ -264,6 +259,7 @@ BulkEditTable.defaultProps = {
idKey: "id",
onEdit: null,
onDelete: null,
+ getRowActions: null,
getName: (item) => item.name,
deleteDialogTitle: null,
deleteDialogBody: null,
diff --git a/src/components/mui/BulkEditTable/__tests__/BulkEditTable.test.js b/src/components/mui/BulkEditTable/__tests__/BulkEditTable.test.js
index c9401ead..08dd79a3 100644
--- a/src/components/mui/BulkEditTable/__tests__/BulkEditTable.test.js
+++ b/src/components/mui/BulkEditTable/__tests__/BulkEditTable.test.js
@@ -21,12 +21,18 @@ import {
waitFor
} from "@testing-library/react";
import BulkEditTable from "../BulkEditTable";
+import showConfirmDialog from "../../showConfirmDialog";
jest.mock("i18n-react/dist/i18n-react", () => ({
__esModule: true,
default: { translate: (key) => key }
}));
+jest.mock("../../showConfirmDialog", () => ({
+ __esModule: true,
+ default: jest.fn()
+}));
+
describe("BulkEditTable", () => {
const baseProps = {
options: {
@@ -48,6 +54,51 @@ describe("BulkEditTable", () => {
jest.clearAllMocks();
});
+ test("delete from the row menu confirms, then passes the row's idKey value", async () => {
+ const user = userEvent.setup();
+ const onDelete = jest.fn();
+ showConfirmDialog.mockResolvedValueOnce(true);
+
+ render(
+
+ );
+
+ await user.click(screen.getAllByTestId("row-actions-menu-trigger")[1]);
+ await user.click(screen.getByRole("menuitem", { name: "general.delete" }));
+
+ await waitFor(() => expect(onDelete).toHaveBeenCalledWith("a-2"));
+ expect(showConfirmDialog).toHaveBeenCalledWith(
+ expect.objectContaining({ text: "general.row_remove_warning Event 2" })
+ );
+ });
+
+ test("shows getRowActions items in the row menu and runs their onClick", async () => {
+ const user = userEvent.setup();
+ const onArchive = jest.fn();
+
+ render(
+ [{ label: "Archive", onClick: () => onArchive(row) }]}
+ />
+ );
+
+ await user.click(screen.getAllByTestId("row-actions-menu-trigger")[1]);
+ await user.click(screen.getByRole("menuitem", { name: "Archive" }));
+
+ expect(onArchive).toHaveBeenCalledWith(expect.objectContaining({ id: 2 }));
+ });
+
test("applies bulk updates to selected rows", async () => {
const user = userEvent.setup();
const onUpdate = jest.fn(() => Promise.resolve());
diff --git a/src/components/mui/BulkEditTable/components/Row.js b/src/components/mui/BulkEditTable/components/Row.js
index e20c7718..68722b3e 100644
--- a/src/components/mui/BulkEditTable/components/Row.js
+++ b/src/components/mui/BulkEditTable/components/Row.js
@@ -13,14 +13,9 @@
import React from "react";
import PropTypes from "prop-types";
-import Box from "@mui/material/Box";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import Checkbox from "@mui/material/Checkbox";
-import IconButton from "@mui/material/IconButton";
-import EditIcon from "@mui/icons-material/Edit";
-import DeleteIcon from "@mui/icons-material/Delete";
-import T from "i18n-react/dist/i18n-react";
import Cell from "./Cell";
import RowActionsMenu from "../../tables/components/row-actions-menu";
import { getColumnWidthSx } from "../../tables/components/table-styles";
@@ -45,31 +40,19 @@ const Row = (props) => {
onFieldChange,
onEdit,
onDelete,
- idKey,
- collapseActions,
- actionsBreakpoint
+ getRowActions,
+ idKey
} = props;
const isEditingRow = isSelected && editEnabled;
- const rowActions = [
- onEdit && {
- label: T.translate("general.edit"),
- onClick: () => onEdit(row)
- },
- onDelete && {
- label: T.translate("general.delete"),
- onClick: () => onDelete(row)
- }
- ].filter(Boolean);
-
const onRowChange = (ev) => {
const { value, id } = ev.target;
onFieldChange(id, value);
};
return (
-
+
{
/>
))}
- {(onEdit || onDelete) && (
+ {(onEdit || onDelete || getRowActions) && (
-
- {onEdit && (
- onEdit(row)}
- sx={{ padding: 0 }}
- aria-label={`Edit row ${row[idKey]}`}
- >
-
-
- )}
- {onDelete && (
- onDelete(row)}
- sx={{ padding: 0 }}
- aria-label={`Delete row ${row[idKey]}`}
- >
-
-
- )}
-
- {collapseActions && (
-
-
-
- )}
+ onEdit(row))}
+ onDelete={onDelete && (() => onDelete(row))}
+ />
)}
@@ -156,17 +109,15 @@ Row.propTypes = {
onFieldChange: PropTypes.func,
onEdit: PropTypes.func,
onDelete: PropTypes.func,
- idKey: PropTypes.string,
- collapseActions: PropTypes.bool,
- actionsBreakpoint: PropTypes.string
+ getRowActions: PropTypes.func,
+ idKey: PropTypes.string
};
Row.defaultProps = {
idKey: "id",
onEdit: null,
onDelete: null,
- collapseActions: false,
- actionsBreakpoint: "md"
+ getRowActions: null
};
export default Row;
diff --git a/src/components/mui/__tests__/mui-table-editable.test.js b/src/components/mui/__tests__/mui-table-editable.test.js
index 8dd3f6a0..4862c3b6 100644
--- a/src/components/mui/__tests__/mui-table-editable.test.js
+++ b/src/components/mui/__tests__/mui-table-editable.test.js
@@ -30,11 +30,12 @@ jest.mock("@mui/material/IconButton", () => {
const React = require("react");
return {
__esModule: true,
- default: ({ children, onClick, ...rest }) => (
-
@@ -288,23 +286,6 @@ const MuiTableEditable = ({
)}
))}
- {onEdit && (
-
- onEdit(row)}
- size="small"
- aria-label={T.translate("general.edit")}
- >
-
-
-
- )}
{onArchive && (
)}
- {onDelete && (
+ {(onEdit || onDelete || getRowActions) && (
- handleDelete(row)}
- size="small"
- aria-label={T.translate("general.delete")}
- >
-
-
+ onEdit(row))}
+ onDelete={onDelete && (() => handleDelete(row))}
+ />
)}
@@ -390,6 +370,7 @@ MuiTableEditable.propTypes = {
onEdit: PropTypes.func,
onArchive: PropTypes.func,
onDelete: PropTypes.func,
+ getRowActions: PropTypes.func,
onCellChange: PropTypes.func,
deleteDialogBody: PropTypes.func,
confirmButtonColor: PropTypes.string
diff --git a/src/components/mui/tables/mui-table/index.js b/src/components/mui/tables/mui-table/index.js
index fa34da32..081c0667 100644
--- a/src/components/mui/tables/mui-table/index.js
+++ b/src/components/mui/tables/mui-table/index.js
@@ -23,14 +23,13 @@ import {
TableRow,
Tooltip
} from "@mui/material";
-import EditIcon from "@mui/icons-material/Edit";
-import DeleteIcon from "@mui/icons-material/Delete";
import ArchiveIcon from "@mui/icons-material/Archive";
import UnarchiveIcon from "@mui/icons-material/Unarchive";
import styles from "../components/styles.module.less";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import PropTypes from "prop-types";
import TableCellContent from "../components/table-cell-content";
+import TableCellLink from "../components/table-cell-link";
import TableShell from "../components/table-shell";
import createDeleteHandler from "../components/create-delete-handler";
import SortableHeaderContent from "../components/sortable-header-content";
@@ -40,7 +39,6 @@ import {
getColumnWidthSx,
getArchivedRowSx,
getActionCellSx,
- getActionsMenuBreakpoint,
ACTION_CELL_SX
} from "../components/table-styles";
@@ -61,7 +59,9 @@ const MuiTable = ({
onEdit,
onArchive,
onDelete,
+ getRowActions,
onSelect,
+ onNavigate,
canDelete = () => true,
deleteDialogTitle = null,
deleteDialogBody = null,
@@ -75,17 +75,11 @@ const MuiTable = ({
disableProp: null, // disableProp is the prop that will disable the row
...userOptions
};
- const activeActionsCount =
- (onEdit ? 1 : 0) + (onDelete ? 1 : 0) + (onArchive ? 1 : 0) + (onSelect ? 1 : 0);
- // responsive:false opts out of collapsing actions into a menu — needed when
- // extra rows (e.g. TotalRow) are passed as children with a static column
- // count that can't track the collapse breakpoint
- const collapseActions =
- options.responsive !== false && !children && activeActionsCount >= 2;
- const collapseBreakpoint = getActionsMenuBreakpoint(columns.length);
+ const hasRowMenu = !!(onEdit || onDelete || getRowActions);
+ const actionColumnsCount =
+ (hasRowMenu ? 1 : 0) + (onArchive ? 1 : 0) + (onSelect ? 1 : 0);
- const totalColumnsCount =
- columns.length + activeActionsCount + (collapseActions ? 1 : 0);
+ const totalColumnsCount = columns.length + actionColumnsCount;
const {sortCol, sortDir} = options;
@@ -94,49 +88,25 @@ const MuiTable = ({
...(col.headSx || {})
});
- // individual action cells hide below collapseBreakpoint once actions collapse into a menu
- const getIndividualActionSx = () =>
- collapseActions ? { display: { xs: "none", [collapseBreakpoint]: "table-cell" } } : {};
-
- const menuCellSx = { display: { xs: "table-cell", [collapseBreakpoint]: "none" } };
-
- const getRowActions = (row) =>
- [
- onEdit && {
- label: T.translate("general.edit"),
- onClick: () => onEdit(row),
- disabled: !!(options.disableProp && row[options.disableProp])
- },
- onArchive && {
- label: row.is_archived
- ? T.translate("general.unarchive")
- : T.translate("general.archive"),
- onClick: () => onArchive(row),
- disabled: !!(
- options.disableProp &&
- options.disableProp !== "is_archived" &&
- row[options.disableProp]
- )
- },
- onDelete &&
- canDelete(row) && {
- label: T.translate("general.delete"),
- onClick: () => handleDelete(row),
- disabled: !!(options.disableProp && row[options.disableProp])
- },
- onSelect && {
- label: T.translate("general.view"),
- onClick: () => onSelect(row),
- disabled: !!(options.disableProp && row[options.disableProp])
- }
- ].filter(Boolean);
-
const getCellSx = (row, col) => ({
...getColumnWidthSx(col),
...(col.cellSx || {}),
...getArchivedRowSx(row, options.disableProp)
});
+ // columns with getHref render as a link; no href for a row falls back to plain text
+ const renderCellContent = (row, col) => {
+ const content = ;
+ const href = col.getHref?.(row);
+ return href ? (
+
+ {content}
+
+ ) : (
+ content
+ );
+ };
+
const handleDelete = createDeleteHandler({
onDelete,
getName,
@@ -174,11 +144,9 @@ const MuiTable = ({
/>
))}
- {onEdit && }
- {onArchive && }
- {onDelete && }
- {onSelect && }
- {collapseActions && }
+ {onArchive && }
+ {hasRowMenu && }
+ {onSelect && }
@@ -188,7 +156,7 @@ const MuiTable = ({
{data.map((row) => row.renderRow ? (
{row.renderRow(totalColumnsCount)}
) : (
-
+
{/* Main content columns */}
{columns.map((col) => (
-
+ {renderCellContent(row, col)}
))}
- {/* Edit column */}
- {onEdit && (
-
-
-
- onEdit(row)}
- sx={{ padding: 0 }}
- data-testid="action-edit"
- disabled={options.disableProp && row[options.disableProp]}
- >
-
-
-
-
-
- )}
{onArchive && (
)}
- {/* Delete column */}
- {onDelete && (
+ {/* Row actions menu column */}
+ {hasRowMenu && (
- {canDelete(row) && (
-
-
- handleDelete(row)}
- data-testid="action-delete"
- sx={{ padding: 0 }}
- disabled={options.disableProp && row[options.disableProp]}
- >
-
-
-
-
- )}
+ onEdit(row))}
+ onDelete={onDelete && canDelete(row) ? () => handleDelete(row) : undefined}
+ disabled={!!(options.disableProp && row[options.disableProp])}
+ />
)}
{onSelect && (
@@ -315,15 +240,6 @@ const MuiTable = ({
)}
- {collapseActions && (
-
-
-
- )}
))}
{/* Here we inject extra rows passed as children */}
@@ -358,7 +274,9 @@ MuiTable.propTypes = {
onEdit: PropTypes.func,
onArchive: PropTypes.func,
onDelete: PropTypes.func,
+ getRowActions: PropTypes.func,
onSelect: PropTypes.func,
+ onNavigate: PropTypes.func,
canDelete: PropTypes.func,
deleteDialogTitle: PropTypes.string,
deleteDialogBody: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
diff --git a/src/components/mui/tables/sortable-table-v2/mui-table-sortable-v2.js b/src/components/mui/tables/sortable-table-v2/mui-table-sortable-v2.js
index aaf1225a..8ba60e68 100644
--- a/src/components/mui/tables/sortable-table-v2/mui-table-sortable-v2.js
+++ b/src/components/mui/tables/sortable-table-v2/mui-table-sortable-v2.js
@@ -24,8 +24,6 @@ import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import { IconButton } from "@mui/material";
-import EditIcon from "@mui/icons-material/Edit";
-import DeleteIcon from "@mui/icons-material/Delete";
import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { visuallyHidden } from "@mui/utils";
@@ -35,6 +33,7 @@ import styles from "./styles.module.less";
import showConfirmDialog from "../../showConfirmDialog";
import SortableRow from "./sortable-row";
import TableCellContent from "../components/table-cell-content";
+import RowActionsMenu from "../components/row-actions-menu";
import CustomTablePagination from "../components/CustomTablePagination";
import parsePaginationPosition from "../components/pagination-position";
import useDndKitReorder from "../../DragNDropList/hooks/useDndKitReorder";
@@ -59,6 +58,7 @@ const MuiTableSortableV2 = ({
getName = (item) => item.name,
onEdit,
onDelete,
+ getRowActions,
deleteDialogTitle = null,
deleteDialogBody = null,
onReorder,
@@ -176,8 +176,7 @@ const MuiTableSortableV2 = ({
)}
))}
- {onEdit && }
- {onDelete && }
+ {(onEdit || onDelete || getRowActions) && }
{onReorder && }
@@ -206,36 +205,18 @@ const MuiTableSortableV2 = ({
))}
- {/* Edit column */}
- {onEdit && (
+ {/* Row actions menu column */}
+ {(onEdit || onDelete || getRowActions) && (
- onEdit(row)}
- sx={{ padding: 0 }}
- >
-
-
-
- )}
- {/* Delete column */}
- {onDelete && (
-
- handleDelete(row)}
- sx={{ padding: 0 }}
- >
-
-
+ onEdit(row))}
+ onDelete={onDelete && (() => handleDelete(row))}
+ />
)}
{/* Re order column */}
@@ -259,8 +240,7 @@ const MuiTableSortableV2 = ({
item.name,
onEdit,
onDelete,
+ getRowActions,
deleteDialogTitle = null,
deleteDialogBody = null,
onReorder,
@@ -120,8 +120,7 @@ const MuiTableSortable = ({
/>
))}
- {onEdit && }
- {onDelete && }
+ {(onEdit || onDelete || getRowActions) && }
{onReorder && }
@@ -176,36 +175,18 @@ const MuiTableSortable = ({
))}
- {/* Edit column */}
- {onEdit && (
+ {/* Row actions menu column */}
+ {(onEdit || onDelete || getRowActions) && (
- onEdit(row)}
- sx={{ padding: 0 }}
- >
-
-
-
- )}
- {/* Delete column */}
- {onDelete && (
-
- handleDelete(row)}
- sx={{ padding: 0 }}
- >
-
-
+ onEdit(row))}
+ onDelete={onDelete && (() => handleDelete(row))}
+ />
)}
{/* Re order column */}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a3ebbaa6..7e19f986 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -31,6 +31,7 @@
"edit": "Edit",
"delete": "Delete",
"view": "View",
+ "more_actions": "More actions",
"cancel": "Cancel",
"apply": "Apply",
"n_a": "N/A",