[IMP] web: new field service
We move the function loadFields from the view service to a newly created field service. That service can also be used to load model fields for a given path (loadPath), i.e. load all model fields for the models traversed by the given path. We use loadPath in useModelField and DomainSelector. closes odoo/odoo#117882 Related: odoo/enterprise#39418 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import * as testUtils from "@web/../tests/helpers/utils";
|
||||
import weTestUtils from "web_editor.test_utils";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let fixture;
|
||||
let serverData;
|
||||
@@ -107,6 +109,7 @@ QUnit.module('favorite filter widget', (hooks) => {
|
||||
});
|
||||
serverData = { models };
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("field", fieldService);
|
||||
});
|
||||
|
||||
QUnit.test('create favorite filter', async (assert) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class DomainSelector extends Component {
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.view = useService("view");
|
||||
this.fieldService = useService("field");
|
||||
this.treeBuilder = new DomainTreeBuilder();
|
||||
this.tree = useState({
|
||||
isSupported: false,
|
||||
@@ -107,16 +107,16 @@ export class DomainSelector extends Component {
|
||||
if ("01".includes(field.toString())) {
|
||||
return { type: "integer" };
|
||||
}
|
||||
|
||||
const names = field.length ? field.split(".") : [];
|
||||
let currentModel = resModel;
|
||||
let fieldDef = null;
|
||||
for (const name of names) {
|
||||
const model = await this.view.loadFields(currentModel);
|
||||
fieldDef = model[name];
|
||||
currentModel = fieldDef.relation;
|
||||
if (typeof field !== "string" || !field) {
|
||||
return null;
|
||||
}
|
||||
return fieldDef;
|
||||
const { isInvalid, names, modelsInfo } = await this.fieldService.loadPath(resModel, field);
|
||||
if (isInvalid) {
|
||||
return null;
|
||||
}
|
||||
const name = names.at(-1);
|
||||
const { fieldDefs } = modelsInfo.at(-1);
|
||||
return fieldDefs[name];
|
||||
}
|
||||
|
||||
createNewLeaf() {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Cache } from "@web/core/utils/cache";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/**
|
||||
* @typedef {Object} LoadFieldsOptions
|
||||
* @property {string[]|false} [fieldNames]
|
||||
* @property {string[]} [attributes]
|
||||
*/
|
||||
|
||||
export const fieldService = {
|
||||
dependencies: ["orm"],
|
||||
start(env, { orm }) {
|
||||
const cache = new Cache(
|
||||
(resModel, options) => {
|
||||
return orm
|
||||
.call(resModel, "fields_get", [options.fieldNames, options.attributes])
|
||||
.catch((error) => {
|
||||
cache.clear(resModel, options);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
(resModel, options) =>
|
||||
JSON.stringify([resModel, options.fieldNames, options.attributes])
|
||||
);
|
||||
|
||||
env.bus.addEventListener("CLEAR-CACHES", () => cache.invalidate());
|
||||
|
||||
/**
|
||||
* @param {string} resModel
|
||||
* @param {LoadFieldsOptions} [options]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async function loadFields(resModel, options = {}) {
|
||||
if (typeof resModel !== "string" || !resModel) {
|
||||
throw new Error(`Invalid model name: ${resModel}`);
|
||||
}
|
||||
return cache.read(resModel, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} resModel valid model name
|
||||
* @param {Object} fieldDefs
|
||||
* @param {string[]} names
|
||||
*/
|
||||
async function _loadPath(resModel, fieldDefs, names) {
|
||||
const [name, ...remainingNames] = names;
|
||||
const modelsInfo = [{ resModel, fieldDefs }];
|
||||
|
||||
const fieldDef = fieldDefs[name];
|
||||
if (name !== "*" && !fieldDef) {
|
||||
return { isInvalid: "path", names, modelsInfo };
|
||||
}
|
||||
|
||||
if (!remainingNames.length) {
|
||||
return { names, modelsInfo };
|
||||
}
|
||||
|
||||
if (name === "*") {
|
||||
return { isInvalid: "path", names, modelsInfo };
|
||||
}
|
||||
|
||||
if (fieldDef.relation) {
|
||||
const subResult = await _loadPath(
|
||||
fieldDef.relation,
|
||||
await loadFields(fieldDef.relation),
|
||||
remainingNames
|
||||
);
|
||||
const result = {
|
||||
names,
|
||||
modelsInfo: [...modelsInfo, ...subResult.modelsInfo],
|
||||
};
|
||||
if (subResult.isInvalid) {
|
||||
result.isInvalid = "path";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return { isInvalid: "path", names, modelsInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: the symbol * can be used at the end of path (e.g path="*" or path="user_id.*").
|
||||
* It says to load the fields of the appropriate model.
|
||||
* @param {string} resModel
|
||||
* @param {string} path
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function loadPath(resModel, path = "*") {
|
||||
const fieldDefs = await loadFields(resModel);
|
||||
if (typeof path !== "string" || !path) {
|
||||
throw new Error(`Invalid path: ${path}`);
|
||||
}
|
||||
return _loadPath(resModel, fieldDefs, path.split("."));
|
||||
}
|
||||
|
||||
return { loadFields, loadPath };
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("field", fieldService);
|
||||
@@ -1,33 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { zipWith } from "@web/core/utils/arrays";
|
||||
|
||||
export function useModelField() {
|
||||
const view = useService("view");
|
||||
const fieldService = useService("field");
|
||||
|
||||
const loadModelFields = (resModel) => {
|
||||
return view.loadFields(resModel);
|
||||
return fieldService.loadFields(resModel);
|
||||
};
|
||||
|
||||
const loadChain = async (resModel, fieldName) => {
|
||||
const fieldNameChain = fieldName.length ? fieldName.split(".") : [];
|
||||
let currentNode = {
|
||||
resModel,
|
||||
field: null,
|
||||
};
|
||||
const chain = [currentNode];
|
||||
for (const fieldName of fieldNameChain) {
|
||||
const fieldsInfo = await loadModelFields(currentNode.resModel);
|
||||
Object.assign(currentNode, {
|
||||
field: { ...fieldsInfo[fieldName], name: fieldName },
|
||||
});
|
||||
if (fieldsInfo[fieldName].relation) {
|
||||
currentNode = {
|
||||
resModel: fieldsInfo[fieldName].relation,
|
||||
field: null,
|
||||
};
|
||||
chain.push(currentNode);
|
||||
}
|
||||
const loadChain = async (resModel, path) => {
|
||||
if ("01".includes(path.toString())) {
|
||||
return [{ resModel, field: null }];
|
||||
}
|
||||
if (typeof path !== "string" || !path) {
|
||||
return [{ resModel, field: null }];
|
||||
}
|
||||
const { isInvalid, names, modelsInfo } = await fieldService.loadPath(resModel, path);
|
||||
if (isInvalid) {
|
||||
return [{ resModel, field: null }];
|
||||
}
|
||||
const chain = zipWith(names, modelsInfo, (name, { resModel, fieldDefs }) => {
|
||||
return { resModel, field: fieldDefs[name] };
|
||||
});
|
||||
const lastField = chain.at(-1)?.field;
|
||||
if (lastField.relation) {
|
||||
chain.push({ resModel: lastField.relation, field: null });
|
||||
}
|
||||
return chain;
|
||||
};
|
||||
|
||||
@@ -41,12 +41,6 @@ import { UPDATE_METHODS } from "@web/core/orm_service";
|
||||
* @property {boolean} loadIrFilters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} LoadFieldsOptions
|
||||
* @property {string[] | false} [fieldNames]
|
||||
* @property {string[]} [attributes]
|
||||
*/
|
||||
|
||||
export const viewService = {
|
||||
dependencies: ["orm"],
|
||||
start(env, { orm }) {
|
||||
@@ -69,31 +63,6 @@ export const viewService = {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads fields information
|
||||
*
|
||||
* @param {string} resModel
|
||||
* @param {LoadFieldsOptions} [options]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async function loadFields(resModel, options = {}) {
|
||||
const key = JSON.stringify([
|
||||
"fields",
|
||||
resModel,
|
||||
options.fieldNames,
|
||||
options.attributes,
|
||||
]);
|
||||
if (!cache[key]) {
|
||||
cache[key] = orm
|
||||
.call(resModel, "fields_get", [options.fieldNames, options.attributes])
|
||||
.catch((error) => {
|
||||
delete cache[key];
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads various information concerning views: fields_view for each view,
|
||||
* fields of the corresponding model, and optionally the filters.
|
||||
@@ -128,10 +97,6 @@ export const viewService = {
|
||||
relatedModels: models,
|
||||
views: {},
|
||||
};
|
||||
for (const [resModel, fields] of Object.entries(modelsCopy)) {
|
||||
const key = JSON.stringify(["fields", resModel, undefined, undefined]);
|
||||
cache[key] = Promise.resolve(fields);
|
||||
}
|
||||
for (const viewType in views) {
|
||||
const { arch, toolbar, id, filters, custom_view_id } = views[viewType];
|
||||
const viewDescription = { arch, id, custom_view_id };
|
||||
@@ -152,7 +117,7 @@ export const viewService = {
|
||||
}
|
||||
return cache[key];
|
||||
}
|
||||
return { loadViews, loadFields };
|
||||
return { loadViews };
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { DomainSelector } from "@web/core/domain_selector/domain_selector";
|
||||
import { OPERATOR_DESCRIPTIONS } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
@@ -64,7 +64,7 @@ QUnit.module("Components", (hooks) => {
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
registry.category("services").add("view", viewService);
|
||||
registry.category("services").add("field", fieldService);
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
function getModelInfo(resModel) {
|
||||
return {
|
||||
resModel,
|
||||
fieldDefs: serverData.models[resModel].fields,
|
||||
};
|
||||
}
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Field Service", {
|
||||
async beforeEach() {
|
||||
serverData = {
|
||||
models: {
|
||||
tortoise: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
name: { string: "Name", type: "char", default: "name" },
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
age: { type: "integer", string: "Age" },
|
||||
location_id: { type: "many2one", string: "Location", relation: "location" },
|
||||
},
|
||||
},
|
||||
location: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
name: { string: "Name", type: "char", default: "name" },
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
tortoise_ids: { type: "one2many", string: "Turtles", relation: "tortoise" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("field", fieldService);
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("loadPath", async (assert) => {
|
||||
const toTest = [
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "*",
|
||||
expectedResult: {
|
||||
names: ["*"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "*.a",
|
||||
expectedResult: {
|
||||
isInvalid: "path",
|
||||
names: ["*", "a"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.*",
|
||||
expectedResult: {
|
||||
names: ["location_id", "*"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getModelInfo("location")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "age",
|
||||
expectedResult: {
|
||||
names: ["age"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id",
|
||||
expectedResult: {
|
||||
names: ["location_id"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.tortoise_ids",
|
||||
expectedResult: {
|
||||
names: ["location_id", "tortoise_ids"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getModelInfo("location")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.tortoise_ids.age",
|
||||
expectedResult: {
|
||||
names: ["location_id", "tortoise_ids", "age"],
|
||||
modelsInfo: [
|
||||
getModelInfo("tortoise"),
|
||||
getModelInfo("location"),
|
||||
getModelInfo("tortoise"),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.tortoise_ids.age",
|
||||
expectedResult: {
|
||||
names: ["location_id", "tortoise_ids", "age"],
|
||||
modelsInfo: [
|
||||
getModelInfo("tortoise"),
|
||||
getModelInfo("location"),
|
||||
getModelInfo("tortoise"),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const env = await makeTestEnv({ serverData });
|
||||
|
||||
for (const { resModel, path, expectedResult } of toTest) {
|
||||
const result = await env.services.field.loadPath(resModel, path);
|
||||
assert.deepEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
const errorToTest = [
|
||||
{ resModel: "notAModel" },
|
||||
{ resModel: "tortoise", path: {} },
|
||||
{ resModel: "tortoise", path: "" },
|
||||
];
|
||||
|
||||
for (const { resModel, path } of errorToTest) {
|
||||
try {
|
||||
await env.services.field.loadPath(resModel, path);
|
||||
} catch {
|
||||
assert.step("error");
|
||||
}
|
||||
}
|
||||
assert.verifySteps(errorToTest.map(() => "error"));
|
||||
});
|
||||
|
||||
QUnit.test("store loadFields calls in cache in success", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
};
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
|
||||
await env.services.field.loadFields("tortoise");
|
||||
await env.services.field.loadFields("tortoise");
|
||||
|
||||
assert.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("does not store loadFields calls in cache when failed", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
return Promise.reject("my little error");
|
||||
}
|
||||
};
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
|
||||
try {
|
||||
await env.services.field.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
try {
|
||||
await env.services.field.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
|
||||
assert.verifySteps(["fields_get", "fields_get"]);
|
||||
});
|
||||
@@ -6,12 +6,12 @@ import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, triggerEvent, mount, editInput } from "../helpers/utils";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
@@ -60,7 +60,7 @@ QUnit.module("Components", (hooks) => {
|
||||
registry.category("services").add("orm", ormService);
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("view", viewService);
|
||||
registry.category("services").add("field", fieldService);
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
@@ -91,6 +93,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("field", fieldService);
|
||||
});
|
||||
|
||||
QUnit.module("DomainField");
|
||||
@@ -815,7 +818,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.verifySteps(["/web/webclient/load_menus"]);
|
||||
|
||||
await doAction(webClient, 1);
|
||||
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count"]);
|
||||
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
@@ -133,10 +135,10 @@ QUnit.module("Fields", (hooks) => {
|
||||
id: 3,
|
||||
display_name: "third partner",
|
||||
properties: [
|
||||
{name: "property_1", type: "char"},
|
||||
{name: "property_2", type: "char", definition_deleted: true},
|
||||
{name: "property_3", type: "char", definition_changed: true},
|
||||
{name: "property_4", type: "char"},
|
||||
{ name: "property_1", type: "char" },
|
||||
{ name: "property_2", type: "char", definition_deleted: true },
|
||||
{ name: "property_3", type: "char", definition_changed: true },
|
||||
{ name: "property_4", type: "char" },
|
||||
],
|
||||
company_id: 37,
|
||||
},
|
||||
@@ -144,8 +146,8 @@ QUnit.module("Fields", (hooks) => {
|
||||
id: 4,
|
||||
display_name: "fourth partner",
|
||||
properties: [
|
||||
{name: "property_2", type: "char", definition_deleted: true},
|
||||
{name: "property_3", type: "char", definition_deleted: true},
|
||||
{ name: "property_2", type: "char", definition_deleted: true },
|
||||
{ name: "property_3", type: "char", definition_deleted: true },
|
||||
],
|
||||
company_id: 37,
|
||||
},
|
||||
@@ -220,6 +222,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("field", fieldService);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
@@ -1308,18 +1311,20 @@ QUnit.module("Fields", (hooks) => {
|
||||
* Check the behavior of the domain (properies with "definition_deleted" should be ignored).
|
||||
* In that case, some properties start without the flag "definition_deleted".
|
||||
*/
|
||||
QUnit.test("properties: form view and falsy domain, properties are not empty", async function (assert) {
|
||||
async function mockRPC(route, { method, model, kwargs }) {
|
||||
if (method === "check_access_rights") {
|
||||
return true;
|
||||
QUnit.test(
|
||||
"properties: form view and falsy domain, properties are not empty",
|
||||
async function (assert) {
|
||||
async function mockRPC(route, { method, model, kwargs }) {
|
||||
if (method === "check_access_rights") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
serverData,
|
||||
arch: `
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
@@ -1332,45 +1337,48 @@ QUnit.module("Fields", (hooks) => {
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC,
|
||||
});
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
mockRPC,
|
||||
});
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
|
||||
// delete a property, 2 properties left
|
||||
await click(target, ".o_property_field:first-child .o_field_property_open_popover");
|
||||
await click(target, ".o_field_property_definition_delete");
|
||||
await click(target, ".modal-content .btn-primary");
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
// delete a property, 2 properties left
|
||||
await click(target, ".o_property_field:first-child .o_field_property_open_popover");
|
||||
await click(target, ".o_field_property_definition_delete");
|
||||
await click(target, ".modal-content .btn-primary");
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
|
||||
// delete a property, 1 property left
|
||||
await click(target, ".o_property_field:first-child .o_field_property_open_popover");
|
||||
await click(target, ".o_field_property_definition_delete");
|
||||
await click(target, ".modal-content .btn-primary");
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
// delete a property, 1 property left
|
||||
await click(target, ".o_property_field:first-child .o_field_property_open_popover");
|
||||
await click(target, ".o_field_property_definition_delete");
|
||||
await click(target, ".modal-content .btn-primary");
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
|
||||
// delete a property, no property left
|
||||
await click(target, ".o_property_field:first-child .o_field_property_open_popover");
|
||||
await click(target, ".o_field_property_definition_delete");
|
||||
await click(target, ".modal-content .btn-primary");
|
||||
assert.notOk(target.querySelector(".o_test_properties_not_empty"));
|
||||
});
|
||||
// delete a property, no property left
|
||||
await click(target, ".o_property_field:first-child .o_field_property_open_popover");
|
||||
await click(target, ".o_field_property_definition_delete");
|
||||
await click(target, ".modal-content .btn-primary");
|
||||
assert.notOk(target.querySelector(".o_test_properties_not_empty"));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Check the behavior of the domain (properties with "definition_deleted" should be ignored).
|
||||
* In that case, all properties start with the flag "definition_deleted".
|
||||
*/
|
||||
QUnit.test("properties: form view and falsy domain, properties are empty", async function (assert) {
|
||||
async function mockRPC(route, { method, model, kwargs }) {
|
||||
if (method === "check_access_rights") {
|
||||
return true;
|
||||
QUnit.test(
|
||||
"properties: form view and falsy domain, properties are empty",
|
||||
async function (assert) {
|
||||
async function mockRPC(route, { method, model, kwargs }) {
|
||||
if (method === "check_access_rights") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: `
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
@@ -1383,12 +1391,13 @@ QUnit.module("Fields", (hooks) => {
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC,
|
||||
});
|
||||
assert.notOk(target.querySelector(".o_test_properties_not_empty"));
|
||||
mockRPC,
|
||||
});
|
||||
assert.notOk(target.querySelector(".o_test_properties_not_empty"));
|
||||
|
||||
// create the first property
|
||||
await click(target, ".o_field_property_add button");
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
});
|
||||
// create the first property
|
||||
await click(target, ".o_field_property_add button");
|
||||
assert.ok(target.querySelector(".o_test_properties_not_empty"));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,8 @@ QUnit.module("View service", (hooks) => {
|
||||
records: [],
|
||||
},
|
||||
"ir.ui.view": {
|
||||
fields: {}, records: []
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -142,77 +143,4 @@ QUnit.module("View service", (hooks) => {
|
||||
await loadView();
|
||||
assert.verifySteps([]); // cache was not invalidated
|
||||
});
|
||||
|
||||
QUnit.test("loadViews stores fields in cache", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("get_views")) {
|
||||
assert.step("get_views");
|
||||
}
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
await env.services.views.loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 1 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
await env.services.views.loadFields("take.five");
|
||||
|
||||
assert.verifySteps(["get_views"]);
|
||||
});
|
||||
|
||||
QUnit.test("store loadFields calls in cache in success", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
await env.services.views.loadFields("take.five");
|
||||
await env.services.views.loadFields("take.five");
|
||||
|
||||
assert.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("store loadFields calls in cache when failed", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
return Promise.reject("my little error");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
try {
|
||||
await env.services.views.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
try {
|
||||
await env.services.views.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
|
||||
assert.verifySteps(["fields_get", "fields_get"]);
|
||||
});
|
||||
});
|
||||
|
||||
+2
@@ -5,6 +5,7 @@ declare module "services" {
|
||||
import { cookieService } from "@web/core/browser/cookie_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { effectService } from "@web/core/effects/effect_service";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { httpService } from "@web/core/network/http_service";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
@@ -25,6 +26,7 @@ declare module "services" {
|
||||
cookie: ReturnType<typeof cookieService.start>;
|
||||
dialog: ReturnType<typeof dialogService.start>;
|
||||
effect: ReturnType<typeof effectService.start>;
|
||||
field: ReturnType<typeof fieldService.start>;
|
||||
hotkey: ReturnType<typeof hotkeyService.start>;
|
||||
http: ReturnType<typeof httpService.start>;
|
||||
menu: Awaited<ReturnType<typeof menuService.start>>;
|
||||
|
||||
Reference in New Issue
Block a user