[REF] web: RelationalModel: add option to save after update

This commit adds an option to save the record after an update. When the
save option is set to true, the record will be saved and reload after
the update. Note that, the onChange is not done in this case.

Part-of task-id~3179751

closes odoo/odoo#130328

Related: odoo/enterprise#44905
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2023-08-29 11:10:45 +00:00
parent c21ff19a32
commit 4bf77b9ef1
14 changed files with 41 additions and 200 deletions
@@ -41,13 +41,13 @@ export class ProjectTaskKanbanDynamicGroupList extends RelationalModel.DynamicGr
}
export class ProjectTaskRecord extends RelationalModel.Record {
async _update(changes, options) {
_update(changes, options) {
const value = changes.personal_stage_type_ids;
if (Array.isArray(value)) {
delete changes.personal_stage_type_ids;
changes.personal_stage_type_id = value;
}
await super._update(changes, options);
return super._update(changes, options);
}
get context() {
@@ -116,10 +116,10 @@ export class DynamicGroupList extends DynamicList {
sourceGroup._addRecord(record, oldIndex);
};
try {
await record.update({ [targetGroup.groupByField.name]: value });
const res = await record.save({ noReload: true });
const changes = { [targetGroup.groupByField.name]: value };
const res = await record.update(changes, { save: true });
if (!res) {
revert();
return revert();
}
} catch (e) {
// revert changes
@@ -237,11 +237,11 @@ export class Record extends DataPoint {
return this.model.mutex.exec(() => this._toggleArchive(false));
}
update(changes) {
update(changes, { save } = {}) {
if (this.model._urgentSave) {
return this._update(changes);
return this._update(changes, { save: false }); // save is already scheduled
}
return this.model.mutex.exec(() => this._update(changes));
return this.model.mutex.exec(() => this._update(changes, { save }));
}
urgentSave() {
@@ -913,7 +913,7 @@ export class Record extends DataPoint {
}
}
async _update(changes, { withoutOnchange, withoutParentUpdate } = {}) {
async _update(changes, { withoutOnchange, withoutParentUpdate, save } = {}) {
this.dirty = true;
const prom = this._preprocessChanges(changes);
if (prom && !this.model._urgentSave) {
@@ -935,7 +935,7 @@ export class Record extends DataPoint {
const onChangeFields = Object.keys(changes).filter(
(fieldName) => this.activeFields[fieldName] && this.activeFields[fieldName].onChange
);
if (onChangeFields.length && !this.model._urgentSave && !withoutOnchange) {
if (onChangeFields.length && !this.model._urgentSave && !withoutOnchange && !save) {
const localChanges = this._getChanges(
{ ...this._changes, ...changes },
{ withReadonly: true }
@@ -976,5 +976,8 @@ export class Record extends DataPoint {
}
await this.model.hooks.onRecordChanged(this, this._getChanges());
}
if (save) {
return this._save();
}
}
}
@@ -19,10 +19,8 @@ export class BooleanFavoriteField extends Component {
};
async update() {
await this.props.record.update({ [this.props.name]: !this.props.record.data[this.props.name] });
if (this.props.autosave) {
await this.props.record.save();
}
const changes = { [this.props.name]: !this.props.record.data[this.props.name] };
await this.props.record.update(changes, { save: this.props.autosave });
}
}
@@ -12,10 +12,8 @@ export class BooleanToggleField extends BooleanField {
};
async onChange(newValue) {
await this.props.record.update({ [this.props.name]: newValue });
if (this.props.autosave) {
return this.props.record.save();
}
const changes = { [this.props.name]: newValue };
await this.props.record.update(changes, { save: this.props.autosave });
}
}
@@ -8,12 +8,8 @@ export class ListBooleanToggleField extends BooleanToggleField {
async onClick() {
if (!this.props.readonly && this.props.record.isInEdition) {
await this.props.record.update({
[this.props.name]: !this.props.record.data[this.props.name],
});
if (this.props.autosave) {
return this.props.record.save();
}
const changes = { [this.props.name]: !this.props.record.data[this.props.name] };
await this.props.record.update(changes, { save: this.props.autosave });
}
}
}
@@ -352,10 +352,10 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
if (tagRecord.data[this.props.colorField] != 0) {
this.previousColorsMap[tagRecord.resId] = tagRecord.data[this.props.colorField];
}
tagRecord.update({
const changes = {
[this.props.colorField]: isHidden ? 0 : this.previousColorsMap[tagRecord.resId] || 1,
});
tagRecord.save();
};
tagRecord.update(changes, { save: true });
this.popover.close();
}
@@ -363,8 +363,7 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
const tagRecord = this.props.record.data[this.props.name].records.find(
(record) => record.id === tag.id
);
tagRecord.update({ [this.props.colorField]: colorIndex });
tagRecord.save();
tagRecord.update({ [this.props.colorField]: colorIndex }, { save: true });
this.popover.close();
}
}
@@ -82,10 +82,7 @@ export class PriorityField extends Component {
}
async updateRecord(value) {
await this.props.record.update({ [this.props.name]: value });
if (this.props.autosave) {
return this.props.record.save();
}
await this.props.record.update({ [this.props.name]: value }, { save: this.props.autosave });
}
}
@@ -75,10 +75,7 @@ export class ProgressBarField extends Component {
if (this.props.record.fields[fieldName].type === "integer") {
parsedValue = Math.floor(parsedValue);
}
this.props.record.update({ [fieldName]: parsedValue });
if (this.props.readonly) {
this.props.record.save();
}
this.props.record.update({ [fieldName]: parsedValue }, { save: this.props.readonly });
}
onCurrentValueChange(ev) {
this.onValueChange(ev.target.value, this.currentValueField);
@@ -71,10 +71,7 @@ export class StateSelectionField extends Component {
}
async updateRecord(value) {
await this.props.record.update({ [this.props.name]: value });
if (this.props.autosave) {
return this.props.record.save();
}
await this.props.record.update({ [this.props.name]: value }, { save: this.props.autosave });
}
}
@@ -59,8 +59,7 @@ export class KanbanCoverImageDialog extends Component {
async setCover() {
const id = this.state.selectedAttachmentId ? [this.state.selectedAttachmentId] : false;
await this.props.record.update({ [this.props.fieldName]: id });
await this.props.record.save();
await this.props.record.update({ [this.props.fieldName]: id }, { save: true });
this.props.close();
}
@@ -297,8 +297,7 @@ export class KanbanRecord extends Component {
async selectColor(colorIndex) {
const { archInfo, record } = this.props;
await record.update({ [archInfo.colorField]: colorIndex });
await record.save();
await record.update({ [archInfo.colorField]: colorIndex }, { save: true });
}
/**
@@ -13097,12 +13097,7 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps(["get_views partner", "web_read partner"]);
await click(target, ".o_boolean_toggle");
assert.verifySteps([
"onchange2 turtle",
"onchange2 partner",
"write turtle",
"web_read turtle",
]);
assert.verifySteps(["onchange2 partner", "write turtle", "web_read turtle"]);
});
QUnit.test("create a new record with an x2m invisible", async function (assert) {
@@ -15,7 +15,6 @@ import {
mouseEnter,
nextTick,
patchWithCleanup,
selectDropdownItem,
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
@@ -5432,11 +5431,7 @@ QUnit.module("Views", (hooks) => {
}
);
QUnit.test("prevent drag and drop of record if onchange fails", async (assert) => {
serverData.models.partner.onchanges = {
product_id() {},
};
QUnit.test("prevent drag and drop of record if save fails", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
@@ -5453,7 +5448,7 @@ QUnit.module("Views", (hooks) => {
"</kanban>",
groupBy: ["product_id"],
async mockRPC(route, { model, method }) {
if (model === "partner" && method === "onchange2") {
if (model === "partner" && method === "write") {
return Promise.reject({});
}
},
@@ -9928,14 +9923,17 @@ QUnit.module("Views", (hooks) => {
"unity_web_search_read",
"unity_web_search_read",
"write",
"web_read",
"read_progress_bar",
"/web/dataset/resequence",
"read",
"write",
"web_read",
"read_progress_bar",
"/web/dataset/resequence",
"read",
"write",
"web_read",
"read_progress_bar",
"/web/dataset/resequence",
"read",
@@ -9996,6 +9994,7 @@ QUnit.module("Views", (hooks) => {
"unity_web_search_read",
"unity_web_search_read",
"write",
"web_read",
"read_progress_bar",
"web_read_group",
"/web/dataset/resequence",
@@ -10077,6 +10076,7 @@ QUnit.module("Views", (hooks) => {
"unity_web_search_read",
"unity_web_search_read",
"write",
"web_read",
"read_progress_bar",
"/web/dataset/resequence",
"read",
@@ -11544,6 +11544,7 @@ QUnit.module("Views", (hooks) => {
"unity_web_search_read",
"unity_web_search_read",
"write",
"web_read",
"read_progress_bar",
"/web/dataset/resequence",
"read",
@@ -11629,6 +11630,7 @@ QUnit.module("Views", (hooks) => {
assert.deepEqual(getCardTexts(1), ["2blip", "3gnap"]);
assert.verifySteps([
"write",
"web_read",
"read_progress_bar",
"unity_web_search_read",
"/web/dataset/resequence",
@@ -12841,97 +12843,6 @@ QUnit.module("Views", (hooks) => {
assert.verifySteps(["resequence"]);
});
QUnit.test("dragged record cannot be saved", async (assert) => {
let def;
serverData.models.partner.records = [
{ id: 1, bar: false, state: "abc" },
{ id: 2, bar: true, state: "def", foo: "blip" },
];
serverData.models.partner.onchanges = {
bar() {},
};
const kanban = await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="bar"/>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo" required="[('bar', '=', True)]"/>
<field name="state"/>
</div>
</t>
</templates>
</kanban>
`,
groupBy: ["bar"],
async mockRPC(route, args) {
if (args.method === "onchange2") {
assert.step("onchange2");
await Promise.resolve(def);
}
},
});
patchWithCleanup(kanban.env.services.notification, {
add(message, options) {
assert.step("notification");
assert.strictEqual(options.title, "Invalid fields: ");
assert.strictEqual(`${message}`, "<ul><li>Foo</li></ul>");
},
});
def = makeDeferred();
assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_record");
assert.strictEqual(
target.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record").innerText,
"ABC"
);
assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record");
assert.strictEqual(
target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText,
"blipDEF"
);
assert.verifySteps([]);
// move "yop" from first to second column
await dragAndDrop(
".o_kanban_group:nth-child(1) .o_kanban_record",
".o_kanban_group:nth-child(2)"
);
assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record");
assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2);
assert.deepEqual(
[...target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(
(el) => el.innerText
),
["blipDEF", "ABC"]
);
assert.verifySteps(["onchange2"]);
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_record");
assert.strictEqual(
target.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record").innerText,
"ABC"
);
assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record");
assert.strictEqual(
target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText,
"blipDEF"
);
assert.verifySteps(["notification"]);
});
QUnit.test("renders banner_route", async (assert) => {
await makeView({
type: "kanban",
@@ -13190,6 +13101,7 @@ QUnit.module("Views", (hooks) => {
"unity_web_search_read",
"unity_web_search_read",
"write",
"web_read",
"read_progress_bar",
"web_read_group",
]);
@@ -13284,55 +13196,6 @@ QUnit.module("Views", (hooks) => {
]);
});
QUnit.test("Move new record with onchanges and different active fields", async (assert) => {
serverData.models.partner.fields.foo.default = "abc";
serverData.models.partner.onchanges = {
bar(obj) {
obj.foo = [...obj.foo].reverse().join("");
},
};
serverData.views["partner,some_view_ref,form"] = /* xml */ `
<form>
<field name="int_field" />
<field name="category_ids" widget="many2many_tags" />
</form>`;
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: /* xml */ `
<kanban on_create="quick_create" quick_create_view="some_view_ref">
<field name="foo" />
<field name="bar" />
<templates>
<div t-name="kanban-box">
<t t-esc="record.foo.raw_value" />
<field name="int_field" />
</div>
</templates>
</kanban>
`,
groupBy: ["bar"],
});
assert.deepEqual(getCardTexts(0), ["blip-4"]);
assert.deepEqual(getCardTexts(1), ["yop10", "blip9", "gnap17"]);
await quickCreateRecord(0);
await editQuickCreateInput("int_field", "13");
await selectDropdownItem(target, "category_ids", "gold");
await validateRecord();
assert.deepEqual(getCardTexts(0), ["abc13", "blip-4"]);
assert.deepEqual(getCardTexts(1), ["yop10", "blip9", "gnap17"]);
await dragAndDrop(".o_kanban_record", ".o_kanban_group:nth-child(2)");
assert.deepEqual(getCardTexts(0), ["blip-4"]);
assert.deepEqual(getCardTexts(1), ["yop10", "blip9", "gnap17", "cba13"]);
});
QUnit.test("no sample data when all groups are folded then one is unfolded", async (assert) => {
await makeView({
type: "kanban",
@@ -13606,7 +13469,7 @@ QUnit.module("Views", (hooks) => {
prom.resolve();
await nextTick();
assert.verifySteps(["write", "/web/dataset/resequence", "read"]);
assert.verifySteps(["write", "web_read", "/web/dataset/resequence", "read"]);
assert.deepEqual(
[...target.querySelectorAll(".o_kanban_record")].map((el) => el.innerText),
["hello", "hello", "hello", "xmo"]
@@ -13672,7 +13535,7 @@ QUnit.module("Views", (hooks) => {
</kanban>`,
groupBy: ["product_id"],
mockRPC: async (route, { method }) => {
if (method === "onchange2") {
if (method === "write") {
await def;
}
},