[FIX] web: relationalModel: basic keys supported in record context

Have an editable list view, displaying a many2many tags field. The m2m field
has a context, mentioning `active_id`. (`context="{'default_field': active_id}"`)

"basic context keys" is to be understood as the usual suspects: active_id, active_ids, current_company_id, active_model

Type something in the m2m input, and selet create and edit, in order to create a new record in a dialog form view.

Before this commit, there was a crash because active_id was not present in the evaluation context.

After this commit, the new record opens in its form view, with the right value for the field that must have a default.

closes odoo/odoo#96910

Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
Lucas Perais
2022-08-04 18:35:32 +02:00
parent b3bc5a0a03
commit 6c155e1713
8 changed files with 219 additions and 16 deletions
@@ -36,7 +36,7 @@ QUnit.module('Views', function (hooks) {
}
}
setupControlPanelServiceRegistry();
serviceRegistry.add("company", companyService);
serviceRegistry.add("company", companyService, { force: true });
serviceRegistry.add("dialog", dialogService);
});
@@ -457,6 +457,10 @@ export class Record extends DataPoint {
return {
// ...
...this.dataContext,
active_id: this.resId || false,
active_ids: this.resId ? [this.resId] : [],
active_model: this.resModel,
current_company_id: this.model.company.currentCompany.id,
};
}
@@ -3199,12 +3203,13 @@ export class StaticList extends DataPoint {
StaticList.DEFAULT_LIMIT = 40;
export class RelationalModel extends Model {
setup(params, { action, dialog, notification, rpc, user, view }) {
setup(params, { action, dialog, notification, rpc, user, view, company }) {
this.action = action;
this.dialogService = dialog;
this.notificationService = notification;
this.rpc = rpc;
this.user = user;
this.company = company;
this.viewService = view;
this.orm = new RequestBatcherORM(rpc, user);
this.keepLast = new KeepLast();
@@ -3342,7 +3347,7 @@ export class RelationalModel extends Model {
}
}
RelationalModel.services = ["action", "dialog", "notification", "rpc", "user", "view"];
RelationalModel.services = ["action", "dialog", "notification", "rpc", "user", "view", "company"];
RelationalModel.Record = Record;
RelationalModel.Group = Group;
RelationalModel.DynamicRecordList = DynamicRecordList;
@@ -12,6 +12,7 @@ import { ActionDialog } from "@web/webclient/actions/action_dialog";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { makeTestEnv, utils } from "../../helpers/mock_env";
import {
fakeCompanyService,
fakeCommandService,
makeFakeDialogService,
makeFakeLocalizationService,
@@ -296,6 +297,7 @@ QUnit.module("DebugMenu", (hooks) => {
}
};
prepareRegistriesWithCleanup();
registry.category("services").add("company", fakeCompanyService);
patchWithCleanup(odoo, {
debug: true,
@@ -387,6 +389,7 @@ QUnit.module("DebugMenu", (hooks) => {
}
};
prepareRegistriesWithCleanup();
registry.category("services").add("company", fakeCompanyService);
patchWithCleanup(odoo, {
debug: true,
@@ -10,7 +10,6 @@ import { effectService } from "@web/core/effects/effect_service";
import { objectToUrlEncodedString } from "@web/core/utils/urls";
import { registerCleanup } from "./cleanup";
import { patchWithCleanup } from "./utils";
import { companyService } from "@web/webclient/company_service";
import { uiService } from "@web/core/ui/ui_service";
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
@@ -70,7 +69,8 @@ export function makeFakeRPCService(mockRPC) {
.then(resolve)
.catch(reject);
});
rpcProm.abort = () => rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
rpcProm.abort = () =>
rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
return rpcProm;
};
},
@@ -79,7 +79,7 @@ export function makeFakeRPCService(mockRPC) {
}
export function makeMockXHR(response, sendCb, def) {
let MockXHR = function () {
const MockXHR = function () {
return {
_loadListener: null,
url: "",
@@ -269,6 +269,17 @@ export function makeFakeUserService(hasGroup = () => false) {
};
}
export const fakeCompanyService = {
start() {
return {
availableCompanies: {},
allowedCompanyIds: [],
currentCompany: {},
setCompanies: () => {},
};
},
};
export function makeFakeHTTPService(getResponse, postResponse) {
getResponse =
getResponse ||
@@ -295,7 +306,7 @@ export function makeFakeHTTPService(getResponse, postResponse) {
}
export const mocks = {
company: () => companyService,
company: () => fakeCompanyService,
command: () => fakeCommandService,
cookie: () => fakeCookieService,
effect: () => effectService, // BOI The real service ? Is this what we want ?
@@ -16,6 +16,9 @@ import {
import { editSearch, validateSearch } from "@web/../tests/search/helpers";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { session } from "@web/session";
import { companyService } from "@web/webclient/company_service";
import { registry } from "@web/core/registry";
let target;
let serverData;
@@ -1869,4 +1872,184 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, ".modal");
assert.strictEqual(target.querySelector(".modal-title").textContent, "Search: pokemon");
});
QUnit.test("many2many basic keys in field evalcontext -- in list", async (assert) => {
assert.expect(6);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
patchWithCleanup(session, {
user_companies: {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1 },
2: { id: 2, name: "Herman's", sequence: 2 },
1: { id: 1, name: "Heroes TM", sequence: 3 },
},
current_company: 3,
},
});
registry.category("services").add("company", companyService, { force: true });
patchWithCleanup(AutoComplete, {
timeout: 0,
});
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
</tree>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
});
await click(target.querySelector(".o_data_cell"));
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
);
});
QUnit.test("many2many basic keys in field evalcontext -- in form", async (assert) => {
assert.expect(6);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
patchWithCleanup(session, {
user_companies: {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1 },
2: { id: 2, name: "Herman's", sequence: 2 },
1: { id: 1, name: "Heroes TM", sequence: 3 },
},
current_company: 3,
},
});
registry.category("services").add("company", companyService, { force: true });
patchWithCleanup(AutoComplete, {
timeout: 0,
});
await makeView({
type: "form",
resId: 1,
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
});
await clickEdit(target);
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
);
});
QUnit.test(
"many2many basic keys in field evalcontext -- in a x2many in form",
async (assert) => {
assert.expect(6);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
const rec = serverData.models.partner.records.find(({ id }) => id === 2);
rec.p = [1];
patchWithCleanup(session, {
user_companies: {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1 },
2: { id: 2, name: "Herman's", sequence: 2 },
1: { id: 1, name: "Heroes TM", sequence: 3 },
},
current_company: 3,
},
});
registry.category("services").add("company", companyService, { force: true });
patchWithCleanup(AutoComplete, {
timeout: 0,
});
await makeView({
type: "form",
resId: 2,
resModel: "partner",
serverData,
arch: `
<form>
<field name="p">
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
});
await clickEdit(target);
await click(target, ".o_data_cell");
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
);
}
);
});
+2
View File
@@ -11,6 +11,7 @@ import {
} from "../search/helpers";
import { addLegacyMockEnvironment } from "../webclient/helpers";
import {
fakeCompanyService,
makeFakeLocalizationService,
makeFakeRouterService,
makeFakeUserService,
@@ -105,4 +106,5 @@ export function setupViewRegistries() {
serviceRegistry.add("localization", makeFakeLocalizationService()), { force: true };
serviceRegistry.add("dialog", dialogService), { force: true };
serviceRegistry.add("popover", popoverService), { force: true };
serviceRegistry.add("company", fakeCompanyService);
}
@@ -12,10 +12,8 @@ import {
triggerEvent,
mockTimeout,
} from "@web/../tests/helpers/utils";
import { makeView } from "@web/../tests/views/helpers";
import { dialogService } from "@web/core/dialog/dialog_service";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
import { makeFakeUserService } from "../../helpers/mock_services";
const serviceRegistry = registry.category("services");
@@ -97,13 +95,12 @@ QUnit.module("ViewDialogs", (hooks) => {
},
};
target = getFixture();
setupControlPanelServiceRegistry();
setupViewRegistries();
function hasGroup(group) {
return group === "base.group_allow_export";
}
serviceRegistry.add("user", makeFakeUserService(hasGroup), { force: true });
serviceRegistry.add("dialog", dialogService);
fetchedFields = {
root: [
@@ -595,7 +592,7 @@ QUnit.module("ViewDialogs", (hooks) => {
QUnit.test("Export dialog: display on small screen after resize", async function (assert) {
const { execRegisteredTimeouts } = mockTimeout();
let ui = {
const ui = {
activateElement: () => {},
deactivateElement: () => {},
size: 4,
+5 -3
View File
@@ -33,6 +33,7 @@ import { registerCleanup } from "../helpers/cleanup";
import { makeTestEnv } from "../helpers/mock_env";
import {
fakeTitleService,
fakeCompanyService,
makeFakeLocalizationService,
makeFakeRouterService,
makeFakeHTTPService,
@@ -74,7 +75,7 @@ export function setupWebClientRegistries() {
options: { sequence: 0 },
},
};
for (let [key, { value, options }] of Object.entries(favoriveMenuItems)) {
for (const [key, { value, options }] of Object.entries(favoriveMenuItems)) {
if (!favoriteMenuRegistry.contains(key)) {
favoriteMenuRegistry.add(key, value, options);
}
@@ -97,8 +98,9 @@ export function setupWebClientRegistries() {
ui: () => uiService,
user: () => makeFakeUserService(),
view: () => viewService,
company: () => fakeCompanyService,
};
for (let serviceName in services) {
for (const serviceName in services) {
if (!serviceRegistry.contains(serviceName)) {
serviceRegistry.add(serviceName, services[serviceName]());
}
@@ -195,7 +197,7 @@ export async function addLegacyMockEnvironment(env, legacyParams = {}) {
serviceRegistry.add("legacy_action_manager", legacyActionManagerService);
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv));
// deploy wowl services into the legacy env.
const wowlToLegacyServiceMappers = registry.category('wowlToLegacyServiceMappers').getEntries();
const wowlToLegacyServiceMappers = registry.category("wowlToLegacyServiceMappers").getEntries();
for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) {
serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv));
}