[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
This commit is contained in:
Julien Mougenot
2023-05-05 13:52:21 +02:00
parent f853ca8eec
commit 7874bf3fc1
14 changed files with 83 additions and 62 deletions
@@ -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.
@@ -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,
+6 -5
View File
@@ -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;
@@ -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;
+1 -2
View File
@@ -35,10 +35,9 @@ export function deepCopy(obj) {
* @template {keyof T} K
* @param {T} object
* @param {K[]} properties
* @returns {Omit<T, K>}
*/
export function omit(object, ...properties) {
/** @type {any} */
/** @type {Omit<T, K>} */
const result = {};
const propertiesSet = new Set(properties);
for (const key in object) {
+21 -4
View File
@@ -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<number, HTMLElement[]>} */
const byTabIndex = {};
for (const el of [...elements]) {
if (!byTabIndex[el.tabIndex]) {
@@ -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: {},
@@ -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 },
@@ -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)
@@ -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.
@@ -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,
+18 -8
View File
@@ -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<string, any>} info
* @param {Record<string, Object>} activeFields
* @param {Record<string, Object>} 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 };
+1 -1
View File
@@ -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");
}
}
+5 -6
View File
@@ -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`<Dialog><View t-props="props.viewProps"/></Dialog
*/
/**
* @template {Component} T
* @param {MakeViewParams} params
* @param {boolean} [inDialog=false]
* @returns {Component}
* @returns {Promise<T>}
*/
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);