[REF] web: adds the callback onWillSaveRecord and onRecordSaved
The purpose of this commit is to add extension points that allow code
to be executed before and after the save of a record.
CallBack:
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.
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.
This commit will replace all the save overrides by the onWillSaveRecord
and onRecordSaved callBack.
Observed problem:
We could notice that each of the overrides of save in order to execute
for example a doAction after this one did not take into account the fact
that the save did not succeed.
For example, a required field is invalid and we try to save, then the
doAction will be executed without the "write" of the save.
The callBack onRecordSaved will prevent this common error.
closes odoo/odoo#105180
Related: odoo/enterprise#33710
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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: `<form js_class="custom_form"><field name="foo" required="1"/></form>`,
|
||||
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: `<form js_class="custom_form"><field name="foo" required="1"/></form>`,
|
||||
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"]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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: '<tree><field name="foo"/></tree>',
|
||||
arch: '<tree js_class="custom_list"><field name="foo"/></tree>',
|
||||
});
|
||||
|
||||
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:
|
||||
'<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]}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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:
|
||||
'<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") {
|
||||
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: '<tree editable="top"><field name="foo" required="1"/></tree>',
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user