[IMP] web: name service
We define a new service "name". That service makes possible to load in batch display names and maintains a cache. Some known display names (fetched otherwise) can be added to the cache. The cache is cleared at least each time the UI is updated via _updateUI (action service). Part-of: odoo/odoo#124090 Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Lucas Lefevre <lul@odoo.com>
This commit is contained in:
co-authored by
Lucas Lefevre
parent
c8bd18ff80
commit
f2edbb27f4
@@ -0,0 +1,101 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { unique, zip } from "@web/core/utils/arrays";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
|
||||
export const ERROR_INACCESSIBLE_OR_MISSING = Symbol("INACCESSIBLE OR MISSING RECORD ID");
|
||||
|
||||
function isId(val) {
|
||||
return Number.isInteger(val) && val >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, (string|ERROR_INACCESSIBLE_OR_MISSING)>} DisplayNames
|
||||
*/
|
||||
|
||||
export const nameService = {
|
||||
dependencies: ["orm"],
|
||||
async: ["loadDisplayNames"],
|
||||
start(env, { orm }) {
|
||||
let cache = {};
|
||||
const batches = {};
|
||||
|
||||
function clearCache() {
|
||||
cache = {};
|
||||
}
|
||||
|
||||
env.bus.addEventListener("ACTION_MANAGER:UPDATE", clearCache);
|
||||
|
||||
function getMapping(resModel) {
|
||||
if (!cache[resModel]) {
|
||||
cache[resModel] = {};
|
||||
}
|
||||
return cache[resModel];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} resModel valid resModel name
|
||||
* @param {DisplayNames} displayNames
|
||||
*/
|
||||
function addDisplayNames(resModel, displayNames) {
|
||||
const mapping = getMapping(resModel);
|
||||
for (const resId in displayNames) {
|
||||
mapping[resId] = new Deferred();
|
||||
mapping[resId].resolve(displayNames[resId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} resModel valid resModel name
|
||||
* @param {number[]} resIds valid ids
|
||||
* @returns {Promise<DisplayNames>}
|
||||
*/
|
||||
async function loadDisplayNames(resModel, resIds) {
|
||||
const mapping = getMapping(resModel);
|
||||
const proms = [];
|
||||
const resIdsToFetch = [];
|
||||
for (const resId of unique(resIds)) {
|
||||
if (!isId(resId)) {
|
||||
throw new Error(`Invalid ID: ${resId}`);
|
||||
}
|
||||
if (!(resId in mapping)) {
|
||||
mapping[resId] = new Deferred();
|
||||
resIdsToFetch.push(resId);
|
||||
}
|
||||
proms.push(mapping[resId]);
|
||||
}
|
||||
if (resIdsToFetch.length) {
|
||||
if (batches[resModel]) {
|
||||
batches[resModel].push(...resIdsToFetch);
|
||||
} else {
|
||||
batches[resModel] = resIdsToFetch;
|
||||
await Promise.resolve();
|
||||
const idsInBatch = unique(batches[resModel]);
|
||||
delete batches[resModel];
|
||||
|
||||
orm.silent
|
||||
.webSearchRead(resModel, [["id", "in", idsInBatch]], ["display_name"])
|
||||
.then(({ records }) => {
|
||||
const displayNames = Object.fromEntries(
|
||||
records.map((rec) => [rec.id, rec.display_name])
|
||||
);
|
||||
for (const resId of idsInBatch) {
|
||||
mapping[resId].resolve(
|
||||
resId in displayNames
|
||||
? displayNames[resId]
|
||||
: ERROR_INACCESSIBLE_OR_MISSING
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const names = await Promise.all(proms);
|
||||
return Object.fromEntries(zip(resIds, names));
|
||||
}
|
||||
|
||||
return { addDisplayNames, clearCache, loadDisplayNames };
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("name", nameService);
|
||||
@@ -0,0 +1,142 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ERROR_INACCESSIBLE_OR_MISSING, nameService } from "@web/core/name_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Name Service", {
|
||||
async beforeEach() {
|
||||
serverData = {
|
||||
models: {
|
||||
dev: {
|
||||
fields: {},
|
||||
records: [
|
||||
{ id: 1, display_name: "Julien" },
|
||||
{ id: 2, display_name: "Pierre" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("name", nameService);
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("single loadDisplayNames", async (assert) => {
|
||||
const env = await makeTestEnv({ serverData });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [1, 2]);
|
||||
assert.deepEqual(displayNames, { 1: "Julien", 2: "Pierre" });
|
||||
});
|
||||
|
||||
QUnit.test("loadDisplayNames is done in silent mode", async (assert) => {
|
||||
assert.expect(2);
|
||||
const env = await makeTestEnv({ serverData });
|
||||
env.bus.addEventListener("RPC:REQUEST", (ev) => {
|
||||
const silent = ev.detail.settings.silent;
|
||||
assert.step("RPC:REQUEST" + (silent ? " (silent)" : ""));
|
||||
});
|
||||
await env.services.name.loadDisplayNames("dev", [1]);
|
||||
assert.verifySteps(["RPC:REQUEST (silent)"]);
|
||||
});
|
||||
|
||||
QUnit.test("single loadDisplayNames following addDisplayNames", async (assert) => {
|
||||
const mockRPC = (_, { method }) => {
|
||||
assert.step(method);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
env.services.name.addDisplayNames("dev", { 1: "JUM", 2: "PIPU" });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [1, 2]);
|
||||
assert.deepEqual(displayNames, { 1: "JUM", 2: "PIPU" });
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("single loadDisplayNames following addDisplayNames (2)", async (assert) => {
|
||||
const mockRPC = (_, { kwargs, method }) => {
|
||||
assert.step(method);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
env.services.name.addDisplayNames("dev", { 1: "JUM" });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [1, 2]);
|
||||
assert.deepEqual(displayNames, { 1: "JUM", 2: "Pierre" });
|
||||
assert.verifySteps(["web_search_read", "id(s): 2"]);
|
||||
});
|
||||
|
||||
QUnit.test("loadDisplayNames in batch", async (assert) => {
|
||||
const mockRPC = (_, { kwargs, method }) => {
|
||||
assert.step(method);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const prom1 = env.services.name.loadDisplayNames("dev", [1]);
|
||||
assert.verifySteps([]);
|
||||
const prom2 = env.services.name.loadDisplayNames("dev", [2]);
|
||||
assert.verifySteps([]);
|
||||
const [displayNames1, displayNames2] = await Promise.all([prom1, prom2]);
|
||||
assert.deepEqual(displayNames1, { 1: "Julien" });
|
||||
assert.deepEqual(displayNames2, { 2: "Pierre" });
|
||||
assert.verifySteps(["web_search_read", "id(s): 1, 2"]);
|
||||
});
|
||||
|
||||
QUnit.test("loadDisplayNames on different models", async (assert) => {
|
||||
serverData.models.PO = {
|
||||
fields: {},
|
||||
records: [{ id: 1, display_name: "Damien" }],
|
||||
};
|
||||
const mockRPC = (_, { kwargs, method, model }) => {
|
||||
assert.step(method);
|
||||
assert.step(model);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const prom1 = env.services.name.loadDisplayNames("dev", [1]);
|
||||
assert.verifySteps([]);
|
||||
const prom2 = env.services.name.loadDisplayNames("PO", [1]);
|
||||
assert.verifySteps([]);
|
||||
const [displayNames1, displayNames2] = await Promise.all([prom1, prom2]);
|
||||
assert.deepEqual(displayNames1, { 1: "Julien" });
|
||||
assert.deepEqual(displayNames2, { 1: "Damien" });
|
||||
assert.verifySteps(["web_search_read", "dev", "id(s): 1", "web_search_read", "PO", "id(s): 1"]);
|
||||
});
|
||||
|
||||
QUnit.test("invalid id", async (assert) => {
|
||||
assert.expect(1);
|
||||
const env = await makeTestEnv({ serverData });
|
||||
try {
|
||||
await env.services.name.loadDisplayNames("dev", ["a"]);
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.message, "Invalid ID: a");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("inaccessible or missing id", async (assert) => {
|
||||
const mockRPC = (_, { method }) => {
|
||||
assert.step(method);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [3]);
|
||||
assert.deepEqual(displayNames, { 3: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("batch + inaccessible/missing", async (assert) => {
|
||||
const mockRPC = (_, { method, kwargs }) => {
|
||||
assert.step(method);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const prom1 = env.services.name.loadDisplayNames("dev", [1, 3]);
|
||||
assert.verifySteps([]);
|
||||
const prom2 = env.services.name.loadDisplayNames("dev", [2, 4]);
|
||||
assert.verifySteps([]);
|
||||
const [displayNames1, displayNames2] = await Promise.all([prom1, prom2]);
|
||||
assert.deepEqual(displayNames1, { 1: "Julien", 3: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
assert.deepEqual(displayNames2, { 2: "Pierre", 4: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
assert.verifySteps(["web_search_read", "id(s): 1, 3, 2, 4"]);
|
||||
});
|
||||
+2
@@ -9,6 +9,7 @@ declare module "services" {
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { httpService } from "@web/core/network/http_service";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
@@ -30,6 +31,7 @@ declare module "services" {
|
||||
hotkey: ReturnType<typeof hotkeyService.start>;
|
||||
http: ReturnType<typeof httpService.start>;
|
||||
menu: Awaited<ReturnType<typeof menuService.start>>;
|
||||
name: ReturnType<typeof nameService.start>;
|
||||
notification: ReturnType<typeof notificationService.start>;
|
||||
orm: ReturnType<typeof ormService.start>;
|
||||
popover: ReturnType<typeof popoverService.start>;
|
||||
|
||||
Reference in New Issue
Block a user