[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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user