From a1365d7655bd1c9b49cb02f382c13ff431660fdd Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Thu, 6 Jul 2023 12:51:32 +0000 Subject: [PATCH] [IMP] web: model field selector: search on string and name in debug mode We allow the user to make a fuzzy search on the string and the name of the fields when the props isDebugMode is true. Part-of: odoo/odoo#121688 --- .../model_field_selector_popover.js | 36 +++++++++++++------ addons/web/static/src/core/utils/search.js | 22 ++++++++++-- .../tests/core/model_field_selector_tests.js | 31 ++++++++++++++++ 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js index 83eb837697e..c869613ffe0 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js +++ b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js @@ -8,11 +8,13 @@ import { sortBy } from "@web/core/utils/arrays"; import { useService } from "@web/core/utils/hooks"; class Page { - constructor(resModel, fieldDefs, previousPage = null, selectedName = null) { + constructor(resModel, fieldDefs, options = {}) { this.resModel = resModel; this.fieldDefs = fieldDefs; + const { previousPage = null, selectedName = null, isDebugMode } = options; this.previousPage = previousPage; this.selectedName = selectedName; + this.isDebugMode = isDebugMode; this.sortedFieldNames = sortBy(Object.keys(fieldDefs), (key) => fieldDefs[key].string); this.fieldNames = this.sortedFieldNames; this.query = ""; @@ -74,11 +76,13 @@ class Page { this.query = query; this.fieldNames = this.sortedFieldNames; if (query) { - this.fieldNames = fuzzyLookup( - query, - this.fieldNames, - (key) => this.fieldDefs[key].string - ); + this.fieldNames = fuzzyLookup(query, this.fieldNames, (key) => { + const vals = [this.fieldDefs[key].string]; + if (this.isDebugMode) { + vals.push(key); + } + return vals; + }); } this.resetFocusedFieldName(); } @@ -149,7 +153,10 @@ export class ModelFieldSelectorPopover extends Component { this.state.page.selectedName = fieldDef.name; const { resModel, fieldDefs } = modelsInfo.at(-1); this.openPage( - new Page(resModel, this.filter(fieldDefs, this.state.page.path), this.state.page) + new Page(resModel, this.filter(fieldDefs, this.state.page.path), { + previousPage: this.state.page, + isDebugMode: this.props.isDebugMode, + }) ); } @@ -166,7 +173,9 @@ export class ModelFieldSelectorPopover extends Component { async loadPages(resModel, path) { if (typeof path !== "string" || !path.length) { const fieldDefs = await this.fieldService.loadFields(resModel); - return new Page(resModel, this.filter(fieldDefs, path)); + return new Page(resModel, this.filter(fieldDefs, path), { + isDebugMode: this.props.isDebugMode, + }); } const { isInvalid, modelsInfo, names } = await this.fieldService.loadPath(resModel, path); switch (isInvalid) { @@ -174,14 +183,21 @@ export class ModelFieldSelectorPopover extends Component { throw new Error(`Invalid model name: ${resModel}`); case "path": { const { resModel, fieldDefs } = modelsInfo[0]; - return new Page(resModel, this.filter(fieldDefs, path), null, path); + return new Page(resModel, this.filter(fieldDefs, path), { + selectedName: path, + isDebugMode: this.props.isDebugMode, + }); } default: { let page = null; for (let index = 0; index < names.length; index++) { const name = names[index]; const { resModel, fieldDefs } = modelsInfo[index]; - page = new Page(resModel, this.filter(fieldDefs, path), page, name); + page = new Page(resModel, this.filter(fieldDefs, path), { + previousPage: page, + selectedName: name, + isDebugMode: this.props.isDebugMode, + }); } return page; } diff --git a/addons/web/static/src/core/utils/search.js b/addons/web/static/src/core/utils/search.js index ea484fdeec9..09604e7b50f 100644 --- a/addons/web/static/src/core/utils/search.js +++ b/addons/web/static/src/core/utils/search.js @@ -2,6 +2,22 @@ import { unaccent } from "./strings"; +/** + * @param {string} pattern + * @param {string|string[]} strs + * @returns {number} + */ +function match(pattern, strs) { + if (!Array.isArray(strs)) { + strs = [strs]; + } + let globalScore = 0; + for (const str of strs) { + globalScore = Math.max(globalScore, _match(pattern, str)); + } + return globalScore; +} + /** * This private function computes a score that represent the fact that the * string contains the pattern, or not @@ -17,7 +33,7 @@ import { unaccent } from "./strings"; * @param {string} str * @returns {number} */ -function match(pattern, str) { +function _match(pattern, str) { let totalScore = 0; let currentScore = 0; const len = str.length; @@ -47,7 +63,7 @@ function match(pattern, str) { * @template T * @param {string} pattern * @param {T[]} list - * @param {(element: T) => string} fn + * @param {(element: T) => (string|string[])} fn * @returns {T[]} */ export function fuzzyLookup(pattern, list, fn) { @@ -72,5 +88,5 @@ export function fuzzyLookup(pattern, list, fn) { * @returns {boolean} */ export function fuzzyTest(pattern, string) { - return match(pattern, string) !== 0; + return _match(pattern, string) !== 0; } diff --git a/addons/web/static/tests/core/model_field_selector_tests.js b/addons/web/static/tests/core/model_field_selector_tests.js index a46afde6837..15be1526071 100644 --- a/addons/web/static/tests/core/model_field_selector_tests.js +++ b/addons/web/static/tests/core/model_field_selector_tests.js @@ -831,4 +831,35 @@ QUnit.module("Components", (hooks) => { ); assert.containsNone(target, ".o_model_field_selector_warning"); }); + + QUnit.test("search on field string and name in debug mode", async (assert) => { + patchWithCleanup(browser, { setTimeout: (fn) => fn() }); // for debouncedSearchFields + serverData.models.partner.fields.ucit = { + type: "char", + string: "Some string", + searchable: true, + }; + class Parent extends Component { + static components = { ModelFieldSelector }; + static template = xml` + + `; + } + await mountComponent(Parent); + await openModelFieldSelectorPopover(target); + await editInput( + target, + ".o_model_field_selector_popover .o_model_field_selector_popover_search input", + "uct" + ); + assert.deepEqual(getDisplayedFieldNames(target), [ + "Productproduct_id (many2one)", + "Some stringucit (char)", + ]); + }); });