Before this commit, creating a new record with the "many2many" widget
did not allow to use a context containing a default value
for the parentID (id of the Record containing the many2many).
This value is ignored.
How to reproduce?
- Go to a form view with a field using the "many2many" widget
with context = {"default_parentID" = ...}
- Click on "Add a new line"
- Click on "Create" (The form view should display the parentID field)
Before this commit:
The parentID field is empty.
After this commit:
The parentID field contains the record corresponding
to "default_parentID".
closes odoo/odoo#102157
X-original-commit: a52eacc5397dc2f98eb052f4cd1e362a97621074
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
1373 lines
48 KiB
JavaScript
1373 lines
48 KiB
JavaScript
/* @odoo-module */
|
|
|
|
import { makeContext } from "@web/core/context";
|
|
import { Domain } from "@web/core/domain";
|
|
import { WarningDialog } from "@web/core/errors/error_dialogs";
|
|
import { deserializeDate, deserializeDateTime } from "@web/core/l10n/dates";
|
|
import { KeepLast } from "@web/core/utils/concurrency";
|
|
import { escape } from "@web/core/utils/strings";
|
|
import { mapDoActionOptionAPI } from "@web/legacy/backend_utils";
|
|
import { Model } from "@web/views/model";
|
|
import { evalDomain } from "@web/views/utils";
|
|
import {
|
|
mapWowlValueToLegacy,
|
|
mapViews,
|
|
mapActiveFieldsToFieldsInfo,
|
|
} from "@web/views/legacy_utils";
|
|
import { localization } from "@web/core/l10n/localization";
|
|
import BasicModel from "web.BasicModel";
|
|
import Context from "web.Context";
|
|
|
|
const { markup, toRaw } = owl;
|
|
|
|
const DEFAULT_HANDLE_FIELD = "sequence";
|
|
|
|
let nextId = 0;
|
|
class DataPoint {
|
|
/**
|
|
* @param {RelationalModel} model
|
|
* @param {Object} [params={}]
|
|
* @param {Object} [state={}]
|
|
*/
|
|
constructor(model, params, state = {}) {
|
|
this.id = `datapoint_${nextId++}`;
|
|
this.model = model;
|
|
|
|
let info;
|
|
if (params.__bm_load_params__) {
|
|
this.__bm_load_params__ = params.__bm_load_params__;
|
|
info = this.__bm_load_params__;
|
|
this.context = info.context;
|
|
} else if (params.handle) {
|
|
this.__bm_handle__ = params.handle;
|
|
info = this.model.__bm__.get(this.__bm_handle__);
|
|
this.context = this.model.__bm__.localData[this.__bm_handle__].getContext();
|
|
} else {
|
|
throw new Error("Datapoint needs load params or handle");
|
|
}
|
|
this.resModel = info.model || info.modelName;
|
|
this.fields = info.fields;
|
|
this.activeFields = {};
|
|
|
|
this.__syncParent = params.__syncParent || (() => {});
|
|
this.__viewType = params.viewType || info.viewType;
|
|
const fieldsInfo = (info.fieldsInfo && info.fieldsInfo[this.__viewType]) || {};
|
|
for (const [name, descr] of Object.entries(fieldsInfo)) {
|
|
this.activeFields[name] = descr.__WOWL_FIELD_DESCR__ || {};
|
|
}
|
|
|
|
this.setup(params, state);
|
|
}
|
|
|
|
/**
|
|
* @abstract
|
|
* @param {Object} params
|
|
* @param {Object} state
|
|
*/
|
|
setup() {}
|
|
|
|
get evalContext() {
|
|
const datapoint = this.model.__bm__.localData[this.__bm_handle__];
|
|
return this.model.__bm__._getLazyEvalContext(datapoint);
|
|
}
|
|
|
|
get fieldNames() {
|
|
return Object.keys(this.activeFields);
|
|
}
|
|
|
|
exportState() {
|
|
return {};
|
|
}
|
|
|
|
async load() {
|
|
throw new Error("load must be implemented");
|
|
}
|
|
}
|
|
|
|
export class Record extends DataPoint {
|
|
setup(params, state) {
|
|
this.data = {};
|
|
this._invalidFields = new Set();
|
|
this.preloadedData = {};
|
|
this.selected = false;
|
|
this.isInQuickCreation = params.isInQuickCreation || false;
|
|
this._onChangePromise = Promise.resolve({});
|
|
this._savePromise = Promise.resolve();
|
|
this._domains = {};
|
|
this._closeInvalidFieldsNotification = () => {};
|
|
|
|
this._requiredFields = {};
|
|
for (const [fieldName, activeField] of Object.entries(this.activeFields)) {
|
|
const { modifiers } = activeField;
|
|
if (modifiers && modifiers.required) {
|
|
this._requiredFields[fieldName] = modifiers.required;
|
|
}
|
|
}
|
|
|
|
if (!this.resId && this.__viewType === "form") {
|
|
this.mode = "edit"; // always edit a new record in form view.
|
|
} else {
|
|
this.mode = params.mode || state.mode || "readonly";
|
|
}
|
|
|
|
this._onWillSwitchMode = params.onRecordWillSwitchMode || (() => {});
|
|
|
|
if (this.__bm_handle__) {
|
|
this.__syncData();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Getters
|
|
// -------------------------------------------------------------------------
|
|
|
|
get canBeAbandoned() {
|
|
return this.model.__bm__.canBeAbandoned(this.__bm_handle__);
|
|
}
|
|
|
|
/**
|
|
* Since the ORM can support both `active` and `x_active` fields for
|
|
* the archiving mechanism, check if any such field exists and prioritize
|
|
* them. The `active` field should always take priority over its custom
|
|
* version.
|
|
*
|
|
* @returns {boolean} true iff the field is active or there is no `active`
|
|
* or `x_active` field on the model
|
|
*/
|
|
get isActive() {
|
|
if ("active" in this.activeFields) {
|
|
return this.data.active;
|
|
} else if ("x_active" in this.activeFields) {
|
|
return this.data.x_active;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
get isDirty() {
|
|
return this.model.__bm__.isDirty(this.__bm_handle__);
|
|
}
|
|
|
|
get dirtyFields() {
|
|
const changes = this.model.__bm__.localData[this.__bm_handle__]._changes;
|
|
if (!changes) {
|
|
return [];
|
|
}
|
|
return Object.keys(changes).map((change) => this.activeFields[change]);
|
|
}
|
|
|
|
get translatableFields() {
|
|
if (!localization.multiLang) {
|
|
return [];
|
|
}
|
|
return Object.values(this.fields)
|
|
.filter((f) => f.translate)
|
|
.map((f) => this.activeFields[f.name]);
|
|
}
|
|
|
|
get dirtyTranslatableFields() {
|
|
return this.translatableFields.filter((f) => this.dirtyFields.includes(f));
|
|
}
|
|
|
|
get isInEdition() {
|
|
return this.mode === "edit";
|
|
}
|
|
|
|
get isNew() {
|
|
return this.model.__bm__.isNew(this.__bm_handle__);
|
|
}
|
|
|
|
get isVirtual() {
|
|
// FIXME: not sure about this virtual thing
|
|
return !this.resId;
|
|
}
|
|
|
|
get isValid() {
|
|
return !this._invalidFields.size;
|
|
}
|
|
|
|
get resId() {
|
|
if (this.__bm_handle__) {
|
|
const resId = this.model.__bm__.localData[this.__bm_handle__].res_id;
|
|
if (typeof resId === "string") {
|
|
return false;
|
|
}
|
|
return resId;
|
|
} else {
|
|
// record not loaded yet
|
|
return this.__bm_load_params__.res_id;
|
|
}
|
|
}
|
|
|
|
get resIds() {
|
|
return this.model.__bm__.localData[this.__bm_handle__].res_ids;
|
|
}
|
|
|
|
async askChanges() {
|
|
const proms = [];
|
|
this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES", { proms });
|
|
return Promise.all([...proms, this._updatePromise]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Getters
|
|
// -------------------------------------------------------------------------
|
|
|
|
async checkValidity(urgent) {
|
|
if (!urgent) {
|
|
await this.askChanges();
|
|
}
|
|
for (const fieldName in this.activeFields) {
|
|
const fieldType = this.fields[fieldName].type;
|
|
const activeField = this.activeFields[fieldName];
|
|
if (fieldName in this._requiredFields) {
|
|
if (
|
|
!evalDomain(this._requiredFields[fieldName], this.evalContext) ||
|
|
(activeField && activeField.alwaysInvisible)
|
|
) {
|
|
this._removeInvalidFields([fieldName]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const isSet =
|
|
activeField && activeField.FieldComponent && activeField.FieldComponent.isSet;
|
|
|
|
if (this.isRequired(fieldName) && isSet && !isSet(this.data[fieldName])) {
|
|
this.setInvalidField(fieldName);
|
|
continue;
|
|
}
|
|
|
|
switch (fieldType) {
|
|
case "boolean":
|
|
case "float":
|
|
case "integer":
|
|
case "monetary":
|
|
continue;
|
|
case "properties":
|
|
if (!this.checkPropertiesValidity(fieldName)) {
|
|
this._setInvalidField(fieldName);
|
|
}
|
|
break;
|
|
case "one2many":
|
|
case "many2many":
|
|
if (!(await this.checkX2ManyValidity(fieldName))) {
|
|
this._setInvalidField(fieldName);
|
|
}
|
|
break;
|
|
default:
|
|
if (!isSet && this.isRequired(fieldName) && !this.data[fieldName]) {
|
|
this._setInvalidField(fieldName);
|
|
}
|
|
}
|
|
}
|
|
return !this._invalidFields.size;
|
|
}
|
|
|
|
async switchMode(mode, options) {
|
|
if (this.mode === mode) {
|
|
return true;
|
|
}
|
|
const canSwitch = await this._onWillSwitchMode(this, mode, options);
|
|
if (canSwitch === false) {
|
|
return false;
|
|
}
|
|
|
|
if (mode === "edit") {
|
|
// wait for potential pending changes to be saved (done with widgets
|
|
// allowing to edit in readonly)
|
|
await this.model.__bm__.mutex.getUnlockedDef();
|
|
}
|
|
|
|
if (mode === "readonly") {
|
|
for (const fieldName in this.activeFields) {
|
|
if (["one2many", "many2many"].includes(this.fields[fieldName].type)) {
|
|
const editedRecord = this.data[fieldName] && this.data[fieldName].editedRecord;
|
|
if (editedRecord) {
|
|
editedRecord.switchMode("readonly");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.mode = mode;
|
|
this.model.notify();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* FIXME: memoize this at some point?
|
|
* @param {string} fieldName
|
|
* @returns {boolean}
|
|
*/
|
|
isReadonly(fieldName) {
|
|
const { readonly } = this.activeFields[fieldName].modifiers || {};
|
|
return evalDomain(readonly, this.evalContext);
|
|
}
|
|
|
|
/**
|
|
* FIXME: memoize this at some point?
|
|
* @param {string} fieldName
|
|
* @returns {boolean}
|
|
*/
|
|
isRequired(fieldName) {
|
|
const { required } = this.activeFields[fieldName].modifiers || {};
|
|
return evalDomain(required, this.evalContext);
|
|
}
|
|
|
|
async checkX2ManyValidity(fieldName) {
|
|
const list = this.data[fieldName];
|
|
const record = list.editedRecord;
|
|
if (record && !(await record.checkValidity())) {
|
|
if (record.canBeAbandoned && !record.isDirty) {
|
|
list.abandonRecord(record.id);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The label and the id of the properties are always required.
|
|
*
|
|
* @param {string} fieldName
|
|
* @returns {boolean}
|
|
*/
|
|
checkPropertiesValidity(fieldName) {
|
|
const value = this.data[fieldName];
|
|
if (!value) {
|
|
return true;
|
|
}
|
|
return value.every(
|
|
(propertyDefinition) =>
|
|
!propertyDefinition.id ||
|
|
(propertyDefinition.string && propertyDefinition.string.length)
|
|
);
|
|
}
|
|
|
|
_setInvalidField(fieldName) {
|
|
this._invalidFields.add(fieldName);
|
|
this.model.notify();
|
|
}
|
|
|
|
setInvalidField(fieldName) {
|
|
const bm = this.model.__bm__;
|
|
bm.setDirty(this.__bm_handle__);
|
|
this._setInvalidField(fieldName);
|
|
}
|
|
|
|
isInvalid(fieldName) {
|
|
return this._invalidFields.has(fieldName);
|
|
}
|
|
|
|
async load(params = {}) {
|
|
if (!this.__bm_handle__) {
|
|
this.__bm_handle__ = await this.model.__bm__.load({
|
|
...this.__bm_load_params__,
|
|
viewType: this.__viewType,
|
|
});
|
|
} else {
|
|
this.__bm_handle__ = await this.model.__bm__.reload(this.__bm_handle__, {
|
|
viewType: this.__viewType,
|
|
});
|
|
}
|
|
this.__syncData();
|
|
this._invalidFields = new Set();
|
|
}
|
|
|
|
exportState() {
|
|
return {
|
|
mode: this.mode,
|
|
resId: this.resId,
|
|
resIds: this.resIds,
|
|
};
|
|
}
|
|
|
|
__syncData(force) {
|
|
const bm = this.model.__bm__;
|
|
const legDP = bm.get(this.__bm_handle__);
|
|
const data = Object.assign({}, legDP.data);
|
|
for (const fieldName of this.fieldNames) {
|
|
const fieldType = legDP.fields[fieldName].type;
|
|
switch (fieldType) {
|
|
case "date":
|
|
case "datetime": {
|
|
// from moment to luxon
|
|
if (data[fieldName]) {
|
|
const deserialize =
|
|
fieldType === "date" ? deserializeDate : deserializeDateTime;
|
|
data[fieldName] = deserialize(data[fieldName].toJSON());
|
|
}
|
|
break;
|
|
}
|
|
case "one2many":
|
|
case "many2many": {
|
|
const currentVal = this.data[fieldName];
|
|
if (
|
|
!data[fieldName] ||
|
|
(currentVal && currentVal.__bm_handle__ === data[fieldName].id && !force)
|
|
) {
|
|
data[fieldName] = this.data[fieldName];
|
|
data[fieldName].__syncData();
|
|
} else {
|
|
const { viewMode, views } = this.activeFields[fieldName];
|
|
const handleField =
|
|
(views && views[viewMode] && views[viewMode].handleField) || null;
|
|
data[fieldName] = new StaticList(this.model, {
|
|
handle: data[fieldName].id,
|
|
handleField,
|
|
viewType: viewMode,
|
|
__syncParent: async (value) => {
|
|
await this.model.__bm__.save(this.__bm_handle__, {
|
|
savePoint: true,
|
|
});
|
|
await this.update({ [fieldName]: value });
|
|
},
|
|
});
|
|
data[fieldName].__fieldName__ = fieldName;
|
|
}
|
|
break;
|
|
}
|
|
case "many2one": {
|
|
if (data[fieldName]) {
|
|
data[fieldName] = [
|
|
data[fieldName].data.id,
|
|
data[fieldName].data.display_name || "",
|
|
];
|
|
} else {
|
|
data[fieldName] = false;
|
|
}
|
|
break;
|
|
}
|
|
case "reference": {
|
|
data[fieldName] = data[fieldName]
|
|
? {
|
|
resModel: data[fieldName].model,
|
|
resId: data[fieldName].data.id,
|
|
displayName: data[fieldName].data.display_name,
|
|
}
|
|
: false;
|
|
break;
|
|
}
|
|
case "char": {
|
|
data[fieldName] = data[fieldName] || "";
|
|
break;
|
|
}
|
|
case "html": {
|
|
data[fieldName] = markup(data[fieldName] || "");
|
|
break;
|
|
}
|
|
}
|
|
if (legDP.specialData[fieldName]) {
|
|
this.preloadedData[fieldName] = legDP.specialData[fieldName];
|
|
}
|
|
}
|
|
this.data = data;
|
|
}
|
|
|
|
getFieldContext(fieldName) {
|
|
return this.model.__bm__.localData[this.__bm_handle__].getContext({
|
|
fieldName,
|
|
viewType: this.__viewType,
|
|
});
|
|
}
|
|
|
|
getFieldDomain(fieldName) {
|
|
const domain = this.model.__bm__.localData[this.__bm_handle__].getDomain({
|
|
fieldName,
|
|
viewType: this.__viewType,
|
|
});
|
|
return new Domain(JSON.parse(JSON.stringify(domain))); // legacy pyjs inserts weird structures
|
|
}
|
|
|
|
async update(changes) {
|
|
if (this.batchingUpdateProm) {
|
|
// Assign changes in the current batch
|
|
Object.assign(this.batchChanges, changes);
|
|
return this._updatePromise;
|
|
}
|
|
|
|
this.batchingUpdateProm = Promise.resolve();
|
|
this.batchChanges = Object.assign({}, changes);
|
|
|
|
let resolveUpdatePromise;
|
|
this._updatePromise = new Promise((r) => {
|
|
resolveUpdatePromise = r;
|
|
});
|
|
|
|
await this.batchingUpdateProm;
|
|
changes = this.batchChanges;
|
|
this.batchingUpdateProm = null;
|
|
this.batchChanges = null;
|
|
|
|
const data = {};
|
|
for (const [fieldName, value] of Object.entries(changes)) {
|
|
const fieldType = this.fields[fieldName].type;
|
|
data[fieldName] = mapWowlValueToLegacy(value, fieldType);
|
|
}
|
|
if (this._urgentSave) {
|
|
return this.model.__bm__.notifyChanges(this.__bm_handle__, data, {
|
|
viewType: this.__viewType,
|
|
notifyChange: false,
|
|
});
|
|
}
|
|
|
|
const parentID = this.model.__bm__.localData[this.__bm_handle__].parentID;
|
|
if (parentID && this.__viewType === "list") {
|
|
// inside an x2many (parentID is the id of the static list datapoint)
|
|
const mainRecordId = this.model.__bm__.localData[parentID].parentID;
|
|
const mainRecordDP = this.model.__bm__.localData[mainRecordId];
|
|
const mainRecordValues = { ...mainRecordDP.data, ...mainRecordDP._changes };
|
|
const x2manyFieldName = Object.keys(mainRecordValues).find(
|
|
(name) => mainRecordValues[name] === parentID
|
|
);
|
|
if (!x2manyFieldName) {
|
|
throw new Error("couldn't find x2many field name");
|
|
}
|
|
const operation = { operation: "UPDATE", id: this.__bm_handle__, data };
|
|
const prom = this.__syncParent(operation);
|
|
prom.catch(resolveUpdatePromise);
|
|
await prom;
|
|
} else {
|
|
const prom = this.model.__bm__.notifyChanges(this.__bm_handle__, data, {
|
|
viewType: this.__viewType,
|
|
});
|
|
prom.catch(resolveUpdatePromise); // onchange rpc may return an error
|
|
const fieldNames = await prom;
|
|
this._removeInvalidFields(fieldNames);
|
|
for (const fieldName of fieldNames) {
|
|
if (["one2many", "many2many"].includes(this.fields[fieldName].type)) {
|
|
const { editedRecord } = this.data[fieldName];
|
|
if (editedRecord) {
|
|
editedRecord._removeAllInvalidFields();
|
|
}
|
|
}
|
|
}
|
|
this.__syncData();
|
|
}
|
|
this._removeInvalidFields(Object.keys(changes));
|
|
this.model.notify();
|
|
resolveUpdatePromise();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} options
|
|
* @param {boolean} [options.stayInEdition=false]
|
|
* @param {boolean} [options.noReload=false] prevents the record from
|
|
* reloading after changes are applied, typically used to defer the load.
|
|
* @param {boolean} [options.useSaveErrorDialog=false] displays a custom
|
|
* dialog and await the response from this dialog when an error is
|
|
* returned by the server.
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async save(
|
|
options = {
|
|
stayInEdition: true,
|
|
noReload: false,
|
|
savePoint: false,
|
|
useSaveErrorDialog: false,
|
|
}
|
|
) {
|
|
const shouldSwitchToReadonly = !options.stayInEdition && this.isInEdition;
|
|
let resolveSavePromise;
|
|
this._savePromise = new Promise((r) => {
|
|
resolveSavePromise = r;
|
|
});
|
|
this._closeInvalidFieldsNotification();
|
|
if (!(await this.checkValidity())) {
|
|
const invalidFields = [...this._invalidFields].map((fieldName) => {
|
|
return `<li>${escape(this.fields[fieldName].string || fieldName)}</li>`;
|
|
}, this);
|
|
this._closeInvalidFieldsNotification = this.model.notificationService.add(
|
|
markup(`<ul>${invalidFields.join("")}</ul>`),
|
|
{
|
|
title: this.model.env._t("Invalid fields: "),
|
|
type: "danger",
|
|
}
|
|
);
|
|
resolveSavePromise();
|
|
return false;
|
|
}
|
|
const saveOptions = {
|
|
reload: !options.noReload,
|
|
savePoint: options.savePoint,
|
|
};
|
|
try {
|
|
await this.model.__bm__.save(this.__bm_handle__, saveOptions);
|
|
} catch (_e) {
|
|
resolveSavePromise();
|
|
let canProceed = false;
|
|
if (options.useSaveErrorDialog) {
|
|
_e.__raisedOnFormSave = true;
|
|
canProceed = await new Promise((resolve) => {
|
|
_e.onDiscard = async () => {
|
|
await this.discard();
|
|
resolve(true);
|
|
};
|
|
_e.onStayHere = () => resolve(false);
|
|
});
|
|
}
|
|
|
|
if (!this.isInEdition) {
|
|
await this.load();
|
|
this.model.notify();
|
|
}
|
|
return canProceed;
|
|
}
|
|
this.__syncData(true);
|
|
if (shouldSwitchToReadonly) {
|
|
this.switchMode("readonly");
|
|
}
|
|
this.model.notify();
|
|
resolveSavePromise();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* To be called **only** when Odoo is about to be closed, and we want to
|
|
* save potential changes on a given record.
|
|
*
|
|
* We can't follow the normal flow (onchange(s) + save, mutexified),
|
|
* because the 'beforeunload' handler must be *almost* sync (< 10 ms
|
|
* setTimeout seems fine, but an rpc roundtrip is definitely too long),
|
|
* so here we bypass the standard mechanism of notifying changes and
|
|
* saving them:
|
|
* - we ask the model to bypass its mutex for upcoming 'notifyChanges' and
|
|
* 'save' requests
|
|
* - we ask all fields to commit their changes (in case there would
|
|
* be a focused field with a fresh value)
|
|
* - we take all changes that have been reported to the
|
|
* controller, but not yet sent to the model because of the mutex,
|
|
* and directly notify the model about them, see update.
|
|
* - we reset the widgets with all those changes, s.t. a further call
|
|
* to 'canBeRemoved' uses the correct data (it asks the widgets if
|
|
* they are set/valid, based on their internal state)
|
|
* - if the record is dirty, we save directly
|
|
*
|
|
* @param {string} recordID
|
|
* @private
|
|
*/
|
|
async urgentSave() {
|
|
this.model.__bm__.bypassMutex = true;
|
|
this._urgentSave = true;
|
|
this.model.env.bus.trigger("RELATIONAL_MODEL:WILL_SAVE_URGENTLY");
|
|
await Promise.resolve();
|
|
this.__syncData();
|
|
if (this.isDirty && (await this.checkValidity(true))) {
|
|
this.model.__bm__.save(this.__bm_handle__, { reload: false });
|
|
}
|
|
this.model.__bm__.bypassMutex = false;
|
|
}
|
|
|
|
async archive() {
|
|
await this.model.__bm__.actionArchive([this.resId], this.__bm_handle__);
|
|
this.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
async unarchive() {
|
|
await this.model.__bm__.actionUnarchive([this.resId], this.__bm_handle__);
|
|
this.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
// FIXME AAB: to discuss: not sure we want to keep resIds in the model (this concerns
|
|
// "duplicate" and "delete"). Alternative: handle this in form_view (but then what's the
|
|
// point of calling a Record method to do the operation?)
|
|
async duplicate() {
|
|
this.__bm_handle__ = await this.model.__bm__.duplicateRecord(this.__bm_handle__);
|
|
this.__syncData();
|
|
this.switchMode("edit");
|
|
this.model.notify();
|
|
}
|
|
|
|
async delete() {
|
|
await this.model.__bm__.deleteRecords([this.__bm_handle__], this.resModel);
|
|
if (this.resIds.length) {
|
|
await this.load();
|
|
}
|
|
this.model.notify();
|
|
}
|
|
|
|
toggleSelection(selected) {
|
|
if (typeof selected === "boolean") {
|
|
this.selected = selected;
|
|
} else {
|
|
this.selected = !this.selected;
|
|
}
|
|
this.model.notify();
|
|
}
|
|
|
|
async discard() {
|
|
await this._savePromise;
|
|
this._closeInvalidFieldsNotification();
|
|
this.model.__bm__.discardChanges(this.__bm_handle__);
|
|
this._invalidFields = new Set();
|
|
this.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
_removeInvalidFields(fieldNames) {
|
|
for (const fieldName of fieldNames) {
|
|
this._invalidFields.delete(fieldName);
|
|
}
|
|
}
|
|
|
|
_removeAllInvalidFields() {
|
|
this._removeInvalidFields(Object.keys(this.activeFields));
|
|
}
|
|
}
|
|
|
|
export class StaticList extends DataPoint {
|
|
setup(params) {
|
|
/** @type {Record[]} */
|
|
this.records = [];
|
|
|
|
this.handleField = params.handleField;
|
|
|
|
this.editedRecord = null;
|
|
this.onRecordWillSwitchMode = async (record, mode, options = {}) => {
|
|
if (mode === "edit") {
|
|
await this.model.__bm__.save(this.__bm_handle__, { savePoint: true });
|
|
this.model.__bm__.freezeOrder(this.__bm_handle__);
|
|
}
|
|
|
|
const editedRecord = this.editedRecord;
|
|
this.editedRecord = null;
|
|
if (editedRecord) {
|
|
// Validity is checked if one of the following is true:
|
|
// - "switchMode" has been called with explicit "checkValidity"
|
|
// - the record is dirty
|
|
// - the record is new and can be abandonned
|
|
const shouldCheckValidity =
|
|
options.checkValidity || editedRecord.isDirty || editedRecord.canBeAbandoned;
|
|
const isValid = !shouldCheckValidity || (await editedRecord.checkValidity());
|
|
if (isValid) {
|
|
await editedRecord.switchMode("readonly");
|
|
} else if (editedRecord.id !== record.id && editedRecord.canBeAbandoned) {
|
|
this.abandonRecord(editedRecord.id);
|
|
} else {
|
|
this.editedRecord = editedRecord;
|
|
return false;
|
|
}
|
|
}
|
|
if (mode === "edit") {
|
|
this.editedRecord = record;
|
|
}
|
|
};
|
|
|
|
this.__syncData();
|
|
}
|
|
|
|
__syncData() {
|
|
const legacyListDP = this.model.__bm__.get(this.__bm_handle__);
|
|
this.records = legacyListDP.data.map((dp) => {
|
|
let record = this.records.find((r) => r.__bm_handle__ === dp.id);
|
|
if (record) {
|
|
record.__syncData();
|
|
} else {
|
|
record = new this.model.constructor.Record(this.model, {
|
|
handle: dp.id,
|
|
onRecordWillSwitchMode: this.onRecordWillSwitchMode,
|
|
mode: "readonly",
|
|
viewType: this.__viewType,
|
|
__syncParent: async (value) => {
|
|
await this.__syncParent(value);
|
|
},
|
|
});
|
|
if (record.mode === "edit" && this.editedRecord) {
|
|
this.editedRecord.mode = "readonly";
|
|
}
|
|
}
|
|
return record;
|
|
});
|
|
this.editedRecord = this.records.find((r) => r.mode === "edit") || null;
|
|
}
|
|
|
|
// FIXME: remove?
|
|
get resIds() {
|
|
return this.model.__bm__.get(this.__bm_handle__).res_ids;
|
|
}
|
|
get currentIds() {
|
|
return this.model.__bm__.get(this.__bm_handle__).res_ids;
|
|
}
|
|
|
|
get orderBy() {
|
|
return this.model.__bm__.localData[this.__bm_handle__].orderedBy;
|
|
}
|
|
get limit() {
|
|
return this.model.__bm__.localData[this.__bm_handle__].limit;
|
|
}
|
|
get offset() {
|
|
return this.model.__bm__.localData[this.__bm_handle__].offset;
|
|
}
|
|
get count() {
|
|
return this.currentIds.length;
|
|
}
|
|
|
|
abandonRecord(recordId) {
|
|
const record = this.records.find((r) => r.id === recordId);
|
|
this.model.__bm__.removeLine(record.__bm_handle__);
|
|
this.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
canResequence() {
|
|
return this.handleField || DEFAULT_HANDLE_FIELD in this.fields;
|
|
}
|
|
|
|
removeRecord(record) {
|
|
// if (true) { see _onRemoveRecord in rel fields
|
|
this.delete(record.id);
|
|
// }
|
|
}
|
|
|
|
async delete(recordId, operation = "DELETE") {
|
|
const record = this.records.find((r) => r.id === recordId);
|
|
await this.__syncParent({ operation, ids: [record.__bm_handle__] });
|
|
}
|
|
|
|
async add(object, params = { isM2M: false }) {
|
|
let operation;
|
|
const bm = this.model.__bm__;
|
|
if (object instanceof Record) {
|
|
const recHandle = object.__bm_handle__;
|
|
await bm.save(recHandle, { savePoint: !params.isM2M, viewType: object.__viewType });
|
|
if (params.isM2M) {
|
|
const id = bm.localData[recHandle].res_id;
|
|
operation = { operation: "ADD_M2M", ids: [{ id }] };
|
|
} else {
|
|
operation = { operation: "ADD", id: recHandle };
|
|
}
|
|
} else if (Array.isArray(object) && params.isM2M) {
|
|
const oldIds = this.resIds;
|
|
const newIds = object.filter((id) => !oldIds.includes(id)).map((id) => ({ id }));
|
|
operation = { operation: "ADD_M2M", ids: newIds };
|
|
}
|
|
await this.__syncParent(operation);
|
|
}
|
|
|
|
/** Creates a Draft record from nothing and edits it. Relevant in editable x2m's */
|
|
async addNew(params) {
|
|
const position = params.position;
|
|
const operation = { context: [params.context], operation: "CREATE", position };
|
|
await this.model.__bm__.save(this.__bm_handle__, { savePoint: true });
|
|
this.model.__bm__.freezeOrder(this.__bm_handle__);
|
|
await this.__syncParent(operation);
|
|
const newRecord = this.records[position === "bottom" ? this.records.length - 1 : 0];
|
|
if (params.mode === "edit") {
|
|
await newRecord.switchMode("edit");
|
|
}
|
|
return newRecord;
|
|
}
|
|
|
|
// x2many dialog edition
|
|
// addRecord(record) {
|
|
// this.onChanges();
|
|
// this._cache[record.virtualId] = record;
|
|
// this._cache[record.virtualId] = record;
|
|
// this.records.push(record);
|
|
// this.resIds.push(record.virtualId);
|
|
// this.limit = this.limit + 1; // might be not good
|
|
// this.validated[record.virtualId] = false;
|
|
// this.model.notify();
|
|
// }
|
|
|
|
exportState() {
|
|
return {
|
|
limit: this.limit,
|
|
};
|
|
}
|
|
|
|
async load(params) {
|
|
await this.model.__bm__._reload(this.__bm_handle__, params);
|
|
this.__syncData();
|
|
}
|
|
|
|
async sortBy(fieldName) {
|
|
await this.model.__bm__.setSort(this.__bm_handle__, fieldName);
|
|
this.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
async replaceWith(resIds) {
|
|
const addedIds = resIds.filter((id) => !this.currentIds.includes(id));
|
|
const deletedIds = this.currentIds.filter((id) => !resIds.includes(id));
|
|
let operation;
|
|
if (addedIds.length && deletedIds.length) {
|
|
operation = {
|
|
ids: resIds,
|
|
operation: "REPLACE_WITH",
|
|
};
|
|
} else if (addedIds.length) {
|
|
operation = {
|
|
ids: addedIds.map((id) => ({ id })),
|
|
operation: "ADD_M2M",
|
|
};
|
|
} else if (deletedIds.length) {
|
|
operation = {
|
|
ids: deletedIds,
|
|
operation: "FORGET",
|
|
};
|
|
} else {
|
|
// no change?
|
|
return;
|
|
}
|
|
await this.__syncParent(operation);
|
|
}
|
|
|
|
/**
|
|
* @param {Array[]} commands array of commands
|
|
*/
|
|
async applyCommands(fieldName, commands) {
|
|
const commandsWithId = commands.map((command) => {
|
|
return {
|
|
operation: command.operation,
|
|
id: command.record.__bm_handle__,
|
|
data: command.data,
|
|
};
|
|
});
|
|
|
|
const parentID = this.model.__bm__.localData[this.__bm_handle__].parentID;
|
|
await this.model.__bm__.notifyChanges(parentID, {
|
|
[fieldName]: {
|
|
operation: "MULTI",
|
|
commands: commandsWithId,
|
|
},
|
|
});
|
|
|
|
this.model.root.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
/**
|
|
* @param {string} dataRecordId
|
|
* @param {string} dataGroupId
|
|
* @param {string} refId
|
|
* @param {string} targetGroupId
|
|
* @returns {Promise<Record>}
|
|
*/
|
|
async moveRecord(dataRecordId, dataGroupId, refId, targetGroupId) {
|
|
await this.resequence(dataRecordId, refId);
|
|
}
|
|
|
|
/**
|
|
* @param {RecordId} movedId // id of the moved record
|
|
* @param {RecordId | null} targetId // id of the record (if any) that must be before moved record after operation is done
|
|
*/
|
|
async resequence(movedId, targetId) {
|
|
if (!this.canResequence()) {
|
|
// There is no handle field on the current model
|
|
return;
|
|
}
|
|
|
|
if (this.__viewType === "list") {
|
|
await this.model.__bm__.save(this.__bm_handle__, { savePoint: true });
|
|
this.model.__bm__.freezeOrder(this.__bm_handle__);
|
|
}
|
|
|
|
const handleField = this.handleField || DEFAULT_HANDLE_FIELD;
|
|
const records = [...this.records];
|
|
const order = this.orderBy.find((o) => o.name === handleField);
|
|
const asc = !order || order.asc;
|
|
|
|
// Find indices
|
|
const fromIndex = records.findIndex((r) => r.id === movedId);
|
|
let toIndex = 0;
|
|
if (targetId !== null) {
|
|
const targetIndex = records.findIndex((r) => r.id === targetId);
|
|
toIndex = fromIndex > targetIndex ? targetIndex + 1 : targetIndex;
|
|
}
|
|
|
|
const getSequence = (rec) => rec && rec.data[handleField];
|
|
|
|
// Determine what records need to be modified
|
|
const firstIndex = Math.min(fromIndex, toIndex);
|
|
const lastIndex = Math.max(fromIndex, toIndex) + 1;
|
|
let reorderAll = false;
|
|
let lastSequence = (asc ? -1 : 1) * Infinity;
|
|
for (let index = 0; index < records.length; index++) {
|
|
const sequence = getSequence(records[index]);
|
|
if (
|
|
((index < firstIndex || index >= lastIndex) &&
|
|
((asc && lastSequence >= sequence) || (!asc && lastSequence <= sequence))) ||
|
|
(index >= firstIndex && index < lastIndex && lastSequence === sequence)
|
|
) {
|
|
reorderAll = true;
|
|
}
|
|
lastSequence = sequence;
|
|
}
|
|
|
|
// Perform the resequence in the list of records
|
|
const [record] = records.splice(fromIndex, 1);
|
|
records.splice(toIndex, 0, record);
|
|
|
|
// Creates the list of to modify
|
|
let toReorder = records;
|
|
if (!reorderAll) {
|
|
toReorder = toReorder.slice(firstIndex, lastIndex).filter((r) => r.id !== movedId);
|
|
if (fromIndex < toIndex) {
|
|
toReorder.push(record);
|
|
} else {
|
|
toReorder.unshift(record);
|
|
}
|
|
}
|
|
if (!asc) {
|
|
toReorder.reverse();
|
|
}
|
|
|
|
const sequences = toReorder.map(getSequence);
|
|
const offset = sequences.length && Math.min(...sequences);
|
|
|
|
const operations = toReorder.map((record, i) => ({
|
|
operation: "UPDATE",
|
|
id: record.__bm_handle__,
|
|
data: { [handleField]: offset + i },
|
|
}));
|
|
const lastOperation = operations.pop();
|
|
|
|
const parentID = this.model.__bm__.localData[this.__bm_handle__].parentID;
|
|
|
|
await Promise.all(
|
|
operations.map((op) => {
|
|
this.model.__bm__.notifyChanges(
|
|
parentID,
|
|
{ [this.__fieldName__]: op },
|
|
{ notifyChange: false }
|
|
);
|
|
})
|
|
);
|
|
|
|
try {
|
|
await this.model.__bm__.notifyChanges(parentID, {
|
|
[this.__fieldName__]: lastOperation,
|
|
});
|
|
} finally {
|
|
if (this.__viewType === "list") {
|
|
await this.model.__bm__.setSort(this.__bm_handle__, handleField);
|
|
}
|
|
}
|
|
|
|
this.records = records;
|
|
|
|
this.__syncData();
|
|
this.model.notify();
|
|
}
|
|
|
|
async unselectRecord(canDiscard = false) {
|
|
// something seems wrong with switchMode --> review system?
|
|
const editedRecord = this.editedRecord;
|
|
if (!editedRecord) {
|
|
return true;
|
|
}
|
|
|
|
const isValid = await editedRecord.checkValidity();
|
|
const handle = editedRecord.__bm_handle__;
|
|
if (!editedRecord.isDirty && ((canDiscard && editedRecord.isNew) || !isValid)) {
|
|
this.model.__bm__.discardChanges(handle);
|
|
if (editedRecord.canBeAbandoned) {
|
|
this.model.__bm__.removeLine(handle);
|
|
}
|
|
this.__syncData();
|
|
this.editedRecord = null;
|
|
return await editedRecord.switchMode("readonly");
|
|
} else if (isValid) {
|
|
return await editedRecord.switchMode("readonly");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export class RelationalModel extends Model {
|
|
setup(params, { action, dialog, notification }) {
|
|
this.actionService = action;
|
|
this.dialogService = dialog;
|
|
this.notificationService = notification;
|
|
this.keepLast = new KeepLast();
|
|
|
|
if (params.rootType !== "record") {
|
|
throw "only record root type is supported";
|
|
}
|
|
|
|
this.root = null;
|
|
|
|
this.__bm__ = new BasicModel(this, {
|
|
fields: params.fields || {},
|
|
modelName: params.resModel,
|
|
useSampleModel: false, // FIXME AAB
|
|
});
|
|
this.__activeFields = params.activeFields;
|
|
this.__fields = params.fields;
|
|
|
|
const res_id = params.resId || undefined;
|
|
const res_ids = (params.resIds ? toRaw(params.resIds) : null) || (res_id ? [res_id] : []);
|
|
|
|
this.__bm_load_params__ = {
|
|
type: "record",
|
|
modelName: params.resModel,
|
|
res_id,
|
|
res_ids,
|
|
fields: params.fields || {},
|
|
viewType: "form",
|
|
};
|
|
|
|
this.initialMode = params.mode;
|
|
}
|
|
|
|
async duplicateDatapoint(record, params) {
|
|
const bm = this.__bm__;
|
|
const fieldsInfo = mapViews(params.views, this.env);
|
|
const handle = record.__bm_handle__;
|
|
|
|
// Sync with the mutex to wait for potential onchanges
|
|
await bm.mutex.getUnlockedDef();
|
|
|
|
await bm.addFieldsInfo(handle, {
|
|
fields: params.fields,
|
|
viewType: params.viewMode,
|
|
fieldInfo: fieldsInfo[params.viewMode].fieldsInfo[params.viewMode],
|
|
});
|
|
|
|
// determine fieldNames to load (comes from basic_view.js)
|
|
const legRec = this.__bm__.get(record.__bm_handle__);
|
|
const viewType = params.viewMode;
|
|
const viewFields = Object.keys(legRec.fieldsInfo[viewType]);
|
|
const fieldNames = _.difference(viewFields, Object.keys(legRec.data));
|
|
const legFieldsInfo = legRec.fieldsInfo[viewType];
|
|
|
|
// Suppose that in a form view, there is an x2many list view with
|
|
// a field F, and that F is also displayed in the x2many form view.
|
|
// In this case, F is represented in legRec.data (as it is known by
|
|
// the x2many list view), but the loaded information may not suffice
|
|
// in the form view (e.g. if field is a many2many list in the form
|
|
// view, or if it is displayed by a widget requiring specialData).
|
|
// So when this happens, F is added to the list of fieldNames to fetch.
|
|
for (const name of viewFields) {
|
|
if (!fieldNames.includes(name)) {
|
|
const fieldType = legRec.fields[name].type;
|
|
const fieldInfo = legFieldsInfo[name];
|
|
// SpecialData case: field requires specialData that haven't
|
|
// been fetched yet.
|
|
if (fieldInfo.specialData && !(name in legRec.specialData)) {
|
|
fieldNames.push(name);
|
|
continue;
|
|
}
|
|
|
|
// X2Many case: field is an x2many displayed as a list or
|
|
// kanban view, but the related fields haven't been loaded yet.
|
|
if (fieldType === "one2many" || fieldType === "many2many") {
|
|
if (!("fieldsInfo" in legRec.data[name])) {
|
|
fieldNames.push(name);
|
|
} else {
|
|
const x2mFieldInfo = legRec.fieldsInfo[params.viewMode][name];
|
|
const viewType = x2mFieldInfo.viewType || x2mFieldInfo.mode;
|
|
const knownFields = Object.keys(
|
|
legRec.data[name].fieldsInfo[legRec.data[name].viewType] || {}
|
|
);
|
|
const newFields = Object.keys(legRec.data[name].fieldsInfo[viewType] || {});
|
|
if (newFields.filter((f) => !knownFields.includes(f)).length) {
|
|
fieldNames.push(name);
|
|
}
|
|
|
|
if (legRec.data[name].viewType === "default") {
|
|
// Use case: x2many (tags) in x2many list views
|
|
// When opening the x2many legRec form view, the
|
|
// x2many will be reloaded but it may not have
|
|
// the same fields (ex: tags in list and list in
|
|
// form) so we need to merge the fieldsInfo to
|
|
// avoid losing the initial fields (display_name)
|
|
const fieldViews = fieldInfo.views || fieldInfo.fieldsInfo || {};
|
|
const defaultFieldInfo = legRec.data[name].fieldsInfo.default;
|
|
_.each(fieldViews, function (fieldView) {
|
|
_.defaults(fieldView.fields, defaultFieldInfo);
|
|
_.each(fieldView.fieldsInfo, function (x2mFieldInfo) {
|
|
_.defaults(x2mFieldInfo, defaultFieldInfo);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Many2one: context is not the same between the different views
|
|
// this means the result of a name_get could differ
|
|
if (fieldType === "many2one") {
|
|
if (
|
|
JSON.stringify(legRec.data[name].context) !==
|
|
JSON.stringify(fieldInfo.context)
|
|
) {
|
|
fieldNames.push(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fieldNames.length) {
|
|
if (this.__bm__.isNew(record.__bm_handle__)) {
|
|
await this.__bm__.generateDefaultValues(record.__bm_handle__, {
|
|
fieldNames: fieldNames,
|
|
viewType: viewType,
|
|
});
|
|
} else {
|
|
await this.__bm__.reload(record.__bm_handle__, {
|
|
fieldNames: fieldNames,
|
|
keepChanges: true,
|
|
viewType: viewType,
|
|
});
|
|
}
|
|
}
|
|
|
|
const Record = this.constructor.Record;
|
|
const newRecord = new Record(this, {
|
|
handle,
|
|
viewType: params.viewMode,
|
|
mode: params.mode,
|
|
});
|
|
|
|
const recordSave = Record.prototype.save;
|
|
record.save = recordSave;
|
|
record.save = async (...args) => {
|
|
record.__syncData();
|
|
const res = await recordSave.call(record, ...args);
|
|
record.save = recordSave;
|
|
return res;
|
|
};
|
|
|
|
return newRecord;
|
|
}
|
|
async addNewRecord(list, params, withParentId = true) {
|
|
const parentId = this.__bm__.localData[list.__bm_handle__].parentID;
|
|
const fieldName = list.__fieldName__;
|
|
const context = this.__bm__._getContext(this.__bm__.localData[parentId], { fieldName });
|
|
params.context = makeContext([context, params.context]);
|
|
params.__syncParent = () => list.__syncData();
|
|
const newRecord = this.createDataPoint("record", params);
|
|
if (withParentId) {
|
|
newRecord.__bm_load_params__.parentID = list.__bm_handle__;
|
|
}
|
|
await newRecord.load();
|
|
return newRecord;
|
|
}
|
|
async updateRecord(list, record, params = { isM2M: false }) {
|
|
let operation;
|
|
const isM2M = params.isM2M;
|
|
if (!isM2M) {
|
|
operation = { operation: "UPDATE", id: record.__bm_handle__ };
|
|
} else {
|
|
await record.save({ noReload: true });
|
|
operation = { operation: "TRIGGER_ONCHANGE" };
|
|
}
|
|
await list.__syncParent(operation);
|
|
if (isM2M) {
|
|
await record.load();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Object} [params={}]
|
|
* @param {Comparison | null} [params.comparison]
|
|
* @param {Context} [params.context]
|
|
* @param {DomainListRepr} [params.domain]
|
|
* @param {string[]} [params.groupBy]
|
|
* @param {Object[]} [params.orderBy]
|
|
* @param {number} [params.resId] should not be there
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async load(params = {}) {
|
|
if (!this.__bm_load_params__.fieldsInfo) {
|
|
// only the first time (must wait for the subviews to be loaded)
|
|
this.__bm_load_params__.fieldsInfo = mapActiveFieldsToFieldsInfo(
|
|
this.__activeFields || {},
|
|
this.__fields || {},
|
|
"form",
|
|
this.env
|
|
);
|
|
}
|
|
const loadParams = { ...this.__bm_load_params__ };
|
|
if ("resId" in params) {
|
|
loadParams.res_id = params.resId || undefined;
|
|
}
|
|
if ("context" in params) {
|
|
loadParams.context = params.context;
|
|
}
|
|
const state = this.root ? this.root.exportState() : {};
|
|
const nextRoot = new this.constructor.Record(
|
|
this,
|
|
{
|
|
__bm_load_params__: loadParams,
|
|
mode: this.initialMode,
|
|
},
|
|
state
|
|
);
|
|
await this.keepLast.add(nextRoot.load());
|
|
this.root = nextRoot;
|
|
this.__bm_load_params__ = loadParams;
|
|
this.notify();
|
|
}
|
|
_trigger_up(ev) {
|
|
const evType = ev.name;
|
|
const payload = ev.data;
|
|
if (evType === "call_service") {
|
|
let args = payload.args || [];
|
|
if (payload.service === "ajax" && payload.method === "rpc") {
|
|
// ajax service uses an extra 'target' argument for rpc
|
|
args = args.concat(ev.target);
|
|
return payload.callback(owl.Component.env.session.rpc(...args));
|
|
} else if (payload.service === "notification") {
|
|
return this.notificationService.add(payload.message, {
|
|
className: payload.className,
|
|
sticky: payload.sticky,
|
|
title: payload.title,
|
|
type: payload.type,
|
|
});
|
|
}
|
|
throw new Error(`call service ${payload.service} not handled in relational model`);
|
|
} else if (evType === "warning") {
|
|
if (payload.type === "dialog") {
|
|
return this.dialogService.add(WarningDialog, {
|
|
title: payload.title,
|
|
message: payload.message,
|
|
});
|
|
} else {
|
|
return this.notificationService.add(payload.message, {
|
|
className: payload.className,
|
|
sticky: payload.sticky,
|
|
title: payload.title,
|
|
type: "warning",
|
|
});
|
|
}
|
|
} else if (evType === "do_action") {
|
|
// SAME CODE AS legacy_service_provider
|
|
if (payload.action.context) {
|
|
payload.action.context = new Context(payload.action.context).eval();
|
|
}
|
|
const legacyOptions = mapDoActionOptionAPI(payload.options);
|
|
return this.actionService.doAction(payload.action, legacyOptions);
|
|
}
|
|
throw new Error(`trigger_up(${evType}) not handled in relational model`);
|
|
}
|
|
createDataPoint(type, params, state = {}) {
|
|
if (type !== "record") {
|
|
throw new Error("LPE CRASH");
|
|
}
|
|
const fieldsInfo = mapActiveFieldsToFieldsInfo(
|
|
params.activeFields,
|
|
params.fields,
|
|
params.viewType,
|
|
this.env
|
|
);
|
|
params = Object.assign({}, params, {
|
|
__bm_load_params__: {
|
|
type: "record",
|
|
modelName: params.resModel,
|
|
fields: params.fields || {},
|
|
viewType: "form",
|
|
fieldsInfo,
|
|
parentID: params.parentID,
|
|
context: params.context,
|
|
},
|
|
});
|
|
return new this.constructor.Record(this, params, state);
|
|
}
|
|
}
|
|
RelationalModel.services = ["action", "dialog", "notification"];
|
|
RelationalModel.Record = Record;
|