feat: Implement group selection in list view by updating JavaScript logic, UI templates, styling, module manifest, and documentation.
This commit is contained in:
parent
1081cc2a36
commit
979337bbe7
1016
ARCHITECTURE.md
1016
ARCHITECTURE.md
File diff suppressed because it is too large
Load Diff
86
README.md
86
README.md
@ -1,44 +1,44 @@
|
||||
# List View Group Select
|
||||
|
||||
Enhances Odoo 17 list views by adding selection controls to grouped results.
|
||||
Users can select all records in a group with a single click, including records in nested sub-groups.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Checkbox injected into each group header for quick selection
|
||||
- ✅ Recursive selection across nested sub-groups
|
||||
- ✅ Partial-selection indicator when only some records are selected
|
||||
- ✅ Action menu entry “Select All in Visible Groups”
|
||||
- ✅ Compatible with bulk actions (Export, Archive/Unarchive, Delete, etc.)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or copy this module into your Odoo add-ons directory (`customaddons` or similar).
|
||||
2. Restart the Odoo server.
|
||||
3. Activate developer mode in Odoo.
|
||||
4. Update the app list and install **List View Group Select**.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open any list view.
|
||||
2. Apply at least one “Group By”.
|
||||
3. Click the checkbox in the group header to toggle selection:
|
||||
- **Unchecked** → no records selected
|
||||
- **Indeterminate** → some records selected
|
||||
- **Checked** → all records selected
|
||||
4. Use the action menu “Select All in Visible Groups” to bulk-select every visible group.
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required.
|
||||
The module automatically integrates with Odoo’s web assets.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Tested with Odoo 17.0 (community edition)
|
||||
- Works for any model that supports grouped list views
|
||||
- No Python models or database changes
|
||||
|
||||
## License
|
||||
|
||||
# List View Group Select
|
||||
|
||||
Enhances Odoo 17 list views by adding selection controls to grouped results.
|
||||
Users can select all records in a group with a single click, including records in nested sub-groups.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Checkbox injected into each group header for quick selection
|
||||
- ✅ Recursive selection across nested sub-groups
|
||||
- ✅ Partial-selection indicator when only some records are selected
|
||||
- ✅ Action menu entry “Select All in Visible Groups”
|
||||
- ✅ Compatible with bulk actions (Export, Archive/Unarchive, Delete, etc.)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or copy this module into your Odoo add-ons directory (`customaddons` or similar).
|
||||
2. Restart the Odoo server.
|
||||
3. Activate developer mode in Odoo.
|
||||
4. Update the app list and install **List View Group Select**.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open any list view.
|
||||
2. Apply at least one “Group By”.
|
||||
3. Click the checkbox in the group header to toggle selection:
|
||||
- **Unchecked** → no records selected
|
||||
- **Indeterminate** → some records selected
|
||||
- **Checked** → all records selected
|
||||
4. Use the action menu “Select All in Visible Groups” to bulk-select every visible group.
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required.
|
||||
The module automatically integrates with Odoo’s web assets.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Tested with Odoo 17.0 (community edition)
|
||||
- Works for any model that supports grouped list views
|
||||
- No Python models or database changes
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3. See the LICENSE file supplied with Odoo for the framework license.
|
||||
@ -1,31 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "List View Group Select",
|
||||
"summary": "Add recursive group selection controls to list views",
|
||||
"description": """
|
||||
List View Group Select
|
||||
======================
|
||||
|
||||
Adds a checkbox to list view group headers allowing rapid selection or
|
||||
deselection of all records within a group, including nested sub-groups.
|
||||
Compatible with all standard bulk operations (Export, Archive, Delete, etc.).
|
||||
""",
|
||||
"version": "17.0.1.0.1",
|
||||
"category": "Web",
|
||||
"author": "Suherdy Yacob",
|
||||
"website": "https://www.example.com",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["web"],
|
||||
"data": [],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"list_view_group_select/static/src/js/list_renderer_group_select.js",
|
||||
"list_view_group_select/static/src/js/list_controller_group_select.js",
|
||||
"list_view_group_select/static/src/xml/list_renderer_templates.xml",
|
||||
"list_view_group_select/static/src/scss/list_view_group_select.scss",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"auto_install": False,
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "List View Group Select",
|
||||
"summary": "Add recursive group selection controls to list views",
|
||||
"description": """
|
||||
List View Group Select
|
||||
======================
|
||||
|
||||
Adds a checkbox to list view group headers allowing rapid selection or
|
||||
deselection of all records within a group, including nested sub-groups.
|
||||
Compatible with all standard bulk operations (Export, Archive, Delete, etc.).
|
||||
""",
|
||||
"version": "17.0.1.0.1",
|
||||
"category": "Web",
|
||||
"author": "Suherdy Yacob",
|
||||
"website": "https://www.example.com",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["web"],
|
||||
"data": [],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"list_view_group_select/static/src/js/list_renderer_group_select.js",
|
||||
"list_view_group_select/static/src/js/list_controller_group_select.js",
|
||||
"list_view_group_select/static/src/xml/list_renderer_templates.xml",
|
||||
"list_view_group_select/static/src/scss/list_view_group_select.scss",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"auto_install": False,
|
||||
}
|
||||
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,102 +1,102 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
/**
|
||||
* Ensure all records belonging to `group` (and its nested sub-groups) are loaded.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
const ensureGroupFullyLoaded = async (group) => {
|
||||
if (!group || !group.list) {
|
||||
return;
|
||||
}
|
||||
const list = group.list;
|
||||
if (list.isGrouped) {
|
||||
for (const nested of list.groups || []) {
|
||||
await ensureGroupFullyLoaded(nested);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let targetCount = group.count ?? list.count ?? list.records.length;
|
||||
if (list.hasLimitedCount) {
|
||||
await list.fetchCount();
|
||||
targetCount = Math.max(targetCount, list.count || 0);
|
||||
}
|
||||
targetCount = Math.max(targetCount, list.records.length);
|
||||
if (targetCount > list.records.length) {
|
||||
await list.load({ offset: 0, limit: targetCount });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect all records belonging to the provided group (including nested groups).
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @param {ListController["model"]["root"]} rootList
|
||||
* @returns {import("@web/model/relational_model/record").Record[]}
|
||||
*/
|
||||
const getGroupRecords = async (group, rootList) => {
|
||||
if (!group) {
|
||||
return [];
|
||||
}
|
||||
await ensureGroupFullyLoaded(group);
|
||||
if (group.list.isGrouped) {
|
||||
const records = [];
|
||||
for (const nested of group.list.groups || []) {
|
||||
records.push(...(await getGroupRecords(nested, rootList)));
|
||||
}
|
||||
return records;
|
||||
}
|
||||
return [...group.list.records];
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerate every top-level group visible on the controller.
|
||||
*
|
||||
* @param {ListController} controller
|
||||
*/
|
||||
const iterateVisibleGroups = (controller) => {
|
||||
const mainList = controller.model.root;
|
||||
const groups = [];
|
||||
if (mainList.isGrouped) {
|
||||
for (const group of mainList.groups) {
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
patch(ListController.prototype, {
|
||||
getStaticActionMenuItems() {
|
||||
const items = super.getStaticActionMenuItems(...arguments);
|
||||
items.selectAllGroups = {
|
||||
isAvailable: () => this.model.root.isGrouped,
|
||||
sequence: 5,
|
||||
icon: "fa fa-check-square-o",
|
||||
description: _t("Select All in Visible Groups"),
|
||||
callback: () => this.onSelectAllGroups(),
|
||||
};
|
||||
return items;
|
||||
},
|
||||
|
||||
async onSelectAllGroups() {
|
||||
const list = this.model.root;
|
||||
if (!list.isGrouped) {
|
||||
return;
|
||||
}
|
||||
const groups = iterateVisibleGroups(this);
|
||||
for (const group of groups) {
|
||||
const records = await getGroupRecords(group, list);
|
||||
for (const record of records) {
|
||||
if (!record.selected) {
|
||||
record.toggleSelection(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.model.root.selectDomain(false);
|
||||
this.model.root.notify();
|
||||
},
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
/**
|
||||
* Ensure all records belonging to `group` (and its nested sub-groups) are loaded.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
const ensureGroupFullyLoaded = async (group) => {
|
||||
if (!group || !group.list) {
|
||||
return;
|
||||
}
|
||||
const list = group.list;
|
||||
if (list.isGrouped) {
|
||||
for (const nested of list.groups || []) {
|
||||
await ensureGroupFullyLoaded(nested);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let targetCount = group.count ?? list.count ?? list.records.length;
|
||||
if (list.hasLimitedCount) {
|
||||
await list.fetchCount();
|
||||
targetCount = Math.max(targetCount, list.count || 0);
|
||||
}
|
||||
targetCount = Math.max(targetCount, list.records.length);
|
||||
if (targetCount > list.records.length) {
|
||||
await list.load({ offset: 0, limit: targetCount });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect all records belonging to the provided group (including nested groups).
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @param {ListController["model"]["root"]} rootList
|
||||
* @returns {import("@web/model/relational_model/record").Record[]}
|
||||
*/
|
||||
const getGroupRecords = async (group, rootList) => {
|
||||
if (!group) {
|
||||
return [];
|
||||
}
|
||||
await ensureGroupFullyLoaded(group);
|
||||
if (group.list.isGrouped) {
|
||||
const records = [];
|
||||
for (const nested of group.list.groups || []) {
|
||||
records.push(...(await getGroupRecords(nested, rootList)));
|
||||
}
|
||||
return records;
|
||||
}
|
||||
return [...group.list.records];
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerate every top-level group visible on the controller.
|
||||
*
|
||||
* @param {ListController} controller
|
||||
*/
|
||||
const iterateVisibleGroups = (controller) => {
|
||||
const mainList = controller.model.root;
|
||||
const groups = [];
|
||||
if (mainList.isGrouped) {
|
||||
for (const group of mainList.groups) {
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
patch(ListController.prototype, {
|
||||
getStaticActionMenuItems() {
|
||||
const items = super.getStaticActionMenuItems(...arguments);
|
||||
items.selectAllGroups = {
|
||||
isAvailable: () => this.model.root.isGrouped,
|
||||
sequence: 5,
|
||||
icon: "fa fa-check-square-o",
|
||||
description: _t("Select All in Visible Groups"),
|
||||
callback: () => this.onSelectAllGroups(),
|
||||
};
|
||||
return items;
|
||||
},
|
||||
|
||||
async onSelectAllGroups() {
|
||||
const list = this.model.root;
|
||||
if (!list.isGrouped) {
|
||||
return;
|
||||
}
|
||||
const groups = iterateVisibleGroups(this);
|
||||
for (const group of groups) {
|
||||
const records = await getGroupRecords(group, list);
|
||||
for (const record of records) {
|
||||
if (!record.selected) {
|
||||
record.toggleSelection(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.model.root.selectDomain(false);
|
||||
this.model.root.notify();
|
||||
},
|
||||
});
|
||||
@ -1,181 +1,181 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
const collectGroupRecords = (rootGroup) => {
|
||||
const records = [];
|
||||
const seen = new Set();
|
||||
const stack = rootGroup ? [rootGroup] : [];
|
||||
while (stack.length) {
|
||||
const group = stack.pop();
|
||||
if (!group || !group.list) {
|
||||
continue;
|
||||
}
|
||||
if (group.list.isGrouped) {
|
||||
for (const nestedGroup of group.list.groups || []) {
|
||||
stack.push(nestedGroup);
|
||||
}
|
||||
} else {
|
||||
for (const record of group.list.records || []) {
|
||||
if (!record || seen.has(record)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(record);
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
return records;
|
||||
};
|
||||
|
||||
const computeSelectionState = (records) => {
|
||||
if (!records.length) {
|
||||
return "none";
|
||||
}
|
||||
let selectedCount = 0;
|
||||
for (const record of records) {
|
||||
if (record.selected) {
|
||||
selectedCount++;
|
||||
}
|
||||
}
|
||||
if (selectedCount === 0) {
|
||||
return "none";
|
||||
}
|
||||
if (selectedCount === records.length) {
|
||||
return "full";
|
||||
}
|
||||
return "partial";
|
||||
};
|
||||
|
||||
patch(ListRenderer.prototype, {
|
||||
/**
|
||||
* Return every record belonging to the provided group, traversing nested groups.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @returns {import("@web/model/relational_model/record").Record[]}
|
||||
*/
|
||||
getAllRecordsInGroup(group) {
|
||||
return collectGroupRecords(group);
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute the current selection state for the provided group.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @returns {"none"|"partial"|"full"}
|
||||
*/
|
||||
getGroupSelectionState(group) {
|
||||
return computeSelectionState(this.getAllRecordsInGroup(group));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
isGroupSelectionFull(group) {
|
||||
return this.getGroupSelectionState(group) === "full";
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
isGroupSelectionPartial(group) {
|
||||
return this.getGroupSelectionState(group) === "partial";
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle click interaction on the custom group selector control.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
onGroupCheckboxClick(ev, group) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.toggleGroupSelection(group);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle keyboard interaction on the custom group selector control.
|
||||
*
|
||||
* @param {KeyboardEvent} ev
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
onGroupCheckboxKeydown(ev, group) {
|
||||
const { key } = ev;
|
||||
if (key === " " || key === "Enter") {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.toggleGroupSelection(group);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure all records (and nested sub-groups) for the provided group are loaded.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
async ensureGroupFullyLoaded(group) {
|
||||
if (!group || !group.list) {
|
||||
return;
|
||||
}
|
||||
const list = group.list;
|
||||
if (list.isGrouped) {
|
||||
for (const nestedGroup of list.groups || []) {
|
||||
await this.ensureGroupFullyLoaded(nestedGroup);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let targetCount = group.count ?? list.count ?? list.records.length;
|
||||
if (list.hasLimitedCount) {
|
||||
await list.fetchCount();
|
||||
targetCount = Math.max(targetCount, list.count || 0);
|
||||
}
|
||||
targetCount = Math.max(targetCount, list.records.length);
|
||||
if (targetCount > list.records.length) {
|
||||
await list.load({ offset: 0, limit: targetCount });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply a selection state to the provided group.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @param {boolean} [shouldSelect]
|
||||
*/
|
||||
async selectRecordsForGroup(group, shouldSelect) {
|
||||
if (!this.canSelectRecord) {
|
||||
return;
|
||||
}
|
||||
await this.ensureGroupFullyLoaded(group);
|
||||
const records = this.getAllRecordsInGroup(group);
|
||||
if (!records.length) {
|
||||
return;
|
||||
}
|
||||
const selectionState = computeSelectionState(records);
|
||||
const desiredSelection =
|
||||
shouldSelect === undefined ? selectionState !== "full" : Boolean(shouldSelect);
|
||||
|
||||
for (const record of records) {
|
||||
if (record.selected !== desiredSelection) {
|
||||
record.toggleSelection(desiredSelection);
|
||||
}
|
||||
}
|
||||
|
||||
if (desiredSelection && records.length) {
|
||||
this.lastCheckedRecord = records[records.length - 1];
|
||||
}
|
||||
|
||||
this.props.list.selectDomain(false);
|
||||
this.render(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle selection for all records belonging to the provided group (including sub-groups).
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
async toggleGroupSelection(group) {
|
||||
await this.selectRecordsForGroup(group);
|
||||
},
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
const collectGroupRecords = (rootGroup) => {
|
||||
const records = [];
|
||||
const seen = new Set();
|
||||
const stack = rootGroup ? [rootGroup] : [];
|
||||
while (stack.length) {
|
||||
const group = stack.pop();
|
||||
if (!group || !group.list) {
|
||||
continue;
|
||||
}
|
||||
if (group.list.isGrouped) {
|
||||
for (const nestedGroup of group.list.groups || []) {
|
||||
stack.push(nestedGroup);
|
||||
}
|
||||
} else {
|
||||
for (const record of group.list.records || []) {
|
||||
if (!record || seen.has(record)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(record);
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
return records;
|
||||
};
|
||||
|
||||
const computeSelectionState = (records) => {
|
||||
if (!records.length) {
|
||||
return "none";
|
||||
}
|
||||
let selectedCount = 0;
|
||||
for (const record of records) {
|
||||
if (record.selected) {
|
||||
selectedCount++;
|
||||
}
|
||||
}
|
||||
if (selectedCount === 0) {
|
||||
return "none";
|
||||
}
|
||||
if (selectedCount === records.length) {
|
||||
return "full";
|
||||
}
|
||||
return "partial";
|
||||
};
|
||||
|
||||
patch(ListRenderer.prototype, {
|
||||
/**
|
||||
* Return every record belonging to the provided group, traversing nested groups.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @returns {import("@web/model/relational_model/record").Record[]}
|
||||
*/
|
||||
getAllRecordsInGroup(group) {
|
||||
return collectGroupRecords(group);
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute the current selection state for the provided group.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @returns {"none"|"partial"|"full"}
|
||||
*/
|
||||
getGroupSelectionState(group) {
|
||||
return computeSelectionState(this.getAllRecordsInGroup(group));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
isGroupSelectionFull(group) {
|
||||
return this.getGroupSelectionState(group) === "full";
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
isGroupSelectionPartial(group) {
|
||||
return this.getGroupSelectionState(group) === "partial";
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle click interaction on the custom group selector control.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
onGroupCheckboxClick(ev, group) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.toggleGroupSelection(group);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle keyboard interaction on the custom group selector control.
|
||||
*
|
||||
* @param {KeyboardEvent} ev
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
onGroupCheckboxKeydown(ev, group) {
|
||||
const { key } = ev;
|
||||
if (key === " " || key === "Enter") {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.toggleGroupSelection(group);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure all records (and nested sub-groups) for the provided group are loaded.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
async ensureGroupFullyLoaded(group) {
|
||||
if (!group || !group.list) {
|
||||
return;
|
||||
}
|
||||
const list = group.list;
|
||||
if (list.isGrouped) {
|
||||
for (const nestedGroup of list.groups || []) {
|
||||
await this.ensureGroupFullyLoaded(nestedGroup);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let targetCount = group.count ?? list.count ?? list.records.length;
|
||||
if (list.hasLimitedCount) {
|
||||
await list.fetchCount();
|
||||
targetCount = Math.max(targetCount, list.count || 0);
|
||||
}
|
||||
targetCount = Math.max(targetCount, list.records.length);
|
||||
if (targetCount > list.records.length) {
|
||||
await list.load({ offset: 0, limit: targetCount });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply a selection state to the provided group.
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
* @param {boolean} [shouldSelect]
|
||||
*/
|
||||
async selectRecordsForGroup(group, shouldSelect) {
|
||||
if (!this.canSelectRecord) {
|
||||
return;
|
||||
}
|
||||
await this.ensureGroupFullyLoaded(group);
|
||||
const records = this.getAllRecordsInGroup(group);
|
||||
if (!records.length) {
|
||||
return;
|
||||
}
|
||||
const selectionState = computeSelectionState(records);
|
||||
const desiredSelection =
|
||||
shouldSelect === undefined ? selectionState !== "full" : Boolean(shouldSelect);
|
||||
|
||||
for (const record of records) {
|
||||
if (record.selected !== desiredSelection) {
|
||||
record.toggleSelection(desiredSelection);
|
||||
}
|
||||
}
|
||||
|
||||
if (desiredSelection && records.length) {
|
||||
this.lastCheckedRecord = records[records.length - 1];
|
||||
}
|
||||
|
||||
this.props.list.selectDomain(false);
|
||||
this.render(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle selection for all records belonging to the provided group (including sub-groups).
|
||||
*
|
||||
* @param {import("@web/model/relational_model/group").Group} group
|
||||
*/
|
||||
async toggleGroupSelection(group) {
|
||||
await this.selectRecordsForGroup(group);
|
||||
},
|
||||
});
|
||||
@ -1,19 +1,19 @@
|
||||
.o_list_group_selector {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.form-check-input {
|
||||
cursor: pointer;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
& .form-check.indeterminate .form-check-input {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.o_list_group_selector {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.form-check-input {
|
||||
cursor: pointer;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
& .form-check.indeterminate .form-check-input {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="list_view_group_select.ListRendererGroupRow" t-inherit="web.ListRenderer.GroupRow" t-inherit-mode="extension">
|
||||
<xpath expr="//th[@t-att-colspan='getGroupNameCellColSpan(group)']/div" position="inside">
|
||||
<t t-if="hasSelectors">
|
||||
<div class="o_list_group_selector me-2">
|
||||
<div
|
||||
class="form-check mb-0"
|
||||
t-attf-class="{{ this.isGroupSelectionPartial(group) ? 'indeterminate' : '' }}"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
tabindex="0"
|
||||
t-att-checked="this.isGroupSelectionFull(group) ? 'checked' : undefined"
|
||||
t-att-disabled="!this.canSelectRecord ? 'disabled' : undefined"
|
||||
t-on-click.stop="(ev) => this.onGroupCheckboxClick(ev, group)"
|
||||
t-on-keydown.stop="(ev) => this.onGroupCheckboxKeydown(ev, group)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="list_view_group_select.ListRendererGroupRow" t-inherit="web.ListRenderer.GroupRow" t-inherit-mode="extension">
|
||||
<xpath expr="//th[@t-att-colspan='getGroupNameCellColSpan(group)']/div" position="inside">
|
||||
<t t-if="hasSelectors">
|
||||
<div class="o_list_group_selector me-2">
|
||||
<div
|
||||
class="form-check mb-0"
|
||||
t-attf-class="{{ this.isGroupSelectionPartial(group) ? 'indeterminate' : '' }}"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
tabindex="0"
|
||||
t-att-checked="this.isGroupSelectionFull(group) ? 'checked' : undefined"
|
||||
t-att-disabled="!this.canSelectRecord ? 'disabled' : undefined"
|
||||
t-on-click.stop="(ev) => this.onGroupCheckboxClick(ev, group)"
|
||||
t-on-keydown.stop="(ev) => this.onGroupCheckboxKeydown(ev, group)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
Loading…
Reference in New Issue
Block a user