[FIX] mail: onAdd/onDelete hooks on many without inverse

The opportunity is taken to fix an issue where removing a record from
assign would not update the inverse.

closes odoo/odoo#152442

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2024-02-03 06:59:46 +00:00
parent f02397dc5e
commit 4006e346f9
2 changed files with 95 additions and 11 deletions
+20 -11
View File
@@ -669,22 +669,31 @@ class RecordList extends Array {
return Record.MAKE_UPDATE(function recordListAssign() {
/** @type {Record[]|Set<Record>|RecordList<Record|any[]>} */
const collection = Record.isRecord(data) ? [data] : data;
// l1 and collection could be same record list,
// data and collection could be same record list,
// save before clear to not push mutated recordlist that is empty
const vals = [...collection];
/** @type {R[]} */
const oldRecordsProxy = recordList._proxyInternal.slice.call(recordList._proxy);
for (const oldRecordProxy of oldRecordsProxy) {
toRaw(oldRecordProxy)._raw.__uses__.delete(recordList);
}
const recordsProxy = vals.map((val) =>
const oldRecords = recordList._proxyInternal.slice
.call(recordList._proxy)
.map((recordProxy) => toRaw(recordProxy)._raw);
const newRecords = vals.map((val) =>
recordList._insert(val, function recordListAssignInsert(record) {
record.__uses__.add(recordList);
if (record.notIn(oldRecords)) {
record.__uses__.add(recordList);
Record.ADD_QUEUE(recordList.field, "onAdd", record);
}
})
);
recordList._proxy.data = recordsProxy.map(
(recordProxy) => toRaw(recordProxy)._raw.localId
);
const inverse = recordList.fieldDefinition.inverse;
for (const oldRecord of oldRecords) {
if (oldRecord.notIn(newRecords)) {
oldRecord.__uses__.delete(recordList);
Record.ADD_QUEUE(recordList.field, "onDelete", oldRecord);
if (inverse) {
oldRecord._fields.get(inverse).value.delete(recordList.owner);
}
}
}
recordList._proxy.data = newRecords.map((newRecord) => newRecord.localId);
});
}
/** @param {R[]} records */
@@ -4,6 +4,7 @@ import { BaseStore, Record, makeStore, modelRegistry } from "@mail/core/common/r
import { registry } from "@web/core/registry";
import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env";
import { assertSteps, step } from "@web/../tests/utils";
import { markup, reactive, toRaw } from "@odoo/owl";
const serviceRegistry = registry.category("services");
@@ -615,3 +616,77 @@ QUnit.test("store updates can be observed", async (assert) => {
rawStore.Model.store.abc = 3;
assert.verifySteps(["abc:3"], "observable from Model.store");
});
QUnit.test("onAdd/onDelete hooks on one without inverse", async () => {
(class Thread extends Record {
static id = "name";
}).register();
(class Member extends Record {
static id = "name";
name;
thread = Record.one("Thread", {
onAdd: (thread) => step(`thread.onAdd(${thread.name})`),
onDelete: (thread) => step(`thread.onDelete(${thread.name})`),
});
}).register();
const store = await start();
const general = store.Thread.insert("General");
const john = store.Member.insert("John");
await assertSteps([]);
john.thread = general;
await assertSteps(["thread.onAdd(General)"]);
john.thread = general;
await assertSteps([]);
john.thread = undefined;
await assertSteps(["thread.onDelete(General)"]);
});
QUnit.test("onAdd/onDelete hooks on many without inverse", async () => {
(class Thread extends Record {
static id = "name";
name;
members = Record.many("Member", {
onAdd: (member) => step(`members.onAdd(${member.name})`),
onDelete: (member) => step(`members.onDelete(${member.name})`),
});
}).register();
(class Member extends Record {
static id = "name";
}).register();
const store = await start();
const general = store.Thread.insert("General");
const jane = store.Member.insert("Jane");
const john = store.Member.insert("John");
await assertSteps([]);
general.members = jane;
await assertSteps(["members.onAdd(Jane)"]);
general.members = jane;
await assertSteps([]);
general.members = [["ADD", john]];
await assertSteps(["members.onAdd(John)"]);
general.members = undefined;
await assertSteps(["members.onDelete(John)", "members.onDelete(Jane)"]);
});
QUnit.test("record list assign should update inverse fields", async (assert) => {
(class Thread extends Record {
static id = "name";
name;
members = Record.many("Member", { inverse: "thread" });
}).register();
(class Member extends Record {
static id = "name";
thread = Record.one("Thread", { inverse: "members" });
}).register();
const store = await start();
const general = store.Thread.insert("General");
const jane = store.Member.insert("Jane");
general.members = jane; // direct assignation of value goes through assign()
assert.ok(jane.thread.eq(general));
general.members = []; // writing empty array specifically goes through assign()
assert.notOk(jane.thread);
jane.thread = general;
assert.ok(jane.in(general.members));
jane.thread = [];
assert.ok(jane.notIn(general.members));
});