From 7874bf3fc1f857c1189c5018ccfbff477df241da Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Thu, 13 Apr 2023 08:27:52 +0000 Subject: [PATCH] [IMP] web: Function fieldDependencies + utils doc This commit does the following in the Web module utilities: - makes the `fieldDependencies` property of the field objects definitions a function instead of a list of strings: this makes it possible for fields to extract their dependencies based on the fields info from the arch; - adds/improves types and documentation for some utility functions. Part of task 3121497 Part-of: odoo/odoo#112171 --- .../views/activity/activity_arch_parser.js | 6 +---- .../web/static/src/core/l10n/localization.js | 14 ++++++++++ addons/web/static/src/core/utils/arrays.js | 11 ++++---- .../web/static/src/core/utils/concurrency.js | 2 +- addons/web/static/src/core/utils/objects.js | 3 +-- addons/web/static/src/core/utils/ui.js | 25 +++++++++++++++--- addons/web/static/src/views/fields/field.js | 1 + .../src/views/fields/standard_field_props.js | 8 ++++++ .../static/src/views/form/form_arch_parser.js | 12 ++------- .../src/views/kanban/kanban_arch_parser.js | 12 ++------- .../static/src/views/list/list_arch_parser.js | 12 ++------- addons/web/static/src/views/utils.js | 26 +++++++++++++------ addons/web/static/tests/helpers/utils.js | 2 +- addons/web/static/tests/views/helpers.js | 11 ++++---- 14 files changed, 83 insertions(+), 62 deletions(-) diff --git a/addons/mail/static/src/views/activity/activity_arch_parser.js b/addons/mail/static/src/views/activity/activity_arch_parser.js index 24ecf16734f..d304d3417cb 100644 --- a/addons/mail/static/src/views/activity/activity_arch_parser.js +++ b/addons/mail/static/src/views/activity/activity_arch_parser.js @@ -36,11 +36,7 @@ export class ActivityArchParser extends XMLParser { fieldNodes[fieldId] = fieldInfo; node.setAttribute("field_id", fieldId); - addFieldDependencies( - activeFields, - models[modelName], - fieldInfo.field.fieldDependencies - ); + addFieldDependencies(fieldInfo, activeFields, models[modelName]); } // Keep track of last update so images can be reloaded when they may have changed. diff --git a/addons/web/static/src/core/l10n/localization.js b/addons/web/static/src/core/l10n/localization.js index e0cc5bdc5c5..d9e85f12248 100644 --- a/addons/web/static/src/core/l10n/localization.js +++ b/addons/web/static/src/core/l10n/localization.js @@ -1,5 +1,18 @@ /** @odoo-module **/ +/** + * @typedef Localization + * @property {string} dateFormat + * @property {string} dateTimeFormat + * @property {string} timeFormat + * @property {string} decimalPoint + * @property {"ltr" | "rtl"} direction + * @property {[number, number]} grouping + * @property {boolean} multiLang + * @property {string} thousandsSep + * @property {number} weekStart + */ + /** * @type {any} */ @@ -16,6 +29,7 @@ const notReadyError = new Error( * so a code like the following would not work: * import { localization } from "@web/core/l10n/localization"; * const dateFormat = localization.dateFormat; // dateFormat isn't set yet + * @type {Localization} */ export const localization = { dateFormat: notReadyError, diff --git a/addons/web/static/src/core/utils/arrays.js b/addons/web/static/src/core/utils/arrays.js index d5ed7a62171..4bfaed52623 100644 --- a/addons/web/static/src/core/utils/arrays.js +++ b/addons/web/static/src/core/utils/arrays.js @@ -213,15 +213,16 @@ export function unique(array) { } /** - * @template T1, T2, T + * @template T1, T2 * @param {T1[]} array1 * @param {T2[]} array2 - * @returns {T[]} + * @param {boolean} [fill=false] + * @returns {[T1, T2][]} */ -export function zip(array1, array2) { +export function zip(array1, array2, fill = false) { const result = []; - const minLength = Math.min(array1.length, array2.length); - for (let i = 0; i < minLength; i++) { + const getLength = fill ? Math.max : Math.min; + for (let i = 0; i < getLength(array1.length, array2.length); i++) { result.push([array1[i], array2[i]]); } return result; diff --git a/addons/web/static/src/core/utils/concurrency.js b/addons/web/static/src/core/utils/concurrency.js index 266abe3bf31..a81b163cb65 100644 --- a/addons/web/static/src/core/utils/concurrency.js +++ b/addons/web/static/src/core/utils/concurrency.js @@ -169,7 +169,7 @@ export class Race { /** * Deferred is basically a resolvable/rejectable extension of Promise. */ -export class Deferred { +export class Deferred extends Promise { constructor() { let resolve; let reject; diff --git a/addons/web/static/src/core/utils/objects.js b/addons/web/static/src/core/utils/objects.js index 6910ffbb37e..fbf4cd1b634 100644 --- a/addons/web/static/src/core/utils/objects.js +++ b/addons/web/static/src/core/utils/objects.js @@ -35,10 +35,9 @@ export function deepCopy(obj) { * @template {keyof T} K * @param {T} object * @param {K[]} properties - * @returns {Omit} */ export function omit(object, ...properties) { - /** @type {any} */ + /** @type {Omit} */ const result = {}; const propertiesSet = new Set(properties); for (const key in object) { diff --git a/addons/web/static/src/core/utils/ui.js b/addons/web/static/src/core/utils/ui.js index 891f0a70b58..7ae4dd093a6 100644 --- a/addons/web/static/src/core/utils/ui.js +++ b/addons/web/static/src/core/utils/ui.js @@ -156,13 +156,30 @@ export function touching(elements, targetRect) { // - redefine this selector in tests env with ":not(#qunit *)" ? // Following selector is based on this spec: https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex -let TABABLE_SELECTOR = "[tabindex], a, area, button, frame, iframe, input, object, select, textarea, details > summary:nth-child(1)," - .split(",") - .join(':not([tabindex="-1"]):not(:disabled),'); -TABABLE_SELECTOR = TABABLE_SELECTOR.slice(0, TABABLE_SELECTOR.length - 1); +const TABABLE_SELECTOR = [ + "[tabindex]", + "a", + "area", + "button", + "frame", + "iframe", + "input", + "object", + "select", + "textarea", + "details > summary:nth-child(1)", +] + .map((sel) => `${sel}:not([tabindex="-1"]):not(:disabled)`) + .join(","); +/** + * Returns all focusable elements in the given container. + * + * @param {HTMLElement} [container=document.body] + */ export function getTabableElements(container = document.body) { const elements = container.querySelectorAll(TABABLE_SELECTOR); + /** @type {Record} */ const byTabIndex = {}; for (const el of [...elements]) { if (!byTabIndex[el.tabIndex]) { diff --git a/addons/web/static/src/views/fields/field.js b/addons/web/static/src/views/fields/field.js index 24c8f3fe451..65a70d075d7 100644 --- a/addons/web/static/src/views/fields/field.js +++ b/addons/web/static/src/views/fields/field.js @@ -210,6 +210,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) { const field = getFieldFromRegistry(fields[name].type, widget, viewType, jsClass); const fieldInfo = { name, + type: fields[name].type, viewType, widget, modifiers: {}, diff --git a/addons/web/static/src/views/fields/standard_field_props.js b/addons/web/static/src/views/fields/standard_field_props.js index b9f8071b2a1..85c19bb6b7b 100644 --- a/addons/web/static/src/views/fields/standard_field_props.js +++ b/addons/web/static/src/views/fields/standard_field_props.js @@ -1,5 +1,13 @@ /** @odoo-module **/ +/** + * @typedef StandardFieldProps + * @property {string} [id] + * @property {string} name + * @property {boolean} [readonly] + * @property {RelationalRecord} record + */ + export const standardFieldProps = { id: { type: String, optional: true }, name: { type: String }, diff --git a/addons/web/static/src/views/form/form_arch_parser.js b/addons/web/static/src/views/form/form_arch_parser.js index d50975f9316..ca89ac0d793 100644 --- a/addons/web/static/src/views/form/form_arch_parser.js +++ b/addons/web/static/src/views/form/form_arch_parser.js @@ -29,11 +29,7 @@ export class FormArchParser extends XMLParser { if (archParseBoolean(node.getAttribute("default_focus") || "")) { autofocusFieldId = fieldId; } - addFieldDependencies( - activeFields, - models[modelName], - fieldInfo.field.fieldDependencies - ); + addFieldDependencies(fieldInfo, activeFields, models[modelName]); return false; } else if (node.tagName === "div" && node.classList.contains("oe_chatter")) { // remove this when chatter fields are declared as attributes on the root node @@ -43,11 +39,7 @@ export class FormArchParser extends XMLParser { const widgetId = `widget_${++widgetNextId}`; widgetNodes[widgetId] = widgetInfo; node.setAttribute("widget_id", widgetId); - addFieldDependencies( - activeFields, - models[modelName], - widgetInfo.widget.fieldDependencies - ); + addFieldDependencies(widgetInfo, activeFields, models[modelName]); } }); // TODO: generate activeFields for the model based on fieldNodes (merge duplicated fields) diff --git a/addons/web/static/src/views/kanban/kanban_arch_parser.js b/addons/web/static/src/views/kanban/kanban_arch_parser.js index 0156ff831a0..17e945ad5f7 100644 --- a/addons/web/static/src/views/kanban/kanban_arch_parser.js +++ b/addons/web/static/src/views/kanban/kanban_arch_parser.js @@ -124,22 +124,14 @@ export class KanbanArchParser extends XMLParser { if (fieldInfo.widget === "handle") { handleField = name; } - addFieldDependencies( - activeFields, - models[modelName], - fieldInfo.field.fieldDependencies - ); + addFieldDependencies(fieldInfo, activeFields, models[modelName]); } if (node.tagName === "widget") { const widgetInfo = Widget.parseWidgetNode(node); const widgetId = `widget_${++widgetNextId}`; widgetNodes[widgetId] = widgetInfo; node.setAttribute("widget_id", widgetId); - addFieldDependencies( - activeFields, - models[modelName], - widgetInfo.widget.fieldDependencies - ); + addFieldDependencies(widgetInfo, activeFields, models[modelName]); } // Keep track of last update so images can be reloaded when they may have changed. diff --git a/addons/web/static/src/views/list/list_arch_parser.js b/addons/web/static/src/views/list/list_arch_parser.js index 013096d7514..3624492dbf8 100644 --- a/addons/web/static/src/views/list/list_arch_parser.js +++ b/addons/web/static/src/views/list/list_arch_parser.js @@ -119,11 +119,7 @@ export class ListArchParser extends XMLParser { if (fieldInfo.widget === "handle") { handleField = fieldInfo.name; } - addFieldDependencies( - activeFields, - models[modelName], - fieldInfo.field.fieldDependencies - ); + addFieldDependencies(fieldInfo, activeFields, models[modelName]); if (this.isColumnVisible(fieldInfo.modifiers.column_invisible)) { const label = fieldInfo.field.label; columns.push({ @@ -144,11 +140,7 @@ export class ListArchParser extends XMLParser { const widgetId = `widget_${++widgetNextId}`; widgetNodes[widgetId] = widgetInfo; node.setAttribute("widget_id", widgetId); - addFieldDependencies( - activeFields, - models[modelName], - widgetInfo.widget.fieldDependencies - ); + addFieldDependencies(widgetInfo, activeFields, models[modelName]); const widgetProps = { name: widgetInfo.name, diff --git a/addons/web/static/src/views/utils.js b/addons/web/static/src/views/utils.js index c45b7fde15d..84d5fdc41de 100644 --- a/addons/web/static/src/views/utils.js +++ b/addons/web/static/src/views/utils.js @@ -41,18 +41,28 @@ export const BUTTON_CLICK_PARAMS = [ ]; /** - * Add dependencies to activeFields + * Add dependencies to activeFields (and to fields if needed) from an "info" object, + * which is expected to contain either a "field" or a "widget" key containing the + * list of dependencies or a function returning it. * - * @param {Object} activeFields - * @param {Object} [dependencies={}] + * @param {Record} info + * @param {Record} activeFields + * @param {Record} fields */ -export function addFieldDependencies(activeFields, fields, dependencies = []) { - for (const dependency of dependencies) { +export function addFieldDependencies(info, activeFields, fields) { + const { fieldDependencies } = info.field || info.widget; + const deps = + typeof fieldDependencies === "function" ? fieldDependencies(info) : fieldDependencies; + for (const dependency of deps || []) { const { name } = dependency; if (!(name in activeFields)) { - activeFields[name] = Object.assign({ name, attrs: {} }, dependency, { - modifiers: { invisible: true }, - }); + activeFields[name] = { + name, + attrs: {}, + options: {}, + ...dependency, + modifiers: { invisible: true, ...dependency.modifiers }, + }; } if (!(name in fields)) { fields[name] = { ...dependency }; diff --git a/addons/web/static/tests/helpers/utils.js b/addons/web/static/tests/helpers/utils.js index e7a02381154..75ef76cb2ad 100644 --- a/addons/web/static/tests/helpers/utils.js +++ b/addons/web/static/tests/helpers/utils.js @@ -144,7 +144,7 @@ export function getFixture() { if (QUnit.config.debug) { return document.body; } else { - return document.querySelector("#qunit-fixture"); + return document.getElementById("qunit-fixture"); } } diff --git a/addons/web/static/tests/views/helpers.js b/addons/web/static/tests/views/helpers.js index eedd7a05d5c..04b0eb69a01 100644 --- a/addons/web/static/tests/views/helpers.js +++ b/addons/web/static/tests/views/helpers.js @@ -7,16 +7,16 @@ import { Dialog } from "@web/core/dialog/dialog"; import { MainComponentsContainer } from "@web/core/main_components_container"; import { popoverService } from "@web/core/popover/popover_service"; import { registry } from "@web/core/registry"; -import { getDefaultConfig, View } from "@web/views/view"; +import { View, getDefaultConfig } from "@web/views/view"; import { fakeCompanyService, makeFakeLocalizationService, makeFakeRouterService, - makeFakeUserService + makeFakeUserService, } from "../helpers/mock_services"; import { setupControlPanelFavoriteMenuRegistry, - setupControlPanelServiceRegistry + setupControlPanelServiceRegistry, } from "../search/helpers"; import { addLegacyMockEnvironment } from "../webclient/helpers"; @@ -40,9 +40,10 @@ const rootDialogTemplate = xml`} */ async function _makeView(params, inDialog = false) { const props = { ...params }; @@ -123,7 +124,6 @@ async function _makeView(params, inDialog = false) { /** * @param {MakeViewParams} params - * @returns {Component} */ export function makeView(params) { return _makeView(params); @@ -131,7 +131,6 @@ export function makeView(params) { /** * @param {MakeViewParams} params - * @returns {Component} */ export function makeViewInDialog(params) { return _makeView(params, true);