[REF] web: remove legacy fields/widgets compatibility layer
This commit removes the code that wrapped legacy fields and widgets (coming from the legacy registries) into owl components and added them to the wowl registries. This temporary compatibility layer, which was incomplete and only allowed us to merge the new views without converting all fields/widgets in once without breaking everything, is no longer necessary as we now have converted all of them into owl components. X-original-commit: 555959c87e25faa71396f61479e8bb41b66ac164 Part-of: odoo/odoo#102606
This commit is contained in:
@@ -187,7 +187,6 @@ This module provides the core of the Odoo Web Client.
|
||||
|
||||
'web/static/src/core/utils/transitions.scss',
|
||||
'web/static/src/core/**/*',
|
||||
'web/static/src/legacy/legacy_fields.scss',
|
||||
'web/static/src/search/**/*',
|
||||
'web/static/src/webclient/icons.scss', # variables required in list_controller.scss
|
||||
'web/static/src/views/**/*',
|
||||
@@ -244,8 +243,6 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/legacy_dialog.js',
|
||||
'web/static/src/legacy/legacy_load_views.js',
|
||||
'web/static/src/legacy/legacy_views.js',
|
||||
'web/static/src/legacy/legacy_fields.js',
|
||||
'web/static/src/legacy/legacy_view_widgets.js',
|
||||
'web/static/src/legacy/legacy_promise_error_handler.js',
|
||||
'web/static/src/legacy/legacy_rpc_error_handler.js',
|
||||
'web/static/src/legacy/root_widget.js',
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { Domain } from "web.Domain";
|
||||
import legacyFieldRegistry from "web.field_registry";
|
||||
import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import { useWowlService } from "@web/legacy/utils";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
import { useTranslationDialog } from "@web/views/fields/translation_button";
|
||||
|
||||
const { Component, useEffect, xml } = owl;
|
||||
const fieldRegistry = registry.category("fields");
|
||||
|
||||
const legacyFieldTemplate = xml`
|
||||
<FieldAdapter Component="FieldWidget" fieldParams="fieldParams" update.bind="update" record="props.record" id="props.id"/>`;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// FieldAdapter
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class FieldAdapter extends ComponentAdapter {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.translationDialog = useTranslationDialog();
|
||||
this.wowlEnv = this.env;
|
||||
this.env = Component.env;
|
||||
this.orm = useWowlService("orm");
|
||||
useEffect(() => {
|
||||
if (!this.widgetEl || !this.widgetEl.parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldId = this.props.id;
|
||||
if (!this.widgetEl.querySelector(`#${fieldId}`)) {
|
||||
const $el = this.widget.getFocusableElement();
|
||||
if ($el && $el[0]) {
|
||||
$el[0].setAttribute("id", fieldId);
|
||||
}
|
||||
}
|
||||
|
||||
// if classNames are given to the field, we only want
|
||||
// to add those classes to the legacy field without
|
||||
// the parent element affecting the style of the field
|
||||
const fieldClassnames =
|
||||
this.props.fieldParams.options &&
|
||||
this.props.fieldParams.options.attrs &&
|
||||
this.props.fieldParams.options.attrs.class;
|
||||
if (fieldClassnames) {
|
||||
for (const className of fieldClassnames.split(" ")) {
|
||||
this.widgetEl.parentElement.classList.remove(className);
|
||||
this.widgetEl.classList.add(className);
|
||||
}
|
||||
this.widgetEl.parentElement.style.display = "contents";
|
||||
}
|
||||
this.widgetEl.parentElement.classList.remove("o_text_overflow");
|
||||
this.widgetEl.classList.add("o_legacy_field_widget");
|
||||
});
|
||||
this.lastFieldChangedEvent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get widgetArgs() {
|
||||
const { name, record, options } = this.props.fieldParams;
|
||||
return [name, record, options];
|
||||
}
|
||||
|
||||
async updateWidget(nextProps) {
|
||||
const { name, record, options } = nextProps.fieldParams;
|
||||
if (this.widget.mode !== options.mode) {
|
||||
// the mode changed, we need to instantiate a new FieldWidget
|
||||
if (this.oldWidget) {
|
||||
this.widget.destroy(); // we were already updating -> abort, and start over
|
||||
} else {
|
||||
this.oldWidget = this.widget;
|
||||
}
|
||||
this.widget = new this.props.Component(this, name, record, options);
|
||||
this.widgetProm = this.widget._widgetRenderAndInsert(() => {});
|
||||
return this.widgetProm;
|
||||
} else {
|
||||
// the mode is the same, simply reset the FieldWidget with the new record
|
||||
await this.widgetProm;
|
||||
return this.widget.reset(record, this.lastFieldChangedEvent, true);
|
||||
}
|
||||
}
|
||||
|
||||
renderWidget() {
|
||||
if (this.oldWidget) {
|
||||
const parentEl = this.oldWidget.el.parentElement;
|
||||
parentEl.replaceChild(this.widget.el, this.oldWidget.el);
|
||||
this.widgetEl = this.widget.el;
|
||||
if (this.widget.on_attach_callback) {
|
||||
this.widget.on_attach_callback();
|
||||
}
|
||||
this.oldWidget.destroy();
|
||||
this.oldWidget = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _trigger_up(ev) {
|
||||
const evType = ev.name;
|
||||
const payload = ev.data;
|
||||
if (evType === "set_dirty") {
|
||||
// TODO (not yet handled in the Record datapoint)
|
||||
} else if (evType === "field_changed") {
|
||||
const { name, record } = this.props.fieldParams;
|
||||
const proms = [];
|
||||
for (const fieldName in payload.changes) {
|
||||
let value = payload.changes[fieldName];
|
||||
const fieldType = record.fields[fieldName].type;
|
||||
if (fieldType === "many2one" && value) {
|
||||
value = [value.id, value.display_name];
|
||||
} else if (fieldType === "one2many" || fieldType === "many2many") {
|
||||
// TODO map all operations
|
||||
if (value.operation === "ADD_M2M") {
|
||||
const valueIds = value.ids; // can be a lot of stuff
|
||||
let newIds = [];
|
||||
if (Array.isArray(valueIds)) {
|
||||
if (typeof valueIds[0] === "number") {
|
||||
newIds = valueIds; // not sure if it is a real case: a list of ids
|
||||
} else if (valueIds.length && "id" in valueIds[0]) {
|
||||
newIds = valueIds.map((r) => r.id);
|
||||
}
|
||||
} else if ("id" in valueIds && valueIds.id) {
|
||||
newIds = [valueIds.id];
|
||||
} else if ("id" in valueIds) {
|
||||
const fieldName = this.props.fieldParams.name;
|
||||
let newId;
|
||||
try {
|
||||
[newId] = await this.orm.call(
|
||||
this.props.record.fields[fieldName].relation,
|
||||
"name_create",
|
||||
[valueIds.display_name],
|
||||
{
|
||||
context: this.props.record.getFieldContext(fieldName),
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
if (!(e instanceof RPCError)) {
|
||||
throw e;
|
||||
}
|
||||
if (payload.onFailure) {
|
||||
// wrap error for guardedCatch compatibility in legacy code
|
||||
payload.onFailure({
|
||||
message: e,
|
||||
event: $.Event(),
|
||||
legacy: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
newIds = [newId];
|
||||
}
|
||||
value = {
|
||||
resIds: [...record.data[name].res_ids, ...newIds],
|
||||
operation: "REPLACE_WITH",
|
||||
};
|
||||
}
|
||||
} else if (fieldType === "date" || fieldType === "datetime") {
|
||||
// from moment to luxon
|
||||
value = value ? luxon.DateTime.fromISO(value.toISOString()) : false;
|
||||
}
|
||||
proms.push(this.props.update(fieldName, value));
|
||||
}
|
||||
this.lastFieldChangedEvent = ev;
|
||||
await Promise.all(proms);
|
||||
if (payload.onSuccess) {
|
||||
payload.onSuccess();
|
||||
}
|
||||
// TODO: handle onFailure?
|
||||
} else if (evType === "reload") {
|
||||
const record = this.props.record;
|
||||
if (payload.db_id === record.id) {
|
||||
await record.model.reloadRecords(record);
|
||||
} else {
|
||||
await record.model.root.load();
|
||||
record.model.notify();
|
||||
}
|
||||
if (payload.onSuccess) {
|
||||
payload.onSuccess();
|
||||
}
|
||||
} else if (evType === "history_back") {
|
||||
return this.wowlEnv.config.historyBack();
|
||||
} else if (evType === "translate") {
|
||||
const { fieldParams, record, update } = this.props;
|
||||
return this.translationDialog({
|
||||
fieldName: fieldParams.name,
|
||||
record,
|
||||
updateField: update,
|
||||
});
|
||||
}
|
||||
super._trigger_up(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers to map wowl datapoints to basic model datapoints
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let nextId = 1;
|
||||
function createMany2OneDatapoint([resId, displayName]) {
|
||||
return {
|
||||
id: `m2o_${nextId++}`,
|
||||
data: { id: resId, display_name: displayName },
|
||||
fields: {
|
||||
display_name: { type: "char" },
|
||||
id: { type: "integer" },
|
||||
},
|
||||
res_id: resId,
|
||||
ref: resId,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDatapoint(datapoint) {
|
||||
return {
|
||||
id: datapoint.id,
|
||||
model: datapoint.resModel,
|
||||
fields: datapoint.fields,
|
||||
fieldsInfo: {
|
||||
default: datapoint.activeFields, // TODO: how to handle viewTypes?
|
||||
},
|
||||
getContext: () => [], // TODO: not yet handled in wowl
|
||||
getDomain: () => [], // TODO: not yet handled in wowl
|
||||
getFieldNames: () => datapoint.fieldNames,
|
||||
};
|
||||
}
|
||||
|
||||
function mapStaticListDatapoint(staticList) {
|
||||
return {
|
||||
...mapDatapoint(staticList),
|
||||
res_ids: staticList.currentIds,
|
||||
data: staticList.records.map(mapRecordDatapoint),
|
||||
groupedBy: [],
|
||||
orderedBy: staticList.orderBy,
|
||||
count: staticList.count,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapRecordDatapoint(record) {
|
||||
const data = Object.assign({}, record.data);
|
||||
for (const fieldName of record.fieldNames) {
|
||||
const fieldType = record.fields[fieldName].type;
|
||||
switch (fieldType) {
|
||||
case "one2many":
|
||||
case "many2many":
|
||||
data[fieldName] = mapStaticListDatapoint(data[fieldName]);
|
||||
break;
|
||||
case "many2one":
|
||||
data[fieldName] = data[fieldName] && createMany2OneDatapoint(data[fieldName]);
|
||||
break;
|
||||
case "date":
|
||||
case "datetime":
|
||||
// from luxon to moment
|
||||
data[fieldName] = data[fieldName] ? moment(data[fieldName].toISO()) : false;
|
||||
}
|
||||
}
|
||||
data.id = record.resId;
|
||||
const basicModelRecord = {
|
||||
...mapDatapoint(record),
|
||||
res_id: record.resId,
|
||||
data,
|
||||
};
|
||||
Object.defineProperty(basicModelRecord, "evalContext", {
|
||||
get: () => record.evalContext,
|
||||
});
|
||||
Object.defineProperty(basicModelRecord, "isDirty", {
|
||||
get: () => record.isDirty,
|
||||
});
|
||||
Object.defineProperty(basicModelRecord, "isNew", {
|
||||
get: () => record.isDirty,
|
||||
});
|
||||
basicModelRecord.evalModifiers = (modifiers) => {
|
||||
let evalContext = null;
|
||||
const evaluated = {};
|
||||
for (const k of ["invisible", "column_invisible", "readonly", "required"]) {
|
||||
const mod = modifiers[k];
|
||||
if (mod === undefined || mod === false || mod === true) {
|
||||
if (k in modifiers) {
|
||||
evaluated[k] = !!mod;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
evalContext = evalContext || basicModelRecord.evalContext;
|
||||
evaluated[k] = new Domain(mod, evalContext).compute(evalContext);
|
||||
} catch (e) {
|
||||
throw new Error(sprintf('for modifier "%s": %s', k, e.message));
|
||||
}
|
||||
}
|
||||
return evaluated;
|
||||
};
|
||||
return basicModelRecord;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Register legacy field widgets to the wowl field registry (wrapped in a Component)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function registerField(name, LegacyFieldWidget) {
|
||||
class LegacyField extends Component {
|
||||
setup() {
|
||||
this.FieldWidget = LegacyFieldWidget;
|
||||
}
|
||||
|
||||
get fieldParams() {
|
||||
const { name, record } = this.props;
|
||||
let legacyRecord;
|
||||
const fieldInfo = record.activeFields[name];
|
||||
let views = {};
|
||||
const modifiers = JSON.parse(fieldInfo.rawAttrs.modifiers || "{}");
|
||||
const hasReadonlyModifier = record.isReadonly(name);
|
||||
if (record.model.__bm__) {
|
||||
legacyRecord = record.model.__bm__.get(record.__bm_handle__);
|
||||
const form = legacyRecord.fieldsInfo.form;
|
||||
views = form && form[name].views;
|
||||
} else {
|
||||
legacyRecord = mapRecordDatapoint(record);
|
||||
}
|
||||
const options = {
|
||||
attrs: {
|
||||
...fieldInfo.rawAttrs,
|
||||
modifiers,
|
||||
options: fieldInfo.options,
|
||||
widget: fieldInfo.widget,
|
||||
views,
|
||||
mode: fieldInfo.viewMode,
|
||||
},
|
||||
viewType: legacyRecord.viewType,
|
||||
mode: this.props.readonly || hasReadonlyModifier ? "readonly" : record.mode,
|
||||
hasReadonlyModifier,
|
||||
};
|
||||
return { name, record: legacyRecord, options };
|
||||
}
|
||||
|
||||
update(fieldName, value) {
|
||||
if (fieldName === this.props.name) {
|
||||
const record = this.props.record;
|
||||
const fieldType = record.fields[fieldName].type;
|
||||
if (["one2many", "many2many"].includes(fieldType) && !record.model.__bm__) {
|
||||
const staticList = this.props.value;
|
||||
switch (value.operation) {
|
||||
case "REPLACE_WITH": {
|
||||
return staticList.replaceWith(value.resIds);
|
||||
}
|
||||
case "FORGET": {
|
||||
return staticList.delete(value.ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.props.update(value);
|
||||
} else {
|
||||
return this.props.record.update({ [fieldName]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
LegacyField.template = legacyFieldTemplate;
|
||||
LegacyField.components = { FieldAdapter };
|
||||
LegacyField.legacySpecialData = LegacyFieldWidget.prototype.specialData;
|
||||
LegacyField.fieldsToFetch = LegacyFieldWidget.prototype.fieldsToFetch || {};
|
||||
LegacyField.fieldDependencies = LegacyFieldWidget.prototype.fieldDependencies || {};
|
||||
LegacyField.useSubView = LegacyFieldWidget.prototype.useSubview;
|
||||
LegacyField.noLabel = LegacyFieldWidget.prototype.noLabel || false;
|
||||
if (!fieldRegistry.contains(name)) {
|
||||
if (odoo.debug) {
|
||||
console.log(`Fields: using legacy ${name} FieldWidget`);
|
||||
}
|
||||
fieldRegistry.add(name, LegacyField);
|
||||
}
|
||||
}
|
||||
|
||||
// register fields already in the legacy registry, and listens to future registrations
|
||||
for (const [name, FieldWidget] of Object.entries(legacyFieldRegistry.entries())) {
|
||||
registerField(name, FieldWidget);
|
||||
}
|
||||
legacyFieldRegistry.onAdd(registerField);
|
||||
@@ -1,7 +0,0 @@
|
||||
.o_debug {
|
||||
.o_form_view, .o_list_view, .o_kanban_view {
|
||||
.o_legacy_field_widget > * {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import legacyWidgetRegistry from "web.widget_registry";
|
||||
import { mapRecordDatapoint } from "./legacy_fields";
|
||||
|
||||
const { Component, useEffect, xml } = owl;
|
||||
const viewWidgetRegistry = registry.category("view_widgets");
|
||||
|
||||
const legacyWidgetTemplate = xml`
|
||||
<ViewWidgetAdapter Component="ViewWidget" viewWidgetParams="viewWidgetParams" />`;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ViewWidgetAdapter
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
class ViewWidgetAdapter extends ComponentAdapter {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.wowlEnv = this.env;
|
||||
this.env = Component.env;
|
||||
useEffect(() => {
|
||||
this.widgetEl.parentElement.classList.remove("o_text_overflow");
|
||||
this.widgetEl.classList.add("o_legacy_widget");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get widgetArgs() {
|
||||
return this.getParams(this.props);
|
||||
}
|
||||
|
||||
getParams(props) {
|
||||
const { record, node, options, readonly } = props.viewWidgetParams;
|
||||
return [record, node, { mode: readonly ? "readonly" : "edit", ...options }];
|
||||
}
|
||||
|
||||
updateWidget(nextProps) {
|
||||
if (this.oldWidget) {
|
||||
this.widget.destroy(); // we were already updating -> abort, and start over
|
||||
} else {
|
||||
this.oldWidget = this.widget;
|
||||
}
|
||||
this.widget = new this.props.Component(this, ...this.getParams(nextProps));
|
||||
return this.widget._widgetRenderAndInsert(() => {});
|
||||
}
|
||||
|
||||
renderWidget() {
|
||||
if (this.oldWidget) {
|
||||
const parentEl = this.oldWidget.el.parentElement;
|
||||
parentEl.replaceChild(this.widget.el, this.oldWidget.el);
|
||||
this.widgetEl = this.widget.el;
|
||||
if (this.widget.on_attach_callback) {
|
||||
this.widget.on_attach_callback();
|
||||
}
|
||||
this.oldWidget.destroy();
|
||||
this.oldWidget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Register legacy widgets to the wowl widget registry (wrapped in a Component)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function registerWidget(name, LegacyWidgetWidget) {
|
||||
class LegacyViewWidget extends Component {
|
||||
setup() {
|
||||
this.ViewWidget = LegacyWidgetWidget;
|
||||
}
|
||||
get viewWidgetParams() {
|
||||
const { record } = this.props;
|
||||
let legacyRecord;
|
||||
if (record.model.__bm__) {
|
||||
legacyRecord = record.model.__bm__.get(record.__bm_handle__);
|
||||
} else {
|
||||
legacyRecord = mapRecordDatapoint(record);
|
||||
}
|
||||
const options = {
|
||||
viewType: legacyRecord.viewType,
|
||||
mode: record.mode,
|
||||
};
|
||||
return { ...this.props, record: legacyRecord, options };
|
||||
}
|
||||
}
|
||||
LegacyViewWidget.template = legacyWidgetTemplate;
|
||||
LegacyViewWidget.components = { ViewWidgetAdapter };
|
||||
if (!viewWidgetRegistry.contains(name)) {
|
||||
if (odoo.debug) {
|
||||
console.log(`View widgets: using legacy ${name} Widget`);
|
||||
}
|
||||
viewWidgetRegistry.add(name, LegacyViewWidget);
|
||||
}
|
||||
}
|
||||
|
||||
// register widgets already in the legacy registry, and listens to future registrations
|
||||
for (const [name, Widget] of Object.entries(legacyWidgetRegistry.entries())) {
|
||||
registerWidget(name, Widget);
|
||||
}
|
||||
legacyWidgetRegistry.onAdd(registerWidget);
|
||||
@@ -3,12 +3,6 @@
|
||||
* all renderers. For field default rules, see the fields.scss file.
|
||||
*/
|
||||
|
||||
.o_debug .o_legacy_form_view,
|
||||
.o_debug .o_legacy_kanban_view,
|
||||
.o_debug .o_legacy_list_view {
|
||||
border-top: 3px solid red;
|
||||
}
|
||||
|
||||
// Invisible modifier (can be inside the view, the button area, ...)
|
||||
.o_invisible_modifier {
|
||||
display: none!important;
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import fieldRegistry from "web.field_registry";
|
||||
|
||||
export function getTooltipInfo(params) {
|
||||
let widgetDescription = undefined;
|
||||
if (params.fieldInfo.widget) {
|
||||
if (params.fieldInfo.FieldComponent.name === "LegacyField") {
|
||||
const widget = fieldRegistry.get(params.fieldInfo.widget);
|
||||
widgetDescription = widget.prototype.description;
|
||||
} else {
|
||||
widgetDescription = params.fieldInfo.FieldComponent.displayName;
|
||||
}
|
||||
widgetDescription = params.fieldInfo.FieldComponent.displayName;
|
||||
}
|
||||
|
||||
const info = {
|
||||
|
||||
@@ -20,9 +20,6 @@ import { session } from "@web/session";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import legacyFieldRegistry from "web.field_registry";
|
||||
import { FieldMany2ManyTags } from "web.relational_fields";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
@@ -2123,40 +2120,4 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("many2many legacy field in list add a record", async (assert) => {
|
||||
const myM2M = FieldMany2ManyTags.extend({});
|
||||
legacyFieldRegistry.add("many2many_tags_legacy", myM2M);
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="timmy" widget="many2many_tags_legacy"/>
|
||||
</tree>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
assert.step(`write: ${JSON.stringify(args.args[1])}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(target, ".o_badge_text");
|
||||
await click(target.querySelectorAll(".o_data_cell")[0]);
|
||||
await click(target, ".o_legacy_field_widget input");
|
||||
await click(document.querySelectorAll(".ui-autocomplete .dropdown-item")[0]);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_badge_text").textContent, "gold");
|
||||
await click(target);
|
||||
assert.verifySteps([`write: {"timmy":[[6,false,[12]]]}`]);
|
||||
|
||||
await click(target.querySelectorAll(".o_data_cell")[0]);
|
||||
await click(target.querySelector(".badge .o_delete"));
|
||||
await click(target);
|
||||
assert.containsNone(target, ".o_badge_text");
|
||||
assert.verifySteps([`write: {"timmy":[[6,false,[]]]}`]);
|
||||
delete legacyFieldRegistry.map["many2many_tags_legacy"];
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import AbstractField from "web.AbstractField";
|
||||
import {
|
||||
addRow,
|
||||
click,
|
||||
@@ -26,9 +25,7 @@ import {
|
||||
import BasicModel from "web.BasicModel";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { FieldOne2Many } from "web.relational_fields";
|
||||
import { getNextTabableElement } from "@web/core/utils/ui";
|
||||
import fieldRegistry from "web.field_registry";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { registry } from "@web/core/registry";
|
||||
@@ -5504,138 +5501,6 @@ QUnit.module("Fields", (hooks) => {
|
||||
await click(target, ".oe_kanban_action_button");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"one2many kanban with edit type action and widget with specialData",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
patchWithCleanup(BasicModel.prototype, {
|
||||
_fetchSpecialDataForMyWidget() {
|
||||
assert.step("_fetchSpecialDataForMyWidget");
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
const MyWidget = AbstractField.extend({
|
||||
specialData: "_fetchSpecialDataForMyWidget",
|
||||
className: "my_widget",
|
||||
});
|
||||
fieldRegistry.add("specialWidget", MyWidget);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
// turtle_int is displayed without widget in the kanban, and with
|
||||
// widget required specialData in the form
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="turtles" mode="kanban">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="display_name"/>
|
||||
<field name="turtle_foo"/>
|
||||
<field name="turtle_int"/>
|
||||
<a type="edit"> Edit </a>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="product_id" widget="statusbar"/>
|
||||
<field name="turtle_int" widget="specialWidget"/>
|
||||
</form>
|
||||
</field>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".oe_kanban_action"));
|
||||
assert.containsOnce(target, ".modal .my_widget");
|
||||
assert.verifySteps(["_fetchSpecialDataForMyWidget"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("one2many list with onchange and widget using SpecialData", async function (assert) {
|
||||
patchWithCleanup(BasicModel.prototype, {
|
||||
_fetchSpecialDataForMyWidget() {
|
||||
assert.step("_fetchSpecialDataForMyWidget");
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
const MyWidget = AbstractField.extend({
|
||||
specialData: "_fetchSpecialDataForMyWidget",
|
||||
className: "my_widget",
|
||||
});
|
||||
fieldRegistry.add("specialWidget", MyWidget);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
turtles: function (obj) {
|
||||
var virtualID = obj.turtles[1][1];
|
||||
obj.turtles = [
|
||||
[5], // delete all
|
||||
[
|
||||
0,
|
||||
virtualID,
|
||||
{
|
||||
display_name: "coucou",
|
||||
product_id: [37, "xphone"],
|
||||
turtle_bar: false,
|
||||
turtle_foo: "has changed",
|
||||
turtle_int: 42,
|
||||
turtle_qux: 9.8,
|
||||
partner_ids: [],
|
||||
turtle_ref: "product,37",
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
// turtle_int is displayed without widget in the list, and with
|
||||
// widget required specialData in the form
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="turtles" mode="tree">
|
||||
<tree>
|
||||
<field name="display_name"/>
|
||||
<field name="turtle_foo"/>
|
||||
<field name="turtle_int"/>
|
||||
</tree>
|
||||
<form>
|
||||
<field name="turtle_int" widget="specialWidget"/>
|
||||
</form>
|
||||
</field>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await addRow(target, ".o_field_one2many");
|
||||
assert.containsOnce(target, ".modal");
|
||||
await click(target.querySelector(".modal-footer button"));
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_one2many tbody tr .o_data_cell")].map(
|
||||
(el) => el.innerText
|
||||
),
|
||||
["coucou", "has changed", "42"],
|
||||
"the onchange should create one new record and remove the existing"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_field_one2many .o_list_renderer tbody tr td"));
|
||||
|
||||
await clickSave(target.querySelector(".modal"));
|
||||
assert.verifySteps(["_fetchSpecialDataForMyWidget"], "should only fetch special data once");
|
||||
});
|
||||
|
||||
QUnit.test("one2many without inline tree arch", async function (assert) {
|
||||
serverData.models.partner.records[0].turtles = [2, 3];
|
||||
serverData.views = {
|
||||
@@ -10590,46 +10455,6 @@ QUnit.module("Fields", (hooks) => {
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"one2many: internal state is updated after another field changes",
|
||||
async function (assert) {
|
||||
// The FieldOne2Many is configured such that it is reset at any field change.
|
||||
// The MatrixProductConfigurator feature relies on that, and requires that its
|
||||
// internal state is correctly updated. This white-box test artificially checks that.
|
||||
let o2m;
|
||||
// keep using the legacy version considering the above comment
|
||||
const MyFieldOne2Many = FieldOne2Many.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
o2m = this;
|
||||
},
|
||||
});
|
||||
fieldRegistry.add("myone2many", MyFieldOne2Many);
|
||||
registerCleanup(() => {
|
||||
delete fieldRegistry.map.myone2many;
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field name="p" widget="myone2many">
|
||||
<tree><field name="display_name"/></tree>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(o2m.recordData.display_name, false);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=display_name] input", "val");
|
||||
|
||||
assert.strictEqual(o2m.recordData.display_name, "val");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("nested one2many, onchange, no command value", async function (assert) {
|
||||
// This test ensures that we always send all values to onchange rpcs for nested
|
||||
// one2manys, even if some field hasn't changed. In this particular test case,
|
||||
@@ -10695,84 +10520,6 @@ QUnit.module("Fields", (hooks) => {
|
||||
await click(target.querySelector(".o_data_row .o_field_boolean input"));
|
||||
});
|
||||
|
||||
QUnit.test("update a one2many from a custom field widget", async function (assert) {
|
||||
// In this test, we define a custom field widget to render/update a one2many
|
||||
// field. For the update part, we ensure that updating primitive fields of a sub
|
||||
// record works. There is no guarantee that updating a relational field on the sub
|
||||
// record would work. Deleting a sub record works as well. However, creating sub
|
||||
// records isn't supported. There are obviously a lot of limitations, but the code
|
||||
// hasn't been designed to support all this. This test simply encodes what can be
|
||||
// done, and this comment explains what can't (and won't be implemented in stable
|
||||
// versions).
|
||||
serverData.models.partner.records[0].p = [1, 2];
|
||||
const MyRelationalField = AbstractField.extend({
|
||||
events: {
|
||||
"click .update": "_onUpdate",
|
||||
"click .delete": "_onDelete",
|
||||
},
|
||||
async _render() {
|
||||
const records = await this._rpc({
|
||||
method: "read",
|
||||
model: "partner",
|
||||
args: [this.value.res_ids],
|
||||
});
|
||||
this.$el.text(records.map((r) => `${r.display_name}/${r.int_field}`).join(", "));
|
||||
this.$el.append($('<button class="update fa fa-edit">'));
|
||||
this.$el.append($('<button class="delete fa fa-trash">'));
|
||||
},
|
||||
_onUpdate() {
|
||||
this._setValue({
|
||||
operation: "UPDATE",
|
||||
id: this.value.data[0].id,
|
||||
data: {
|
||||
display_name: "new name",
|
||||
int_field: 44,
|
||||
},
|
||||
});
|
||||
},
|
||||
_onDelete() {
|
||||
this._setValue({
|
||||
operation: "DELETE",
|
||||
ids: [this.value.data[0].id],
|
||||
});
|
||||
},
|
||||
});
|
||||
fieldRegistry.add("my_relational_field", MyRelationalField);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
mode: "readonly",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" widget="my_relational_field"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=p]").innerText,
|
||||
"first record/10, second record/9"
|
||||
);
|
||||
|
||||
await click(target.querySelector("button.update"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=p]").innerText,
|
||||
"new name/44, second record/9"
|
||||
);
|
||||
|
||||
await click(target.querySelector("button.delete"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=p]").innerText,
|
||||
"second record/9"
|
||||
);
|
||||
|
||||
delete fieldRegistry.map.my_relational_field;
|
||||
});
|
||||
|
||||
QUnit.test("edition in list containing widget with decoration", async function (assert) {
|
||||
// We use here a badge widget and check its decoration is properly managed
|
||||
// in this scenario (we need a widget with specific decoration handling)
|
||||
|
||||
@@ -20,9 +20,6 @@ import {
|
||||
triggerEvent,
|
||||
triggerHotkey,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { FieldOne2Many } from "web.relational_fields";
|
||||
import { FieldChar as LegacyFieldChar } from "web.basic_fields";
|
||||
import * as legacyFieldRegistry from "web.field_registry";
|
||||
import { toggleActionMenu, toggleGroupByMenu, toggleMenuItem } from "@web/../tests/search/helpers";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
@@ -41,8 +38,6 @@ import { errorService } from "@web/core/errors/error_service";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
import { WarningDialog } from "@web/core/errors/error_dialogs";
|
||||
|
||||
import * as legacyCore from "web.core";
|
||||
|
||||
const fieldRegistry = registry.category("fields");
|
||||
const serviceRegistry = registry.category("services");
|
||||
const widgetRegistry = registry.category("view_widgets");
|
||||
@@ -8857,66 +8852,6 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(target.querySelector(".modal-title").textContent, "Translate: foo");
|
||||
});
|
||||
|
||||
QUnit.test("legacy field support translation dialog", async (assert) => {
|
||||
serverData.models.partner.fields.foo.translate = true;
|
||||
|
||||
patchWithCleanup(legacyCore._t.database, {
|
||||
multi_lang: true,
|
||||
parameters: {
|
||||
code: "CUST",
|
||||
},
|
||||
});
|
||||
|
||||
const myChar = LegacyFieldChar.extend({
|
||||
async _render() {
|
||||
// DIY because the compatibility layer doesn't support
|
||||
// legacy widget having multiple nodes in their this.$el.
|
||||
await this._super(...arguments);
|
||||
const div = document.createElement("div");
|
||||
const $div = $(div).append(this.$el);
|
||||
this.setElement($div);
|
||||
},
|
||||
});
|
||||
|
||||
legacyFieldRegistry.add("legacy_char", myChar);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="legacy_char"/>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
if (route === "/web/dataset/call_button") {
|
||||
return { context: {}, domain: [] };
|
||||
}
|
||||
if (args.method === "get_installed") {
|
||||
return [
|
||||
["CUST", "custom lang"],
|
||||
["CUST2", "second custom"],
|
||||
];
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
|
||||
return Promise.resolve([
|
||||
[
|
||||
{ lang: "CUST", source: "yop", value: "yop" },
|
||||
{ lang: "CUST2", source: "yop", value: "valeur français" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.verifySteps(["get_views", "read"]);
|
||||
await click(target, ".o_field_legacy_char .o_field_translate.btn-link");
|
||||
assert.verifySteps(["get_installed", "get_field_translations"]);
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.strictEqual(target.querySelector(".modal-title").textContent, "Translate: foo");
|
||||
});
|
||||
|
||||
QUnit.test("translation alerts are preserved on pager change", async function (assert) {
|
||||
serverData.models.partner.fields.foo.translate = true;
|
||||
|
||||
@@ -12285,133 +12220,6 @@ QUnit.module("Views", (hooks) => {
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("legacy one2many view mode", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [1];
|
||||
legacyFieldRegistry.add("legacy_one2many", FieldOne2Many);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" mode="tree,kanban" widget="legacy_one2many">
|
||||
<form><field name="display_name"/></form>
|
||||
<tree><field name="foo"/></tree>
|
||||
<kanban>
|
||||
<field name="display_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><t t-esc="record.display_name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsOnce(target, ".o_field_legacy_one2many .o_legacy_list_view");
|
||||
});
|
||||
|
||||
QUnit.test("legacy one2many view mode in small env", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [1];
|
||||
legacyFieldRegistry.add("legacy_one2many", FieldOne2Many);
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = { bus: new EventBus() };
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return ui;
|
||||
},
|
||||
};
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" mode="tree,kanban" widget="legacy_one2many">
|
||||
<form><field name="display_name"/></form>
|
||||
<tree><field name="foo"/></tree>
|
||||
<kanban>
|
||||
<field name="display_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><t t-esc="record.display_name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsOnce(target, ".o_field_legacy_one2many .o_legacy_kanban_view");
|
||||
});
|
||||
|
||||
QUnit.test("legacy one2many view mode in small env (2)", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [1];
|
||||
legacyFieldRegistry.add("legacy_one2many", FieldOne2Many);
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = { bus: new EventBus() };
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return ui;
|
||||
},
|
||||
};
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" widget="legacy_one2many">
|
||||
<form><field name="display_name"/></form>
|
||||
<tree><field name="foo"/></tree>
|
||||
<kanban>
|
||||
<field name="display_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><t t-esc="record.display_name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsOnce(target, ".o_field_legacy_one2many .o_legacy_kanban_view");
|
||||
});
|
||||
|
||||
QUnit.test("legacy one2many with no inlive view", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [1];
|
||||
serverData.views = {
|
||||
"partner,false,list": `<tree><field name="foo"/></tree>`,
|
||||
};
|
||||
legacyFieldRegistry.add("legacy_one2many", FieldOne2Many);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" mode="tree" class="o_my_legacy_class" widget="legacy_one2many"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsOnce(target, ".o_field_legacy_one2many .o_legacy_list_view");
|
||||
assert.containsOnce(target, ".o_field_widget.o_legacy_field_widget.o_my_legacy_class");
|
||||
});
|
||||
|
||||
QUnit.test("Action Button clicked with failing action", async function (assert) {
|
||||
const handler = (ev) => {
|
||||
assert.step("error");
|
||||
|
||||
@@ -38,10 +38,8 @@ import { KanbanAnimatedNumber } from "@web/views/kanban/kanban_animated_number";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { DynamicRecordList } from "@web/views/relational_model";
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
import AbstractField from "web.AbstractField";
|
||||
import legacyFieldRegistry from "web.field_registry";
|
||||
|
||||
const { Component, onWillStart, xml } = owl;
|
||||
const { Component, xml } = owl;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const viewWidgetRegistry = registry.category("view_widgets");
|
||||
@@ -8166,62 +8164,6 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(getCard(2).querySelector(".o_widget").innerText, '{"foo":"gnap"}');
|
||||
});
|
||||
|
||||
QUnit.test("subwidgets with on_attach_callback when changing record color", async (assert) => {
|
||||
// Note: since the OWL refactor the 'on_attach_callback' is only called
|
||||
// once since the card is not entirely re-rendered. Instead we check that
|
||||
// the data displayed in the widget field is correctly updated.
|
||||
let counter = 0;
|
||||
legacyFieldRegistry.add(
|
||||
"test_widget",
|
||||
AbstractField.extend({
|
||||
on_attach_callback: () => counter++,
|
||||
_renderReadonly() {
|
||||
this.el.innerText = this.record.data.color;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "category",
|
||||
serverData,
|
||||
arch:
|
||||
"<kanban>" +
|
||||
'<field name="color"/>' +
|
||||
"<templates>" +
|
||||
'<t t-name="kanban-box">' +
|
||||
'<div color="color">' +
|
||||
'<div class="o_dropdown_kanban dropdown">' +
|
||||
'<a class="dropdown-toggle o-no-caret btn" data-bs-toggle="dropdown" href="#">' +
|
||||
'<span class="fa fa-bars fa-lg"/>' +
|
||||
"</a>" +
|
||||
'<ul class="dropdown-menu" role="menu">' +
|
||||
"<li>" +
|
||||
'<ul class="oe_kanban_colorpicker"/>' +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"</div>" +
|
||||
'<field name="name" widget="test_widget"/>' +
|
||||
"</div>" +
|
||||
"</t>" +
|
||||
"</templates>" +
|
||||
"</kanban>",
|
||||
});
|
||||
|
||||
// counter should be 2 as there are 2 records
|
||||
assert.strictEqual(counter, 2, "on_attach_callback should have been called twice");
|
||||
assert.deepEqual(getCardTexts(), ["2", "5"]);
|
||||
|
||||
// set a color to kanban record
|
||||
await toggleRecordDropdown(0);
|
||||
await click(getCard(0), ".oe_kanban_colorpicker a.oe_kanban_color_9");
|
||||
|
||||
// first record has replaced its $el with a new one
|
||||
assert.hasClass(getCard(0), "oe_kanban_color_9");
|
||||
assert.deepEqual(getCardTexts(), ["9", "5"]);
|
||||
assert.strictEqual(counter, 2, "on_attach_callback should have been called twice");
|
||||
});
|
||||
|
||||
QUnit.test("column progressbars properly work", async (assert) => {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
@@ -9408,201 +9350,6 @@ QUnit.module("Views", (hooks) => {
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("asynchronous rendering of a field widget (ungrouped)", async (assert) => {
|
||||
let fooFieldDef = makeDeferred();
|
||||
legacyFieldRegistry.add(
|
||||
"asyncwidget",
|
||||
AbstractField.extend({
|
||||
async willStart() {
|
||||
await Promise.all([this._super(...arguments), fooFieldDef]);
|
||||
},
|
||||
async start() {
|
||||
this.el.innerText = "LOADED";
|
||||
return this._super(...arguments);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const makeViewProm = makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo" widget="asyncwidget"/></div>' +
|
||||
"</t></templates></kanban>",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".o_kanban_record", "kanban view is not ready yet");
|
||||
|
||||
fooFieldDef.resolve();
|
||||
const kanban = await makeViewProm;
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
|
||||
// reload with a domain
|
||||
fooFieldDef = makeDeferred();
|
||||
await reload(kanban, { domain: [["id", "=", 1]] });
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
|
||||
fooFieldDef.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED"]);
|
||||
});
|
||||
|
||||
QUnit.test("asynchronous rendering of a field widget (grouped)", async (assert) => {
|
||||
let fooFieldDef = makeDeferred();
|
||||
legacyFieldRegistry.add(
|
||||
"asyncwidget",
|
||||
AbstractField.extend({
|
||||
async willStart() {
|
||||
await Promise.all([this._super(...arguments), fooFieldDef]);
|
||||
},
|
||||
async start() {
|
||||
this.el.innerText = "LOADED";
|
||||
return this._super(...arguments);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const makeViewProm = makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo" widget="asyncwidget"/></div>' +
|
||||
"</t></templates></kanban>",
|
||||
groupBy: ["foo"],
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".o_kanban_record", "kanban view is not ready yet");
|
||||
|
||||
fooFieldDef.resolve();
|
||||
const kanban = await makeViewProm;
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
|
||||
// reload with a domain
|
||||
fooFieldDef = makeDeferred();
|
||||
await reload(kanban, { domain: [["id", "=", 1]] });
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
|
||||
fooFieldDef.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED"]);
|
||||
});
|
||||
|
||||
QUnit.test("asynchronous rendering of a field widget with display attr", async (assert) => {
|
||||
const fooFieldDef = makeDeferred();
|
||||
const CharField = legacyFieldRegistry.get("char");
|
||||
legacyFieldRegistry.add(
|
||||
"asyncwidget",
|
||||
CharField.extend({
|
||||
async willStart() {
|
||||
await Promise.all([this._super(...arguments), fooFieldDef]);
|
||||
},
|
||||
async start() {
|
||||
this.el.innerText = "LOADED";
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const makeViewProm = makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo" display="right" widget="asyncwidget"/></div>' +
|
||||
"</t></templates></kanban>",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".o_kanban_record", "kanban view is not ready yet");
|
||||
|
||||
fooFieldDef.resolve();
|
||||
await makeViewProm;
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
assert.hasClass(getCard(0).querySelector(".o_field_asyncwidget"), "float-end");
|
||||
});
|
||||
|
||||
QUnit.test("asynchronous rendering of a widget", async (assert) => {
|
||||
const widgetDef = makeDeferred();
|
||||
class AsyncWidget extends Component {
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
await widgetDef;
|
||||
});
|
||||
}
|
||||
}
|
||||
AsyncWidget.template = xml`<div>LOADED</div>`;
|
||||
viewWidgetRegistry.add("asyncwidget", AsyncWidget);
|
||||
|
||||
const makeViewProm = makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div><widget name="asyncwidget"/></div>' +
|
||||
"</t></templates></kanban>",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".o_kanban_record", "kanban view is not ready yet");
|
||||
|
||||
widgetDef.resolve();
|
||||
await makeViewProm;
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
});
|
||||
|
||||
QUnit.test("update kanban with asynchronous field widget", async (assert) => {
|
||||
const fooFieldDef = makeDeferred();
|
||||
legacyFieldRegistry.add(
|
||||
"asyncwidget",
|
||||
AbstractField.extend({
|
||||
async willStart() {
|
||||
await Promise.all([this._super(...arguments), fooFieldDef]);
|
||||
},
|
||||
async start() {
|
||||
this.el.innerText = "LOADED";
|
||||
return this._super(...arguments);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const kanban = await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo" widget="asyncwidget"/></div>' +
|
||||
"</t></templates></kanban>",
|
||||
domain: [["id", "=", "0"]], // no record matches this domain
|
||||
});
|
||||
|
||||
assert.containsNone(target, ".o_kanban_record:not(.o_kanban_ghost)");
|
||||
|
||||
reload(kanban, { domain: [] }); // this rendering will be async
|
||||
|
||||
assert.containsNone(target, ".o_kanban_record:not(.o_kanban_ghost)");
|
||||
|
||||
fooFieldDef.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(getCardTexts(), ["LOADED", "LOADED", "LOADED", "LOADED"]);
|
||||
});
|
||||
|
||||
QUnit.test("set cover image", async (assert) => {
|
||||
assert.expect(10);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user