[IMP] web: list: select range of records with shift
Before, in the list view, the user could not select a record range easily. He had to check/uncheck manually each checkbox in the range. Now, the user can select a range of records in list view by hitting its first record and and its last record while holding SHIFT for the second. It's also possible to do it with the keyboard: - pressing <SPACE> to select a record and <SHIFT+SPACE> to select a range of records. - pressing <SHIFT+ARROW> to extend to current checkbox task-2039894 Part-of: odoo/odoo#99050
This commit is contained in:
committed by
fdardenne
parent
8414b797c3
commit
edd296da29
@@ -174,6 +174,20 @@ export class ListRenderer extends Component {
|
||||
this.columnWidths = null;
|
||||
this.freezeColumnWidths();
|
||||
});
|
||||
|
||||
useExternalListener(window, "keydown", (ev) => {
|
||||
this.shiftKeyMode = ev.shiftKey;
|
||||
});
|
||||
useExternalListener(window, "keyup", (ev) => {
|
||||
this.shiftKeyMode = ev.shiftKey;
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
if (hotkey === "shift") {
|
||||
this.shiftKeyedRecord = undefined;
|
||||
}
|
||||
});
|
||||
useExternalListener(window, "blur", (ev) => {
|
||||
this.shiftKeyMode = false;
|
||||
});
|
||||
}
|
||||
|
||||
getActiveColumns(list) {
|
||||
@@ -1059,6 +1073,38 @@ export class ListRenderer extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
expandCheckboxes(record, direction) {
|
||||
const { records } = this.props.list;
|
||||
const recordIndex = records.indexOf(record);
|
||||
const shiftKeyedRecordIndex = records.indexOf(this.shiftKeyedRecord);
|
||||
let nextRecord;
|
||||
let isExpanding;
|
||||
switch (direction) {
|
||||
case "up":
|
||||
if (recordIndex <= 0) {
|
||||
return false;
|
||||
}
|
||||
nextRecord = records[recordIndex - 1];
|
||||
isExpanding = shiftKeyedRecordIndex > recordIndex - 1;
|
||||
break;
|
||||
case "down":
|
||||
if (recordIndex === records.length - 1) {
|
||||
return false;
|
||||
}
|
||||
nextRecord = records[recordIndex + 1];
|
||||
isExpanding = shiftKeyedRecordIndex < recordIndex + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isExpanding) {
|
||||
nextRecord.toggleSelection(this.shiftKeyedRecord.selected);
|
||||
} else {
|
||||
record.toggleSelection(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
applyCellKeydownMultiEditMode(hotkey, cell, group, record) {
|
||||
const { list } = this.props;
|
||||
const row = cell.parentElement;
|
||||
@@ -1387,6 +1433,25 @@ export class ListRenderer extends Component {
|
||||
toFocus = buttons[index - 1] || currentButton;
|
||||
}
|
||||
break;
|
||||
case "shift+arrowdown": {
|
||||
if (this.expandCheckboxes(record, "down")) {
|
||||
toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, "down");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "shift+arrowup": {
|
||||
if (this.expandCheckboxes(record, "up")) {
|
||||
toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, "up");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "shift+space":
|
||||
this.toggleRecordSelection(record);
|
||||
toFocus = getElementToFocus(cell);
|
||||
break;
|
||||
case "shift":
|
||||
this.shiftKeyedRecord = record;
|
||||
break;
|
||||
case "enter":
|
||||
if (!group && !record) {
|
||||
return false;
|
||||
@@ -1518,10 +1583,28 @@ export class ListRenderer extends Component {
|
||||
}
|
||||
|
||||
toggleRecordSelection(record) {
|
||||
record.toggleSelection();
|
||||
if (this.shiftKeyMode && this.lastCheckedRecord) {
|
||||
this.toggleRecordShiftSelection(record);
|
||||
} else {
|
||||
record.toggleSelection();
|
||||
}
|
||||
this.lastCheckedRecord = record;
|
||||
this.props.list.selectDomain(false);
|
||||
}
|
||||
|
||||
toggleRecordShiftSelection(record) {
|
||||
const { records } = this.props.list;
|
||||
const recordIndex = records.indexOf(record);
|
||||
const lastCheckedRecordIndex = records.indexOf(this.lastCheckedRecord);
|
||||
const start = Math.min(recordIndex, lastCheckedRecordIndex);
|
||||
const end = Math.max(recordIndex, lastCheckedRecordIndex);
|
||||
const { selected } = record;
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
records[i].toggleSelection(!selected);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleOptionalField(fieldName) {
|
||||
this.optionalActiveFields[fieldName] = !this.optionalActiveFields[fieldName];
|
||||
this.state.columns = this.getActiveColumns(this.props.list);
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
t-on-touchend="() => this.onRowTouchEnd(record)"
|
||||
t-on-touchmove="() => this.onRowTouchMove(record)"
|
||||
>
|
||||
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)" t-if="hasSelectors" class="o_list_record_selector" t-on-click.stop="() => this.toggleRecordSelection(record)" tabindex="-1">
|
||||
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)" t-if="hasSelectors" class="o_list_record_selector user-select-none" t-on-click.stop="() => this.toggleRecordSelection(record)" tabindex="-1">
|
||||
<CheckBox disabled="!!props.list.editedRecord or props.list.model.useSampleModel" value="record.selected" onChange.bind="() => this.toggleRecordSelection(record)" />
|
||||
</td>
|
||||
<t t-foreach="getColumns(record)" t-as="column" t-key="column.id">
|
||||
|
||||
@@ -259,6 +259,158 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.isNotVisible(target.querySelector(".o_list_button_discard"));
|
||||
});
|
||||
|
||||
QUnit.test("select record range with shift click", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
|
||||
});
|
||||
await click(target.querySelectorAll(".o_data_row .o_list_record_selector input")[0]);
|
||||
assert.containsOnce(target.querySelector(".o_cp_buttons"), ".o_list_selection_box");
|
||||
assert.containsNone(target.querySelector(".o_list_selection_box"), ".o_list_select_domain");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_list_selection_box").textContent.trim(),
|
||||
"1 selected"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length,
|
||||
1
|
||||
);
|
||||
|
||||
// shift click the 4th record to have 0-1-2-3 toggled
|
||||
await triggerEvents(
|
||||
target.querySelectorAll(".o_data_row .o_list_record_selector input")[3],
|
||||
null,
|
||||
[["keydown", { key: "Shift", shiftKey: true }], "click"]
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_list_selection_box").textContent.trim(),
|
||||
"4 selected"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length,
|
||||
4
|
||||
);
|
||||
|
||||
// shift click the 3rd record to untoggle 2-3
|
||||
await triggerEvents(
|
||||
target.querySelectorAll(".o_data_row .o_list_record_selector input")[2],
|
||||
null,
|
||||
[["keydown", { key: "Shift", shiftKey: true }], "click"]
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_list_selection_box").textContent.trim(),
|
||||
"2 selected"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length,
|
||||
2
|
||||
);
|
||||
|
||||
// shift click the 1st record to untoggle 0-1
|
||||
await triggerEvents(
|
||||
target.querySelectorAll(".o_data_row .o_list_record_selector input")[0],
|
||||
null,
|
||||
[["keydown", { key: "Shift", shiftKey: true }], "click"]
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("select record range with shift+space", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
|
||||
});
|
||||
|
||||
// Go to the first checkbox and check it
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowDown");
|
||||
await nextTick();
|
||||
let checkbox = target.querySelector(
|
||||
".o_data_row:nth-child(1) .o_list_record_selector input"
|
||||
);
|
||||
assert.strictEqual(document.activeElement, checkbox);
|
||||
await click(checkbox);
|
||||
assert.ok(checkbox.checked);
|
||||
|
||||
// Go to the fourth checkbox and shift+space
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowDown");
|
||||
await nextTick();
|
||||
checkbox = target.querySelector(".o_data_row:nth-child(4) .o_list_record_selector input");
|
||||
assert.strictEqual(document.activeElement, checkbox);
|
||||
assert.ok(!checkbox.checked);
|
||||
await triggerEvents(document.activeElement, null, [
|
||||
["keydown", { key: "Shift", shiftKey: true }],
|
||||
["keydown", { key: " ", shiftKey: true }],
|
||||
]);
|
||||
// focus is on the input and not in the td cell
|
||||
assert.strictEqual(document.activeElement.tagName, "INPUT");
|
||||
|
||||
// Check that all checkbox is checked
|
||||
for (let i = 1; i < 5; i++) {
|
||||
checkbox = target.querySelector(
|
||||
`.o_data_row:nth-child(${i}) .o_list_record_selector input`
|
||||
);
|
||||
assert.ok(checkbox.checked);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("expand range of checkbox with shift+arrow", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
|
||||
});
|
||||
|
||||
// Go to the first checkbox and check it
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowDown");
|
||||
await nextTick();
|
||||
let checkbox = target.querySelector(
|
||||
".o_data_row:nth-child(1) .o_list_record_selector input"
|
||||
);
|
||||
assert.strictEqual(document.activeElement, checkbox);
|
||||
await click(checkbox);
|
||||
assert.ok(checkbox.checked);
|
||||
|
||||
// expand the checkbox with arrowdown
|
||||
await triggerEvent(document.activeElement, null, "keydown", {
|
||||
key: "Shift",
|
||||
shiftKey: true,
|
||||
});
|
||||
triggerHotkey("shift+ArrowDown");
|
||||
triggerHotkey("shift+ArrowDown");
|
||||
triggerHotkey("shift+ArrowDown");
|
||||
triggerHotkey("shift+ArrowUp");
|
||||
await nextTick();
|
||||
await triggerEvent(document.activeElement, null, "keyup", {
|
||||
key: "Shift",
|
||||
shiftKey: false,
|
||||
});
|
||||
|
||||
checkbox = target.querySelector(".o_data_row:nth-child(3) .o_list_record_selector input");
|
||||
assert.strictEqual(document.activeElement, checkbox);
|
||||
assert.ok(checkbox.checked);
|
||||
|
||||
// Check that the three checkbox are checked
|
||||
for (let i = 1; i < 4; i++) {
|
||||
checkbox = target.querySelector(
|
||||
`.o_data_row:nth-child(${i}) .o_list_record_selector input`
|
||||
);
|
||||
assert.ok(checkbox.checked);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("list with class", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
@@ -1993,8 +2145,7 @@ QUnit.module("Views", (hooks) => {
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch:
|
||||
'<tree js_class="custom_list" editable="top"><field name="foo" required="1"/></tree>',
|
||||
arch: '<tree js_class="custom_list" editable="top"><field name="foo" required="1"/></tree>',
|
||||
mockRPC: async (route, args) => {
|
||||
if (args.method === "write") {
|
||||
assert.step(`write ${args.args[0]}`);
|
||||
@@ -2035,8 +2186,7 @@ QUnit.module("Views", (hooks) => {
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch:
|
||||
'<tree js_class="custom_list" editable="top" expand="1"><field name="foo" required="1"/></tree>',
|
||||
arch: '<tree js_class="custom_list" editable="top" expand="1"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ["bar"],
|
||||
mockRPC: async (route, args) => {
|
||||
if (args.method === "write") {
|
||||
@@ -3048,8 +3198,7 @@ QUnit.module("Views", (hooks) => {
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
groupBy: ["m2o"],
|
||||
arch:
|
||||
'<tree editable="bottom"><field name="foo" /><field name="int_field" sum="Sum"/></tree>',
|
||||
arch: '<tree editable="bottom"><field name="foo" /><field name="int_field" sum="Sum"/></tree>',
|
||||
});
|
||||
const groupHeaders = target.querySelectorAll(".o_group_header");
|
||||
assert.strictEqual(
|
||||
|
||||
Reference in New Issue
Block a user