diff --git a/addons/crm/static/src/views/crm_form/crm_form.js b/addons/crm/static/src/views/crm_form/crm_form.js index 57580f55993..6b08c6a4d62 100644 --- a/addons/crm/static/src/views/crm_form/crm_form.js +++ b/addons/crm/static/src/views/crm_form/crm_form.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { registry } from "@web/core/registry"; -import { formView } from "@web/views/form/form_view"; import { checkRainbowmanMessage } from "@crm/views/check_rainbowman_message"; -import { Record, RelationalModel } from "@web/views/basic_relational_model"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { formView } from "@web/views/form/form_view"; /** * This Form Controller makes sure we display a rainbowman message @@ -12,7 +12,15 @@ import { Record, RelationalModel } from "@web/views/basic_relational_model"; * if the lead is won and if a message should be displayed to the user * with a rainbowman like when the user click on the button "Mark Won". */ -export class CrmFormRecord extends Record { + +class CrmFormController extends formView.Controller { + setup() { + super.setup(); + this.orm = useService("orm"); + this.effect = useService("effect"); + this.changedStage = false; + } + /** * Main method used when saving the record hitting the "Save" button. * We check if the stage_id field was altered and if we need to display a rainbowman @@ -30,9 +38,9 @@ export class CrmFormRecord extends Record { * * @override */ - async save() { - const recordID = this.__bm_handle__; - const localData = this.model.__bm__.localData[recordID]; + async onWillSaveRecord(record) { + const recordID = record.__bm_handle__; + const localData = record.model.__bm__.localData[recordID]; const changes = localData._changes || {}; const needsSynchronizationEmail = @@ -58,34 +66,26 @@ export class CrmFormRecord extends Record { if (!localData._changes && Object.keys(changes).length) { localData._changes = changes; } - let changedStage = false; + if ("stage_id" in changes) { - const bm = this.model.__bm__; + const bm = record.model.__bm__; let oldStageId = false; if (bm.localData[recordID].data.stage_id) { oldStageId = bm.get(bm.localData[recordID].data.stage_id).data.id; } const newStageId = bm.get(bm.localData[recordID]._changes.stage_id).data.id; - changedStage = oldStageId !== newStageId; + this.changedStage = oldStageId !== newStageId; } - const isSaved = await super.save(...arguments); - if (changedStage && isSaved) { - await checkRainbowmanMessage(this.model.orm, this.model.effect, this.resId); - } - return isSaved; } -} -class CrmFormModel extends RelationalModel { - setup(params, services) { - this.effect = services.effect; - super.setup(...arguments); + async onRecordSaved(record) { + if (this.changedStage) { + await checkRainbowmanMessage(this.orm, this.effect, record.resId); + } } } -CrmFormModel.Record = CrmFormRecord; -CrmFormModel.services = [...RelationalModel.services, "effect"]; registry.category("views").add("crm_form", { ...formView, - Model: CrmFormModel, + Controller: CrmFormController, }); diff --git a/addons/event_booth_sale/static/src/js/event_booth_configurator_controller.js b/addons/event_booth_sale/static/src/js/event_booth_configurator_controller.js new file mode 100644 index 00000000000..1e1ea6722a3 --- /dev/null +++ b/addons/event_booth_sale/static/src/js/event_booth_configurator_controller.js @@ -0,0 +1,47 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { formView } from "@web/views/form/form_view"; + +/** + * This controller is overridden to allow configuring sale_order_lines through a popup + * window when a product with 'detailed_type' == 'event_booth' is selected. + * + * This allows keeping an editable list view for sales order and remove the noise of + * those 3 fields ('event_id', 'event_booth_category_id' and 'event_booth_ids') + */ + +class EventBoothConfiguratorController extends formView.Controller { + setup() { + super.setup(); + this.action = useService("action"); + } + + onRecordSaved(record) { + const { event_id, event_booth_category_id, event_booth_ids } = record.data; + return this.action.doAction({ + type: "ir.actions.act_window_close", + infos: { + eventBoothConfiguration: { + event_id, + event_booth_category_id, + event_booth_pending_ids: { + operation: "MULTI", + commands: [ + { + operation: "REPLACE_WITH", + ids: event_booth_ids.currentIds, + }, + ], + }, + }, + }, + }); + } +} + +registry.category("views").add("event_booth_configurator_form", { + ...formView, + Controller: EventBoothConfiguratorController, +}); diff --git a/addons/event_booth_sale/static/src/js/event_booth_configurator_model.js b/addons/event_booth_sale/static/src/js/event_booth_configurator_model.js deleted file mode 100644 index 082847f46e2..00000000000 --- a/addons/event_booth_sale/static/src/js/event_booth_configurator_model.js +++ /dev/null @@ -1,47 +0,0 @@ -/** @odoo-module **/ - -import { registry } from "@web/core/registry"; -import { formView } from "@web/views/form/form_view"; -import { Record, RelationalModel } from "@web/views/relational_model"; - -/** - * This model is overridden to allow configuring sale_order_lines through a popup - * window when a product with 'detailed_type' == 'event_booth' is selected. - * - * This allows keeping an editable list view for sales order and remove the noise of - * those 3 fields ('event_id', 'event_booth_category_id' and 'event_booth_ids') - */ -class EventBoothConfiguratorRelationalModel extends RelationalModel {} - -class EventBoothConfiguratorRecord extends Record { - //-------------------------------------------------------------------------- - // Overrides - //-------------------------------------------------------------------------- - async save() { - const isSaved = await super.save(...arguments); - if (!isSaved) { - return false; - } - this.model.action.doAction({type: 'ir.actions.act_window_close', infos: { - eventBoothConfiguration: { - event_id: this.data.event_id, - event_booth_category_id: this.data.event_booth_category_id, - event_booth_pending_ids: { - operation: 'MULTI', - commands: [{ - operation: 'REPLACE_WITH', - ids: this.data.event_booth_ids.currentIds, - }], - } - } - }}); - return true; - } -} - -EventBoothConfiguratorRelationalModel.Record = EventBoothConfiguratorRecord; - -registry.category("views").add("event_booth_configurator_form", { - ...formView, - Model: EventBoothConfiguratorRelationalModel, -}); diff --git a/addons/event_sale/static/src/js/event_configurator_controller.js b/addons/event_sale/static/src/js/event_configurator_controller.js new file mode 100644 index 00000000000..fac0056c865 --- /dev/null +++ b/addons/event_sale/static/src/js/event_configurator_controller.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { formView } from "@web/views/form/form_view"; + +/** + * This controller is overridden to allow configuring sale_order_lines through a popup + * window when a product with 'detailed_type' == 'event' is selected. + * + * This allows keeping an editable list view for sales order and remove the noise of + * those 2 fields ('event_id' + 'event_ticket_id') + */ + +class EventConfiguratorController extends formView.Controller { + setup() { + super.setup(); + this.action = useService("action"); + } + + /** + * We let the regular process take place to allow the validation of the required fields + * to happen. + * + * Then we can manually close the window, providing event information to the caller. + * + * @override + */ + onRecordSaved(record) { + const { event_id, event_ticket_id } = record.data; + return this.action.doAction({ + type: "ir.actions.act_window_close", + infos: { + eventConfiguration: { + event_id, + event_ticket_id, + }, + }, + }); + } +} + +registry.category("views").add("event_configurator_form", { + ...formView, + Controller: EventConfiguratorController, +}); diff --git a/addons/event_sale/static/src/js/event_configurator_model.js b/addons/event_sale/static/src/js/event_configurator_model.js deleted file mode 100644 index 13839eeaaff..00000000000 --- a/addons/event_sale/static/src/js/event_configurator_model.js +++ /dev/null @@ -1,46 +0,0 @@ -/** @odoo-module **/ - -import { registry } from "@web/core/registry"; -import { formView } from "@web/views/form/form_view"; -import { Record, RelationalModel } from "@web/views/relational_model"; - -/** - * This model is overridden to allow configuring sale_order_lines through a popup - * window when a product with 'detailed_type' == 'event' is selected. - * - * This allows keeping an editable list view for sales order and remove the noise of - * those 2 fields ('event_id' + 'event_ticket_id') - */ - -class EventConfiguratorRelationalModel extends RelationalModel {} - -class EventConfiguratorRecord extends Record { - /** - * We let the regular process take place to allow the validation of the required fields - * to happen. - * - * Then we can manually close the window, providing event information to the caller. - * - * @override - */ - async save() { - const isSaved = await super.save(...arguments); - if (!isSaved) { - return false; - } - this.model.action.doAction({type: 'ir.actions.act_window_close', infos: { - eventConfiguration: { - event_id: this.data.event_id, - event_ticket_id: this.data.event_ticket_id, - } - }}); - return true; - } -} - -EventConfiguratorRelationalModel.Record = EventConfiguratorRecord; - -registry.category("views").add("event_configurator_form", { - ...formView, - Model: EventConfiguratorRelationalModel, -}); diff --git a/addons/hr/static/src/views/profile_form_view.js b/addons/hr/static/src/views/profile_form_view.js index 7c8fd305343..7f087430106 100644 --- a/addons/hr/static/src/views/profile_form_view.js +++ b/addons/hr/static/src/views/profile_form_view.js @@ -1,25 +1,24 @@ /** @odoo-module */ import { registry } from "@web/core/registry"; - +import { useService } from "@web/core/utils/hooks"; import { formView } from "@web/views/form/form_view"; -import { Record, RelationalModel } from "@web/views/basic_relational_model"; -export class EmployeeProfileRecord extends Record { - async save() { - const dirtyFields = this.dirtyFields.map((f) => f.name); - const isSaved = await super.save(...arguments); - if (isSaved && dirtyFields.includes("lang")) { - this.model.actionService.doAction("reload_context"); +export class EmployeeProfileController extends formView.Controller { + setup() { + super.setup(); + this.action = useService("action"); + } + + onRecordSaved(record) { + const dirtyFields = record.dirtyFields.map((f) => f.name); + if (dirtyFields.includes("lang")) { + return this.action.doAction("reload_context"); } - return isSaved; } } -class EmployeeProfileModel extends RelationalModel {} -EmployeeProfileModel.Record = EmployeeProfileRecord; - registry.category("views").add("hr_employee_profile_form", { ...formView, - Model: EmployeeProfileModel, + Controller: EmployeeProfileController, }); diff --git a/addons/web/static/src/views/basic_relational_model.js b/addons/web/static/src/views/basic_relational_model.js index 5e75e0ed1ad..f2bd32513db 100644 --- a/addons/web/static/src/views/basic_relational_model.js +++ b/addons/web/static/src/views/basic_relational_model.js @@ -96,6 +96,9 @@ export class Record extends DataPoint { this._domains = {}; this._closeInvalidFieldsNotification = () => {}; + this.onWillSaveRecord = params.onWillSaveRecord || (() => {}); + this.onRecordSaved = params.onRecordSaved || (() => {}); + this._requiredFields = {}; for (const [fieldName, activeField] of Object.entries(this.activeFields)) { const { modifiers } = activeField; @@ -593,6 +596,9 @@ export class Record extends DataPoint { resolveSavePromise(); return false; } + if ((await this.onWillSaveRecord(this)) === false) { + return false; + } const saveOptions = { reload: !options.noReload, savePoint: options.savePoint, @@ -625,6 +631,8 @@ export class Record extends DataPoint { } this.model.notify(); resolveSavePromise(); + + await this.onRecordSaved(this); return true; } @@ -1124,6 +1132,9 @@ export class RelationalModel extends Model { }; this.initialMode = params.mode; + + this.onWillSaveRecord = params.onWillSaveRecord || (() => {}); + this.onRecordSaved = params.onRecordSaved || (() => {}); } async duplicateDatapoint(record, params) { @@ -1306,6 +1317,8 @@ export class RelationalModel extends Model { { __bm_load_params__: loadParams, mode: this.initialMode, + onWillSaveRecord: this.onWillSaveRecord, + onRecordSaved: this.onRecordSaved, }, state ); diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js index 6fc65afc574..2085696fc34 100644 --- a/addons/web/static/src/views/form/form_controller.js +++ b/addons/web/static/src/views/form/form_controller.js @@ -131,6 +131,8 @@ export class FormController extends Component { mode, beforeLoadProm, component: this, + onRecordSaved: this.onRecordSaved.bind(this), + onWillSaveRecord: this.onWillSaveRecord.bind(this), }, { ignoreUseSampleModel: true, @@ -256,6 +258,22 @@ export class FormController extends Component { } } + /** + * onRecordSaved is a callBack that will be executed after the save + * if it was done. It will therefore not be executed if the record + * is invalid or if a server error is thrown. + * @param {Record} record + */ + async onRecordSaved(record) {} + + /** + * onWillSaveRecord is a callBack that will be executed before the + * record save if the record is valid if the record is valid. + * If it returns false, it will prevent the save. + * @param {Record} record + */ + async onWillSaveRecord(record) {} + displayName() { return this.model.root.data.display_name || this.env._t("New"); } diff --git a/addons/web/static/src/views/list/list_controller.js b/addons/web/static/src/views/list/list_controller.js index 8431f198222..8b287e0572c 100644 --- a/addons/web/static/src/views/list/list_controller.js +++ b/addons/web/static/src/views/list/list_controller.js @@ -73,6 +73,8 @@ export class ListController extends Component { groupsLimit: this.archInfo.groupsLimit, multiEdit: this.multiEdit, rootState, + onRecordSaved: this.onRecordSaved.bind(this), + onWillSaveRecord: this.onWillSaveRecord.bind(this), }); onWillStart(async () => { @@ -155,6 +157,22 @@ export class ListController extends Component { ); } + /** + * onRecordSaved is a callBack that will be executed after the save + * if it was done. It will therefore not be executed if the record + * is invalid or if a server error is thrown. + * @param {Record} record + */ + async onRecordSaved(record) {} + + /** + * onWillSaveRecord is a callBack that will be executed before the + * record save if the record is valid if the record is valid. + * If it returns false, it will prevent the save. + * @param {Record} record + */ + async onWillSaveRecord(record) {} + async createRecord({ group } = {}) { const list = (group && group.list) || this.model.root; if (this.editable) { diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index 60e3f90312a..87c8335791b 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -275,6 +275,9 @@ class DataPoint { this.fields = params.fields; this.setActiveFields(params.activeFields); + this.onWillSaveRecord = params.onWillSaveRecord || (() => {}); + this.onRecordSaved = params.onRecordSaved || (() => {}); + this.rawContext = params.rawContext; this.defaultContext = params.defaultContext; this.setup(params, state); @@ -1328,6 +1331,9 @@ export class Record extends DataPoint { }); return false; } + if ((await this.onWillSaveRecord(this)) === false) { + return false; + } const changes = this.getChanges(); const keys = Object.keys(changes); const hasChanges = this.isVirtual || keys.length; @@ -1374,6 +1380,7 @@ export class Record extends DataPoint { if (!options.stayInEdition) { this.switchMode("readonly"); } + await this.onRecordSaved(this); return true; } @@ -1459,7 +1466,7 @@ class DynamicList extends DataPoint { this.editedRecord = record; } if (params.onRecordWillSwitchMode) { - await params.onRecordWillSwitchMode(record, mode, options); + return await params.onRecordWillSwitchMode(record, mode, options); } }; } @@ -1807,6 +1814,22 @@ export class DynamicRecordList extends DynamicList { // Getters // ------------------------------------------------------------------------- + get commonRecordParams() { + return { + resModel: this.resModel, + fields: this.fields, + activeFields: this.activeFields, + onRecordSaved: this.onRecordSaved, + onRecordWillSwitchMode: this.onRecordWillSwitchMode, + onWillSaveRecord: this.onWillSaveRecord, + defaultContext: this.defaultContext, + rawContext: { + parent: this.rawContext, + make: () => this.context, + }, + }; + } + get quickCreateRecord() { return this.records.find((r) => r.isInQuickCreation); } @@ -1822,15 +1845,7 @@ export class DynamicRecordList extends DynamicList { */ async addExistingRecord(resId, atFirstPosition) { const newRecord = this.model.createDataPoint("record", { - resModel: this.resModel, - fields: this.fields, - activeFields: this.activeFields, - onRecordWillSwitchMode: this.onRecordWillSwitchMode, - defaultContext: this.defaultContext, - rawContext: { - parent: this.rawContext, - make: () => this.context, - }, + ...this.commonRecordParams, resId, }); if (this.model.useSampleModel) { @@ -1867,16 +1882,8 @@ export class DynamicRecordList extends DynamicList { */ async createRecord(params = {}, atFirstPosition = false) { const newRecord = this.model.createDataPoint("record", { - resModel: this.resModel, - fields: this.fields, - activeFields: this.activeFields, + ...this.commonRecordParams, parentActiveFields: this.activeFields, - onRecordWillSwitchMode: this.onRecordWillSwitchMode, - defaultContext: this.defaultContext, - rawContext: { - parent: this.rawContext, - make: () => this.context, - }, ...params, }); if (this.model.useSampleModel) { @@ -2051,12 +2058,10 @@ export class DynamicRecordList extends DynamicList { const records = await Promise.all( rawRecords.map(async (data) => { const record = this.model.createDataPoint("record", { - resModel: this.resModel, + ...this.commonRecordParams, resId: data.id, - fields: this.fields, - activeFields: this.activeFields, rawContext: this.rawContext, - onRecordWillSwitchMode: this.onRecordWillSwitchMode, + defaultContext: undefined, }); await record.load({ values: data }); return record; @@ -2130,7 +2135,9 @@ export class DynamicGroupList extends DynamicList { groupByInfo: this.groupByInfo, rawContext: this.rawContext, onCreateRecord: this.onCreateRecord, + onRecordSaved: this.onRecordSaved, onRecordWillSwitchMode: this.onRecordWillSwitchMode, + onWillSaveRecord: this.onWillSaveRecord, }; } @@ -2556,7 +2563,9 @@ export class Group extends DataPoint { countLimit: params.count, groupByInfo: params.groupByInfo, onCreateRecord: params.onCreateRecord, + onRecordSaved: this.onRecordSaved, onRecordWillSwitchMode: params.onRecordWillSwitchMode, + onWillSaveRecord: this.onWillSaveRecord, defaultContext: { ...params.defaultContext, [`default_${this.groupByField.name}`]: this.getServerValue(), @@ -2678,6 +2687,8 @@ export class Group extends DataPoint { resModel: this.resModel, resId: this.value, rawContext: this.rawContext, + onRecordSaved: this.onRecordSaved, + onWillSaveRecord: this.onWillSaveRecord, ...params, }); } @@ -3401,6 +3412,8 @@ export class RelationalModel extends Model { viewMode: params.viewMode || null, resModel: params.resModel, groupByInfo: params.groupByInfo, + onRecordSaved: params.onRecordSaved, + onWillSaveRecord: params.onWillSaveRecord, }; this.fieldNodes = params.fieldNodes; // used by the ListConfirmationDialog if (this.rootType === "record") { diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index f39693eabc3..bcd205e3249 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -1962,7 +1962,11 @@ QUnit.module("Views", (hooks) => { }); assert.containsOnce(target, "label.o_form_label", "Only one label should be displayed"); assert.strictEqual(target.querySelector("label.o_form_label").textContent, "label2"); - assert.containsOnce(target, ".o_inner_group > div", "This group should only contain one line"); + assert.containsOnce( + target, + ".o_inner_group > div", + "This group should only contain one line" + ); } ); @@ -12627,4 +12631,87 @@ QUnit.module("Views", (hooks) => { "Unable to save" ); }); + + QUnit.test( + "execute an action before and after each valid save in a form view", + async function (assert) { + const formView = registry.category("views").get("form"); + class CustomFormController extends formView.Controller { + async onRecordSaved(record) { + assert.step(`onRecordSaved ${record.resId}`); + } + + async onWillSaveRecord(record) { + assert.step(`onWillSaveRecord ${record.resId}`); + } + } + registry.category("views").add("custom_form", { + ...formView, + Controller: CustomFormController, + }); + + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: `
`, + mockRPC(route, args) { + if (args.method === "write") { + assert.step(`write ${args.args[0]}`); + } + }, + }); + + await editInput(target, "[name='foo'] input", ""); + await clickSave(target); + assert.verifySteps([]); + + await editInput(target, "[name='foo'] input", "YOLO"); + await clickSave(target); + assert.verifySteps(["onWillSaveRecord 1", "write 1", "onRecordSaved 1"]); + } + ); + + QUnit.test( + "don't exec a valid save with onWillSaveRecord in a form view", + async function (assert) { + const formView = registry.category("views").get("form"); + class CustomFormController extends formView.Controller { + async onRecordSaved(record) { + throw new Error("should not execute onRecordSaved"); + } + + async onWillSaveRecord(record) { + assert.step(`onWillSaveRecord ${record.resId}`); + return false; + } + } + registry.category("views").add("custom_form", { + ...formView, + Controller: CustomFormController, + }); + + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: `
`, + mockRPC(route, args) { + if (args.method === "write") { + throw new Error("should not save the record"); + } + }, + }); + + await editInput(target, "[name='foo'] input", ""); + await clickSave(target); + assert.verifySteps([]); + + await editInput(target, "[name='foo'] input", "YOLO"); + await clickSave(target); + assert.verifySteps(["onWillSaveRecord 1"]); + } + ); }); diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index d0822476498..56b60cf52c6 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -1935,20 +1935,16 @@ QUnit.module("Views", (hooks) => { assert.expect(6); const listView = registry.category("views").get("list"); - class ListViewCustom extends listView.Controller { + class CustomListController extends listView.Controller { openRecord(record) { assert.step("openRecord"); assert.strictEqual(record.resId, 2); } } - registry.category("views").add( - "list", - { - ...listView, - Controller: ListViewCustom, - }, - { force: true } - ); + registry.category("views").add("custom_list", { + ...listView, + Controller: CustomListController, + }); serverData.models.foo.fields.foo.sortable = true; @@ -1956,7 +1952,7 @@ QUnit.module("Views", (hooks) => { type: "list", resModel: "foo", serverData, - arch: '', + arch: '', }); await click(target.querySelector("tr:nth-child(2) td:not(.o_list_record_selector)")); @@ -1971,6 +1967,142 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["openRecord", "openRecord"]); }); + QUnit.test( + "execute an action before and after each valid save in a list view", + async function (assert) { + const listView = registry.category("views").get("list"); + class CustomListController extends listView.Controller { + async onRecordSaved(record) { + assert.step(`onRecordSaved ${record.resId}`); + } + + async onWillSaveRecord(record) { + assert.step(`onWillSaveRecord ${record.resId}`); + } + } + registry.category("views").add( + "custom_list", + { + ...listView, + Controller: CustomListController, + }, + { force: true } + ); + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: + '', + mockRPC: async (route, args) => { + if (args.method === "write") { + assert.step(`write ${args.args[0]}`); + } + }, + }); + + await click(target.querySelector(".o_data_cell")); + await editInput(target, "[name='foo'] input", ""); + await click(target, ".o_list_view"); + assert.verifySteps([]); + + await editInput(target, "[name='foo'] input", "YOLO"); + await click(target, ".o_list_view"); + assert.verifySteps(["onWillSaveRecord 1", "write 1", "onRecordSaved 1"]); + } + ); + + QUnit.test( + "execute an action before and after each valid save in a grouped list view", + async function (assert) { + const listView = registry.category("views").get("list"); + class CustomListController extends listView.Controller { + async onRecordSaved(record) { + assert.step(`onRecordSaved ${record.resId}`); + } + + async onWillSaveRecord(record) { + assert.step(`onWillSaveRecord ${record.resId}`); + } + } + registry.category("views").add("custom_list", { + ...listView, + Controller: CustomListController, + }); + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: + '', + groupBy: ["bar"], + mockRPC: async (route, args) => { + if (args.method === "write") { + assert.step(`write ${args.args[0]}`); + } + }, + }); + + await click(target.querySelector(".o_data_cell[name='foo']")); + await editInput(target, "[name='foo'] input", ""); + await click(target, ".o_list_view"); + assert.verifySteps([]); + + await editInput(target, "[name='foo'] input", "YOLO"); + await click(target, ".o_list_view"); + assert.verifySteps(["onWillSaveRecord 4", "write 4", "onRecordSaved 4"]); + } + ); + + QUnit.test( + "don't exec a valid save with onWillSaveRecord in a list view", + async function (assert) { + const listView = registry.category("views").get("list"); + class ListViewCustom extends listView.Controller { + async onRecordSaved(record) { + throw new Error("should not execute onRecordSaved"); + } + + async onWillSaveRecord(record) { + assert.step(`onWillSaveRecord ${record.resId}`); + return false; + } + } + registry.category("views").add( + "list", + { + ...listView, + Controller: ListViewCustom, + }, + { force: true } + ); + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + mockRPC: async (route, args) => { + if (args.method === "write") { + throw new Error("should not save the record"); + } + }, + }); + + await click(target.querySelector(".o_data_cell")); + await editInput(target, "[name='foo'] input", ""); + await click(target, ".o_list_view"); + assert.verifySteps([]); + + await click(target.querySelector(".o_data_cell")); + await editInput(target, "[name='foo'] input", "YOLO"); + await click(target, ".o_list_view"); + assert.verifySteps(["onWillSaveRecord 1"]); + } + ); + QUnit.test("action/type attributes on tree arch, type='object'", async (assert) => { const list = await makeView({ type: "list",