import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { exprToBoolean } from "@web/core/utils/strings"; import { combineModifiers } from "@web/model/relational_model/utils"; export const X2M_TYPES = ["one2many", "many2many"]; const NUMERIC_TYPES = ["integer", "float", "monetary"]; /** * @typedef ViewActiveActions { * @property {"view"} type * @property {boolean} edit * @property {boolean} create * @property {boolean} delete * @property {boolean} duplicate */ export const BUTTON_CLICK_PARAMS = [ "name", "type", "args", "block-ui", // Blocks UI with a spinner until the action is done "context", "close", "cancel-label", "confirm", "confirm-title", "confirm-label", "special", "effect", "help", // WOWL SAD: is adding the support for debounce attribute here justified or should we // just override compileButton in kanban compiler to add the debounce? "debounce", // WOWL JPP: is adding the support for not oppening the dialog of confirmation in the settings view // This should be refactor someday "noSaveDialog", ]; /** * @param {string?} type * @returns {string | false} */ function getViewClass(type) { const isValidType = Boolean(type) && registry.category("views").contains(type); return isValidType && `o_${type}_view`; } /** * @param {string?} viewType * @param {Element?} rootNode * @param {string[]} additionalClassList * @returns {string} */ export function computeViewClassName(viewType, rootNode, additionalClassList = []) { const subType = rootNode?.getAttribute("js_class"); const classList = rootNode?.getAttribute("class")?.split(" ") || []; const uniqueClasses = new Set([ getViewClass(viewType), getViewClass(subType), ...classList, ...additionalClassList, ]); return Array.from(uniqueClasses) .filter((c) => c) // remove falsy values .join(" "); } /** * TODO: doc * * @param {Object} fields * @param {Object} fieldAttrs * @param {string[]} activeMeasures * @returns {Object} */ export const computeReportMeasures = ( fields, fieldAttrs, activeMeasures, { sumAggregatorOnly = false } = {} ) => { const measures = { __count: { name: "__count", string: _t("Count"), type: "integer" }, }; for (const [fieldName, field] of Object.entries(fields)) { if (fieldName === "id") { continue; } const { isInvisible } = fieldAttrs[fieldName] || {}; if (isInvisible) { continue; } if ( ["integer", "float", "monetary"].includes(field.type) && ((sumAggregatorOnly && field.aggregator === "sum") || (!sumAggregatorOnly && field.aggregator)) ) { measures[fieldName] = field; } } // add active measures to the measure list. This is very rarely // necessary, but it can be useful if one is working with a // functional field non stored, but in a model with an overridden // read_group method. In this case, the pivot view could work, and // the measure should be allowed. However, be careful if you define // a measure in your pivot view: non stored functional fields will // probably not work (their aggregate will always be 0). for (const measure of activeMeasures) { if (!measures[measure]) { measures[measure] = fields[measure]; } } for (const fieldName in fieldAttrs) { if (fieldAttrs[fieldName].string && fieldName in measures) { measures[fieldName].string = fieldAttrs[fieldName].string; } } const sortedMeasures = Object.entries(measures).sort(([m1, f1], [m2, f2]) => { if (m1 === "__count" || m2 === "__count") { return m1 === "__count" ? 1 : -1; // Count is always last } return f1.string.toLowerCase().localeCompare(f2.string.toLowerCase()); }); return Object.fromEntries(sortedMeasures); }; /** * @param {Record} record * @param {String} fieldName * @param {Object} [fieldInfo] * @returns {String} */ export function getFormattedValue(record, fieldName, fieldInfo = null) { const field = record.fields[fieldName]; const formatter = registry.category("formatters").get(field.type, (val) => val); const formatOptions = {}; if (fieldInfo && formatter.extractOptions) { Object.assign(formatOptions, formatter.extractOptions(fieldInfo)); } formatOptions.data = record.data; formatOptions.field = field; return record.data[fieldName] !== undefined ? formatter(record.data[fieldName], formatOptions) : ""; } /** * @param {Element} rootNode * @returns {ViewActiveActions} */ export function getActiveActions(rootNode) { const activeActions = { type: "view", edit: exprToBoolean(rootNode.getAttribute("edit"), true), create: exprToBoolean(rootNode.getAttribute("create"), true), delete: exprToBoolean(rootNode.getAttribute("delete"), true), }; activeActions.duplicate = activeActions.create && exprToBoolean(rootNode.getAttribute("duplicate"), true); return activeActions; } export function getClassNameFromDecoration(decoration) { if (decoration === "bf") { return "fw-bold"; } else if (decoration === "it") { return "fst-italic"; } return `text-${decoration}`; } export function getDecoration(rootNode) { const decorations = []; for (const name of rootNode.getAttributeNames()) { if (name.startsWith("decoration-")) { decorations.push({ class: getClassNameFromDecoration(name.replace("decoration-", "")), condition: rootNode.getAttribute(name), }); } } return decorations; } /** * @param {any} field * @returns {boolean} */ export function isX2Many(field) { return field && X2M_TYPES.includes(field.type); } /** * @param {Object} field * @returns {boolean} true iff the given field is a numeric field */ export function isNumeric(field) { return NUMERIC_TYPES.includes(field.type); } /** * @param {any} value * @returns {boolean} */ export function isNull(value) { return [null, undefined].includes(value); } export function processButton(node) { const withDefault = { close: (val) => exprToBoolean(val, false), context: (val) => val || "{}", }; const clickParams = {}; const attrs = {}; for (const { name, value } of node.attributes) { if (BUTTON_CLICK_PARAMS.includes(name)) { clickParams[name] = withDefault[name] ? withDefault[name](value) : value; } else { attrs[name] = value; } } return { className: node.getAttribute("class") || "", disabled: !!node.getAttribute("disabled") || false, icon: node.getAttribute("icon") || false, title: node.getAttribute("title") || undefined, string: node.getAttribute("string") || undefined, options: JSON.parse(node.getAttribute("options") || "{}"), display: node.getAttribute("display") || "selection", clickParams, column_invisible: node.getAttribute("column_invisible"), invisible: combineModifiers( node.getAttribute("column_invisible"), node.getAttribute("invisible"), "OR" ), readonly: node.getAttribute("readonly"), required: node.getAttribute("required"), attrs, }; } /** * In the preview implementation of reporting views, the virtual field used to * display the number of records was named __count__, whereas __count is * actually the one used in xml. So basically, activating a filter specifying * __count as measures crashed. Unfortunately, as __count__ was used in the JS, * all filters saved as favorite at that time were saved with __count__, and * not __count. So in order the make them still work with the new * implementation, we handle both __count__ and __count. * * This function replaces occurences of '__count__' by '__count' in the given * element(s). * * @param {any | any[]} [measures] * @returns {any} */ export function processMeasure(measure) { if (Array.isArray(measure)) { return measure.map(processMeasure); } return measure === "__count__" ? "__count" : measure; } /** * Transforms a string into a valid expression to be injected * in a template as a props via setAttribute. * Example: myString = `Some weird language quote (") `; * should become in the template: * * which should be interpreted by owl as a JS expression being a string: * `Some weird language quote (") ` * * @param {string} str The initial value: a pure string to be interpreted as such * @return {string} the valid string to be injected into a component's node props. */ export function toStringExpression(str) { return `\`${str.replaceAll("`", "\\`")}\``; } /** * Generate a unique identifier (64 bits) in hexadecimal. * * @returns {string} */ export function uuid() { const array = new Uint8Array(8); window.crypto.getRandomValues(array); // Uint8Array to hex return [...array].map((b) => b.toString(16).padStart(2, "0")).join(""); }