class GroupSelectableListRenderer extends ListRenderer {
/**
* Toggles the selection of all rows within a group.
* @param {Object} group - The group node (representing the group header data).
* @param {boolean} checked - Whether the rows should be checked or unchecked.
*/
toggleGroupRows(group, checked) {
if (!Array.isArray(group.children)) {
return;
}
for (const item of group.children) {
// Ensure we are only selecting actual records, not other nested group nodes and that the selection state actually needs to change.
if (item.id && this.props.isSelected(item.id) !== checked) {
this.props.selectRow(item, checked);
}
}
}
/**
* Overrides the getGroupRow method to add the group selection checkbox.
* @param {Object} node - The group node.
* @param {number} groupLevel - The nesting level of the group.
* @returns {Object} The VNode representing the group row.
*/
getGroupRow(node, groupLevel) {
// Get the original group row VNode from the parent class.
// This call is crucial as it sets up the basic structure and potentially the default click handlers for expansion/collapse.
const row = super.getGroupRow(node, groupLevel);
// Find the <td> element that contains the group label. This is typically the first <td> in the group row.
const labelCell = row.children.find(child => child.tag === "td");
if (labelCell) {
// Create the checkbox VNode.
const checkboxVNode = {
tag: "input",
attrs: {
type: "checkbox",
class: "o_group_checkbox form-check-input", // Added form-check-input for Odoo styling
style: "margin-right: 8px;",
},
on: {
click: ev => {
// Prevent this click from bubbling up to the labelCell's or row's default click handler (which would toggle the group).
ev.stopPropagation();
},
change: ev => this.toggleGroupRows(node, ev.target.checked),
},
};
// Insert the checkbox at the beginning of the label cell's content. We use unshift to place it before the group name.
if (Array.isArray(labelCell.children)) {
labelCell.children.unshift(checkboxVNode);
} else {
// If labelCell.children is not an array (e.g., it's just a string), we need to wrap the original content and then add the checkbox.
const originalContent = labelCell.children;
labelCell.children = [checkboxVNode, originalContent];
}
// The `row` VNode itself has an `onClick` property if the base ListRenderer adds it to the row directly. Let's try to preserve/augment that.
const originalRowOnClick = row.on && row.on.click;
row.on = {
...(row.on || {}), // Preserve other existing event handlers on the row
click: ev => {
// If the click originated from our checkbox, do nothing here.
// The checkbox's own `stopPropagation` should already prevent this.
if (ev.target.classList.contains("o_group_checkbox")) {
return;
}
// Call the original group toggle handler if it exists.
// This is the cleanest way to ensure default behavior is preserved.
if (originalRowOnClick) {
originalRowOnClick(ev);
} else {
// Fallback: If no original handler, we manually toggle the group.
// The ListModel has a method to toggle group expansion.
// This assumes `this.props.list` is the ListModel instance.
// The `toggleGroup` method of the ListModel expects the group node.
// This is a more direct way to ensure the group state changes.
this.props.list.toggleGroup(node);
}
},
};
}
return row;
}
}
const CustomListView = registry.category("views").get("list");
registry.category("views").add("group_select_list", {
...CustomListView,
Renderer: GroupSelectableListRenderer,
});
// add js_class = "group_select_list" in the <list> tag of whichever list needs to be Group Selectable
// OR add <field name="context">{'view_type': 'group_select_list'}</field> to the action definition