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