Skip to content

Commit 224c61f

Browse files
juliusc2066aab-odoo
authored andcommitted
[IMP] web, account: Allow resequencing across groups in list view
This commit enables users to resequence records between groups in grouped list views, similar to the behavior in ungrouped lists. - The resequencing logic mirrors the approach already used in Kanban views. - To support this feature, handle fields are now retained in grouped views instead of being removed. Part of task-4613142 closes odoo#207198 Related: odoo/enterprise#84002 Signed-off-by: Aaron Bohy (aab) <[email protected]>
1 parent 3150edd commit 224c61f

File tree

5 files changed

+114
-25
lines changed

5 files changed

+114
-25
lines changed

addons/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field_o2m.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,20 @@ export class ProductLabelSectionAndNoteListRender extends SectionAndNoteListRend
3838
return super.processAllColumn(allColumns, list);
3939
}
4040

41-
getActiveColumns(list) {
42-
let activeColumns = super.getActiveColumns(list);
41+
getActiveColumns() {
42+
let activeColumns = super.getActiveColumns();
4343
const productCol = activeColumns.find((col) => this.productColumns.includes(col.name));
4444
const labelCol = activeColumns.find((col) => col.name === "name");
4545

4646
if (productCol) {
4747
if (labelCol) {
48-
list.records.forEach((record) => (record.columnIsProductAndLabel = true));
48+
this.props.list.records.forEach(
49+
(record) => (record.columnIsProductAndLabel = true)
50+
);
4951
} else {
50-
list.records.forEach((record) => (record.columnIsProductAndLabel = false));
52+
this.props.list.records.forEach(
53+
(record) => (record.columnIsProductAndLabel = false)
54+
);
5155
}
5256
activeColumns = activeColumns.filter((col) => col.name !== "name");
5357
this.titleField = productCol.name;

addons/sale/static/src/js/sale_order_line_field/sale_order_line_field.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export class SaleOrderLineListRenderer extends ProductLabelSectionAndNoteListRen
2828
super.getCellTitle(column, record);
2929
}
3030

31-
getActiveColumns(list) {
32-
let activeColumns = super.getActiveColumns(list);
31+
getActiveColumns() {
32+
let activeColumns = super.getActiveColumns();
3333
let productTmplCol = activeColumns.find((col) => col.name === 'product_template_id');
3434
let productCol = activeColumns.find((col) => col.name === 'product_id');
3535

addons/web/static/src/views/list/list_renderer.js

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,11 @@ export class ListRenderer extends Component {
167167
this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);
168168
Object.assign(this.optionalActiveFields, this.computeOptionalActiveFields());
169169
this.debugOpenView = exprToBoolean(browser.localStorage.getItem(this.keyDebugOpenView));
170-
this.columns = this.getActiveColumns(this.props.list);
170+
this.columns = this.getActiveColumns();
171171
this.withHandleColumn = this.columns.some((col) => col.widget === "handle");
172172
});
173173
let dataRowId;
174+
let dataGroupId;
174175
this.rootRef = useRef("root");
175176
this.resequencePromise = Promise.resolve();
176177
useSortable({
@@ -185,10 +186,11 @@ export class ListRenderer extends Component {
185186
onDragStart: (params) => {
186187
const { element } = params;
187188
dataRowId = element.dataset.id;
189+
dataGroupId = this.props.list.isGrouped && element.dataset.groupId;
188190
return this.sortStart(params);
189191
},
190192
onDragEnd: (params) => this.sortStop(params),
191-
onDrop: (params) => this.sortDrop(dataRowId, params),
193+
onDrop: (params) => this.sortDrop(dataRowId, dataGroupId, params),
192194
});
193195

194196
if (this.env.searchModel) {
@@ -270,14 +272,8 @@ export class ListRenderer extends Component {
270272
});
271273
}
272274

273-
/**
274-
* @param {DynamicList | StaticList} list
275-
*/
276-
getActiveColumns(list) {
275+
getActiveColumns() {
277276
return this.allColumns.filter((col) => {
278-
if (list.isGrouped && col.widget === "handle") {
279-
return false; // no handle column if the list is grouped
280-
}
281277
if (col.optional && !this.optionalActiveFields[col.name]) {
282278
return false;
283279
}
@@ -693,7 +689,7 @@ export class ListRenderer extends Component {
693689
const { widget, attrs } = column;
694690
const field = this.props.list.fields[column.name];
695691
const aggregateValue = group.aggregates[column.name];
696-
if (!(column.name in group.aggregates)) {
692+
if (!(column.name in group.aggregates) || widget === "handle") {
697693
return "";
698694
}
699695
const formatter = formatters.get(widget, false) || formatters.get(field.type, false);
@@ -955,7 +951,9 @@ export class ListRenderer extends Component {
955951
// [ group name ][ aggregate cells ][ pager]
956952
// TODO: move this somewhere, compute this only once (same result for each groups actually) ?
957953
getFirstAggregateIndex(group) {
958-
return this.columns.findIndex((col) => col.name in group.aggregates);
954+
return this.columns.findIndex(
955+
(col) => col.name in group.aggregates && col.widget !== "handle"
956+
);
959957
}
960958
getLastAggregateIndex(group) {
961959
const reversedColumns = [...this.columns].reverse(); // reverse is destructive
@@ -2011,14 +2009,23 @@ export class ListRenderer extends Component {
20112009
* @param {HTMLElement} [params.parent]
20122010
* @param {HTMLElement} [params.previous]
20132011
*/
2014-
async sortDrop(dataRowId, { element, previous }) {
2012+
async sortDrop(dataRowId, dataGroupId, { element, previous }) {
20152013
await this.props.list.leaveEditMode();
20162014
element.classList.remove("o_row_draggable");
20172015
const refId = previous ? previous.dataset.id : null;
20182016
try {
2019-
this.resequencePromise = this.props.list.resequence(dataRowId, refId, {
2020-
handleField: this.props.list.handleField,
2021-
});
2017+
if (dataGroupId) {
2018+
this.resequencePromise = this.props.list.moveRecord(
2019+
dataRowId,
2020+
dataGroupId,
2021+
refId,
2022+
previous.dataset.groupId
2023+
);
2024+
} else {
2025+
this.resequencePromise = this.props.list.resequence(dataRowId, refId, {
2026+
handleField: this.props.list.handleField,
2027+
});
2028+
}
20222029
await this.resequencePromise;
20232030
} finally {
20242031
element.classList.add("o_row_draggable");

addons/web/static/src/views/list/list_renderer.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
<t t-if="!group.isFolded">
158158
<t t-call="{{ constructor.rowsTemplate }}">
159159
<t t-set="list" t-value="group.list"/>
160+
<t t-set="groupId" t-value="group.id"/>
160161
</t>
161162
<tr t-if="!group.list.isGrouped and props.editable and canCreate">
162163
<td t-if="hasSelectors"/>
@@ -179,7 +180,8 @@
179180
</t>
180181

181182
<t t-name="web.ListRenderer.GroupRow">
182-
<tr t-attf-class="{{group.count > 0 ? 'o_group_has_content' : ''}} o_group_header {{!group.isFolded ? 'o_group_open' : ''}} cursor-pointer"
183+
<tr t-attf-class="{{group.count > 0 ? 'o_group_has_content' : ''}} o_group_header {{!group.isFolded ? 'o_group_open' : ''}} cursor-pointer {{ canResequenceRows and group_index > 0 ? 'o_row_draggable' : '' }}"
184+
t-att-data-group-id="group.id"
183185
t-on-click="(ev) => this.onGroupHeaderClicked(ev, group)"
184186
>
185187
<th t-on-keydown="(ev) => this.onCellKeydown(ev, group)"
@@ -229,6 +231,7 @@
229231
<tr class="o_data_row"
230232
t-att-class="getRowClass(record)"
231233
t-att-data-id="record.id"
234+
t-att-data-group-id="groupId"
232235
t-on-click.capture="(ev) => this.onClickCapture(record, ev)"
233236
t-on-mouseover.capture="(ev) => this.ignoreEventInSelectionMode(ev)"
234237
t-on-mouseout.capture="(ev) => this.ignoreEventInSelectionMode(ev)"

addons/web/static/tests/views/list/list_view.test.js

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,12 +1880,13 @@ test(`basic grouped list rendering with widget="handle" col`, async () => {
18801880
`,
18811881
groupBy: ["bar"],
18821882
});
1883-
expect(`thead th:not(.o_list_record_selector)`).toHaveCount(2, {
1884-
message: "should have 1 th for checkbox (desktop only), 1 th for Foo, 1 th for Bar",
1883+
expect(`thead th:not(.o_list_record_selector)`).toHaveCount(3, {
1884+
message:
1885+
"should have 1 th for checkbox (desktop only), 1 th for handle, 1 th for Foo, 1 th for Bar",
18851886
});
18861887
expect(`thead th[data-name=foo]`).toHaveCount(1);
18871888
expect(`thead th[data-name=bar]`).toHaveCount(1);
1888-
expect(`thead th[data-name=int_field]`).toHaveCount(0);
1889+
expect(`thead th[data-name=int_field]`).toHaveCount(1);
18891890
expect(`tr.o_group_header`).toHaveCount(2);
18901891
expect(`th.o_group_name`).toHaveCount(2);
18911892
expect(`.o_group_header:eq(0) th`).toHaveCount(2); // group name + colspan 2
@@ -10071,6 +10072,80 @@ test(`editable list with handle widget`, async () => {
1007110072
});
1007210073
});
1007310074

10075+
test.tags("desktop");
10076+
test(`editable grouped list with handle widget`, async () => {
10077+
// resequence makes sense on a sequence field, not on arbitrary fields
10078+
Foo._records[0].int_field = 0;
10079+
Foo._records[1].int_field = 1;
10080+
Foo._records[2].int_field = 2;
10081+
Foo._records[3].int_field = 3;
10082+
10083+
onRpc("web_resequence", async ({ args, kwargs }) => {
10084+
expect.step(["web_resequence", args[0], kwargs.field_name, kwargs.offset]);
10085+
});
10086+
onRpc("web_save", async ({ args }) => {
10087+
expect.step(["web_save", args[0]]);
10088+
});
10089+
10090+
await mountView({
10091+
resModel: "foo",
10092+
type: "list",
10093+
arch: `
10094+
<list editable="top" default_order="int_field">
10095+
<field name="int_field" widget="handle"/>
10096+
<field name="amount" widget="float" digits="[5,0]"/>
10097+
</list>
10098+
`,
10099+
groupBy: ["bar"],
10100+
});
10101+
expect(`.o_group_header`).toHaveCount(2);
10102+
await contains(`.o_group_header:first`).click();
10103+
await contains(`.o_group_header:last`).click();
10104+
expect(`.o_group_header:first`).toHaveText("No (1)\n 0");
10105+
expect(`.o_group_header:last`).toHaveText("Yes (3)\n 2,000");
10106+
expect(`tbody .o_data_row:eq(0) td:last`).toHaveText("0", {
10107+
message: "default fourth record should have amount 0",
10108+
});
10109+
expect(`tbody .o_data_row:eq(1) td:last`).toHaveText("1,200", {
10110+
message: "default first record should have amount 1,200",
10111+
});
10112+
expect(`tbody .o_data_row:eq(2) td:last`).toHaveText("500", {
10113+
message: "default second record should have amount 500",
10114+
});
10115+
expect(`tbody .o_data_row:eq(3) td:last`).toHaveText("300", {
10116+
message: "default third record should have amount 300",
10117+
});
10118+
10119+
// Drag and drop the fourth line in second position
10120+
await contains(`tbody .o_data_row:eq(3) .o_handle_cell`).dragAndDrop(
10121+
queryFirst(`tbody tr:eq(1)`)
10122+
);
10123+
expect.verifySteps([
10124+
["web_save", [3]],
10125+
["web_resequence", [3], "int_field", 2],
10126+
]);
10127+
// Aggregates are not updated, todo later?
10128+
expect(`.o_group_header:first`).toHaveText("No (2)\n 0");
10129+
expect(`.o_group_header:last`).toHaveText("Yes (2)\n 2,000");
10130+
expect(`tbody .o_data_row:eq(0) td:last`).toHaveText("300", {
10131+
message: "new first record should have amount 300",
10132+
});
10133+
expect(`tbody .o_data_row:eq(1) td:last`).toHaveText("0", {
10134+
message: "new second record should have amount 0",
10135+
});
10136+
expect(`tbody .o_data_row:eq(2) td:last`).toHaveText("1,200", {
10137+
message: "new third record should have amount 1,200",
10138+
});
10139+
expect(`tbody .o_data_row:eq(3) td:last`).toHaveText("500", {
10140+
message: "new fourth record should have amount 500",
10141+
});
10142+
10143+
await contains(`tbody .o_data_row:eq(0) div[name='amount']`).click();
10144+
expect(`tbody .o_data_row:eq(0) td:last input`).toHaveValue("300", {
10145+
message: "the edited record should be the good one",
10146+
});
10147+
});
10148+
1007410149
test(`editable target, handle widget locks and unlocks on sort`, async () => {
1007510150
// resequence makes sense on a sequence field, not on arbitrary fields
1007610151
Foo._records[0].int_field = 0;

0 commit comments

Comments
 (0)