From 51362ccb1d808a2b7a34c73672366fba8201ee43 Mon Sep 17 00:00:00 2001 From: Ajmal Cybro Date: Mon, 31 Jan 2022 16:58:27 +0530 Subject: [PATCH] [FIX] Bug Fixed 'code_backend_theme' --- code_backend_theme/__manifest__.py | 9 +- .../static/src/js/fields/colors.js | 30 +- .../static/src/js/fields/graph_arch_parser.js | 82 --- .../static/src/js/fields/graph_model.js | 537 --------------- .../static/src/js/fields/graph_renderer.js | 627 ------------------ .../static/src/js/fields/graph_view.js | 160 ----- code_backend_theme/static/src/scss/login.scss | 6 +- 7 files changed, 10 insertions(+), 1441 deletions(-) delete mode 100644 code_backend_theme/static/src/js/fields/graph_arch_parser.js delete mode 100644 code_backend_theme/static/src/js/fields/graph_model.js delete mode 100644 code_backend_theme/static/src/js/fields/graph_renderer.js delete mode 100644 code_backend_theme/static/src/js/fields/graph_view.js diff --git a/code_backend_theme/__manifest__.py b/code_backend_theme/__manifest__.py index 86b9f58c0..f18eb4235 100644 --- a/code_backend_theme/__manifest__.py +++ b/code_backend_theme/__manifest__.py @@ -25,7 +25,7 @@ "description": """Minimalist and elegant backend theme for Odoo 14, Backend Theme, Theme""", "summary": "Code Backend Theme V15 is an attractive theme for backend", "category": "Themes/Backend", - "version": "15.0.1.0.2", + "version": "15.0.1.0.3", 'author': 'Cybrosys Techno Solutions', 'company': 'Cybrosys Techno Solutions', 'maintainer': 'Cybrosys Techno Solutions', @@ -45,11 +45,8 @@ 'code_backend_theme/static/src/scss/datetimepicker.scss', 'code_backend_theme/static/src/scss/theme.scss', 'code_backend_theme/static/src/scss/sidebar.scss', - ('replace', '/web/static/src/views/graph/colors.js', '/code_backend_theme/static/src/js/fields/colors.js'), - ('replace', '/web/static/src/views/graph/graph_renderer.js', '/code_backend_theme/static/src/js/fields/graph_renderer.js'), - ('replace', '/web/static/src/views/graph/graph_model.js', '/code_backend_theme/static/src/js/fields/graph_model.js'), - ('replace', '/web/static/src/views/graph/graph_arch_parser.js', '/code_backend_theme/static/src/js/fields/graph_arch_parser.js'), - ('replace', '/web/static/src/views/graph/graph_view.js', '/code_backend_theme/static/src/js/fields/graph_view.js'), + + 'code_backend_theme/static/src/js/fields/colors.js', 'code_backend_theme/static/src/js/chrome/sidebar_menu.js', ], 'web.assets_qweb': [ diff --git a/code_backend_theme/static/src/js/fields/colors.js b/code_backend_theme/static/src/js/fields/colors.js index 9c7a4f015..371c29d42 100644 --- a/code_backend_theme/static/src/js/fields/colors.js +++ b/code_backend_theme/static/src/js/fields/colors.js @@ -1,32 +1,10 @@ /** @odoo-module **/ +import { COLORS } from "@web/views/graph/colors"; -export const COLORS = ["#556ee6", "#f1b44c", "#50a5f1", "#ffbb78", "#34c38f", "#98df8a", "#d62728", +var code_backend_color = ["#556ee6", "#f1b44c", "#50a5f1", "#ffbb78", "#34c38f", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"]; -/** - * @param {number} index - * @returns {string} - */ -export function getColor(index) { - return COLORS[index % COLORS.length]; -} - -export const DEFAULT_BG = "#d3d3d3"; - -export const BORDER_WHITE = "rgba(255,255,255,0.6)"; - -const RGB_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; - -/** - * @param {string} hex - * @param {number} opacity - * @returns {string} - */ -export function hexToRGBA(hex, opacity) { - const rgb = RGB_REGEX.exec(hex) - .slice(1, 4) - .map((n) => parseInt(n, 16)) - .join(","); - return `rgba(${rgb},${opacity})`; +for (let i=0;i<=code_backend_color.length;i++){ + COLORS[i] = code_backend_color[i] } diff --git a/code_backend_theme/static/src/js/fields/graph_arch_parser.js b/code_backend_theme/static/src/js/fields/graph_arch_parser.js deleted file mode 100644 index 5e4e28d27..000000000 --- a/code_backend_theme/static/src/js/fields/graph_arch_parser.js +++ /dev/null @@ -1,82 +0,0 @@ -/** @odoo-module **/ - -import { evaluateExpr } from "@web/core/py_js/py"; -import { GROUPABLE_TYPES } from "@web/search/utils/misc"; -import { XMLParser } from "@web/core/utils/xml"; -import { archParseBoolean } from "@web/views/helpers/utils"; - -const MODES = ["bar", "line", "pie"]; -const ORDERS = ["ASC", "DESC", null]; - -export class GraphArchParser extends XMLParser { - parse(arch, fields = {}) { - const archInfo = { fields, fieldAttrs: {}, groupBy: [] }; - this.visitXML(arch, (node) => { - switch (node.tagName) { - case "graph": { - if (node.hasAttribute("disable_linking")) { - archInfo.disableLinking = archParseBoolean( - node.getAttribute("disable_linking") - ); - } - if (node.hasAttribute("stacked")) { - archInfo.stacked = archParseBoolean(node.getAttribute("stacked")); - } - const mode = node.getAttribute("type"); - if (mode && MODES.includes(mode)) { - archInfo.mode = mode; - } - const order = node.getAttribute("order"); - if (order && ORDERS.includes(order)) { - archInfo.order = order; - } - const title = node.getAttribute("string"); - if (title) { - archInfo.title = title; - } - break; - } - case "field": { - let fieldName = node.getAttribute("name"); // exists (rng validation) - if (fieldName === "id") { - break; - } - const string = node.getAttribute("string"); - if (string) { - if (!archInfo.fieldAttrs[fieldName]) { - archInfo.fieldAttrs[fieldName] = {}; - } - archInfo.fieldAttrs[fieldName].string = string; - } - const isInvisible = Boolean( - evaluateExpr(node.getAttribute("invisible") || "0") - ); - if (isInvisible) { - if (!archInfo.fieldAttrs[fieldName]) { - archInfo.fieldAttrs[fieldName] = {}; - } - archInfo.fieldAttrs[fieldName].isInvisible = true; - break; - } - const isMeasure = node.getAttribute("type") === "measure"; - if (isMeasure) { - // the last field with type="measure" (if any) will be used as measure else __count - archInfo.measure = fieldName; - } else { - const { type } = archInfo.fields[fieldName]; // exists (rng validation) - if (GROUPABLE_TYPES.includes(type)) { - let groupBy = fieldName; - const interval = node.getAttribute("interval"); - if (interval) { - groupBy += `:${interval}`; - } - archInfo.groupBy.push(groupBy); - } - } - break; - } - } - }); - return archInfo; - } -} diff --git a/code_backend_theme/static/src/js/fields/graph_model.js b/code_backend_theme/static/src/js/fields/graph_model.js deleted file mode 100644 index d72f17898..000000000 --- a/code_backend_theme/static/src/js/fields/graph_model.js +++ /dev/null @@ -1,537 +0,0 @@ -/** @odoo-module **/ - -import { sortBy } from "@web/core/utils/arrays"; -import { KeepLast, Race } from "@web/core/utils/concurrency"; -import { rankInterval } from "@web/search/utils/dates"; -import { getGroupBy } from "@web/search/utils/group_by"; -import { GROUPABLE_TYPES } from "@web/search/utils/misc"; -import { Model } from "@web/views/helpers/model"; -import { computeReportMeasures, processMeasure } from "@web/views/helpers/utils"; - -export const SEP = " / "; - -/** - * @typedef {import("@web/search/search_model").SearchParams} SearchParams - */ - -class DateClasses { - // We view the param "array" as a matrix of values and undefined. - // An equivalence class is formed of defined values of a column. - // So nothing has to do with dates but we only use Dateclasses to manage - // identification of dates. - /** - * @param {(any[])[]} array - */ - constructor(array) { - this.__referenceIndex = null; - this.__array = array; - for (let i = 0; i < this.__array.length; i++) { - const arr = this.__array[i]; - if (arr.length && this.__referenceIndex === null) { - this.__referenceIndex = i; - } - } - } - - /** - * @param {number} index - * @param {any} o - * @returns {string} - */ - classLabel(index, o) { - return `${this.__array[index].indexOf(o)}`; - } - - /** - * @param {string} classLabel - * @returns {any[]} - */ - classMembers(classLabel) { - const classNumber = Number(classLabel); - const classMembers = new Set(); - for (const arr of this.__array) { - if (arr[classNumber] !== undefined) { - classMembers.add(arr[classNumber]); - } - } - return [...classMembers]; - } - - /** - * @param {string} classLabel - * @param {number} [index] - * @returns {any} - */ - representative(classLabel, index) { - const classNumber = Number(classLabel); - const i = index === undefined ? this.__referenceIndex : index; - if (i === null) { - return null; - } - return this.__array[i][classNumber]; - } - - /** - * @param {number} index - * @returns {number} - */ - arrayLength(index) { - return this.__array[index].length; - } -} - -export class GraphModel extends Model { - /** - * @override - */ - setup(params) { - // concurrency management - this.keepLast = new KeepLast(); - this.race = new Race(); - const _fetchDataPoints = this._fetchDataPoints.bind(this); - this._fetchDataPoints = (...args) => { - return this.race.add(_fetchDataPoints(...args)); - }; - - this.initialGroupBy = null; - - this.metaData = params; - this.data = null; - this.searchParams = null; - } - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @param {SearchParams} searchParams - */ - async load(searchParams) { - this.searchParams = searchParams; - if (!this.initialGroupBy) { - this.initialGroupBy = searchParams.context.graph_groupbys || this.metaData.groupBy; // = arch groupBy --> change that - } - const metaData = this._buildMetaData(); - return this._fetchDataPoints(metaData); - } - - /** - * @override - */ - hasData() { - return this.dataPoints.length > 0; - } - - /** - * Only supposed to be called to change one or several parameters among - * "measure", "mode", "order", and "stacked". - * @param {Object} params - */ - async updateMetaData(params) { - if ("measure" in params) { - const metaData = this._buildMetaData(params); - await this._fetchDataPoints(metaData); - } else { - await this.race.getCurrentProm(); - this.metaData = Object.assign({}, this.metaData, params); - this._prepareData(); - } - this.notify(); - } - - //-------------------------------------------------------------------------- - // Protected - //-------------------------------------------------------------------------- - - /** - * @protected - * @param {Object} [params={}] - * @returns {Object} - */ - _buildMetaData(params) { - const { comparison, domain, context, groupBy } = this.searchParams; - - const metaData = Object.assign({}, this.metaData, { context }); - if (comparison) { - metaData.domains = comparison.domains; - metaData.comparisonField = comparison.fieldName; - } else { - metaData.domains = [{ arrayRepr: domain, description: null }]; - } - metaData.measure = context.graph_measure || metaData.measure; - metaData.mode = context.graph_mode || metaData.mode; - metaData.groupBy = groupBy.length ? groupBy : this.initialGroupBy; - - this._normalize(metaData); - - metaData.measures = computeReportMeasures( - metaData.fields, - metaData.fieldAttrs, - [metaData.measure], - metaData.additionalMeasures - ); - - return Object.assign(metaData, params); - } - - /** - * Fetch the data points determined by the metaData. This function has - * several side effects. It can alter this.metaData and set this.dataPoints. - * @protected - * @param {Object} metaData - */ - async _fetchDataPoints(metaData) { - this.dataPoints = await this.keepLast.add(this._loadDataPoints(metaData)); - this.metaData = metaData; - this._prepareData(); - } - - /** - * Separates dataPoints coming from the read_group(s) into different - * datasets. This function returns the parameters data and labels used - * to produce the charts. - * @protected - * @param {Object[]} - * @returns {Object} - */ - _getData(dataPoints) { - const { comparisonField, groupBy, mode } = this.metaData; - - let identify = false; - if (comparisonField && groupBy.length && groupBy[0].fieldName === comparisonField) { - identify = true; - } - const dateClasses = identify ? this._getDateClasses(dataPoints) : null; - - // dataPoints --> labels - let labels = []; - const labelMap = {}; - for (const dataPt of dataPoints) { - const x = dataPt.labels.slice(0, mode === "pie" ? undefined : 1); - const trueLabel = x.length ? x.join(SEP) : this.env._t("Total"); - if (dateClasses) { - x[0] = dateClasses.classLabel(dataPt.originIndex, x[0]); - } - const key = JSON.stringify(x); - if (labelMap[key] === undefined) { - labelMap[key] = labels.length; - if (dateClasses) { - if (mode === "pie") { - x[0] = dateClasses.classMembers(x[0]).join(", "); - } else { - x[0] = dateClasses.representative(x[0]); - } - } - const label = x.length ? x.join(SEP) : this.env._t("Total"); - labels.push(label); - } - dataPt.labelIndex = labelMap[key]; - dataPt.trueLabel = trueLabel; - } - - // dataPoints + labels --> datasetsTmp --> datasets - const datasetsTmp = {}; - for (const dataPt of dataPoints) { - const { domain, labelIndex, originIndex, trueLabel, value } = dataPt; - const datasetLabel = this._getDatasetLabel(dataPt); - if (!(datasetLabel in datasetsTmp)) { - let dataLength = labels.length; - if (mode !== "pie" && dateClasses) { - dataLength = dateClasses.arrayLength(originIndex); - } - datasetsTmp[datasetLabel] = { - data: new Array(dataLength).fill(0), - trueLabels: labels.slice(0, dataLength), // should be good // check this in case identify = true - domains: new Array(dataLength).fill([]), - label: datasetLabel, - originIndex: originIndex, - }; - } - datasetsTmp[datasetLabel].data[labelIndex] = value; - datasetsTmp[datasetLabel].domains[labelIndex] = domain; - datasetsTmp[datasetLabel].trueLabels[labelIndex] = trueLabel; - } - // sort by origin - let datasets = sortBy(Object.values(datasetsTmp), "originIndex"); - - if (mode === "pie") { - // We kinda have a matrix. We remove the zero columns and rows. This is a global operation. - // That's why it cannot be done before. - datasets = datasets.filter((dataset) => dataset.data.some((v) => Boolean(v))); - const labelsToKeepIndexes = {}; - labels.forEach((_, index) => { - if (datasets.some((dataset) => Boolean(dataset.data[index]))) { - labelsToKeepIndexes[index] = true; - } - }); - labels = labels.filter((_, index) => labelsToKeepIndexes[index]); - for (const dataset of datasets) { - dataset.data = dataset.data.filter((_, index) => labelsToKeepIndexes[index]); - dataset.domains = dataset.domains.filter((_, index) => labelsToKeepIndexes[index]); - dataset.trueLabels = dataset.trueLabels.filter( - (_, index) => labelsToKeepIndexes[index] - ); - } - } - - return { datasets, labels }; - } - - /** - * Determines the dataset to which the data point belongs. - * @protected - * @param {Object} dataPoint - * @returns {string} - */ - _getDatasetLabel(dataPoint) { - const { measure, measures, domains, mode } = this.metaData; - const { labels, originIndex } = dataPoint; - if (mode === "pie") { - return domains[originIndex].description || ""; - } - // ([origin] + second to last groupBys) or measure - let datasetLabel = labels.slice(1).join(SEP); - if (domains.length > 1) { - datasetLabel = - domains[originIndex].description + (datasetLabel ? SEP + datasetLabel : ""); - } - datasetLabel = datasetLabel || measures[measure].string; - return datasetLabel; - } - - /** - * @protected - * @param {Object[]} dataPoints - * @returns {DateClasses} - */ - _getDateClasses(dataPoints) { - const { domains } = this.metaData; - const dateSets = domains.map(() => new Set()); - for (const { labels, originIndex } of dataPoints) { - const date = labels[0]; - dateSets[originIndex].add(date); - } - const arrays = dateSets.map((dateSet) => [...dateSet]); - return new DateClasses(arrays); - } - - /** - * Eventually filters and sort data points. - * @protected - * @returns {Object[]} - */ - _getProcessedDataPoints() { - const { domains, groupBy, mode, order } = this.metaData; - let processedDataPoints = []; - if (mode === "line") { - processedDataPoints = this.dataPoints.filter( - (dataPoint) => dataPoint.labels[0] !== this.env._t("Undefined") - ); - } else { - processedDataPoints = this.dataPoints.filter((dataPoint) => dataPoint.count !== 0); - } - - if (order !== null && mode !== "pie" && domains.length === 1 && groupBy.length > 0) { - // group data by their x-axis value, and then sort datapoints - // based on the sum of values by group in ascending/descending order - const groupedDataPoints = {}; - for (const dataPt of processedDataPoints) { - const key = dataPt.labels[0]; // = x-axis value under the current assumptions - if (!groupedDataPoints[key]) { - groupedDataPoints[key] = []; - } - groupedDataPoints[key].push(dataPt); - } - const groups = Object.values(groupedDataPoints); - const groupTotal = (group) => group.reduce((sum, dataPt) => sum + dataPt.value, 0); - processedDataPoints = sortBy(groups, groupTotal, order.toLowerCase()).flat(); - } - - return processedDataPoints; - } - - /** - * Determines whether the set of data points is good. If not, this.data will be (re)set to null - * @protected - * @param {Object[]} - * @returns {boolean} - */ - _isValidData(dataPoints) { - const { mode } = this.metaData; - let somePositive = false; - let someNegative = false; - if (mode === "pie") { - for (const dataPt of dataPoints) { - if (dataPt.value > 0) { - somePositive = true; - } else if (dataPt.value < 0) { - someNegative = true; - } - } - if (someNegative && somePositive) { - return false; - } - } - return true; - } - - /** - * Fetch and process graph data. It is basically a(some) read_group(s) - * with correct fields for each domain. We have to do some light processing - * to separate date groups in the field list, because they can be defined - * with an aggregation function, such as my_date:week. - * @protected - * @param {Object} metaData - * @returns {Object[]} - */ - async _loadDataPoints(metaData) { - const { measure, domains, fields, groupBy, resModel } = metaData; - - const measures = ["__count"]; - if (measure !== "__count") { - let { group_operator, type } = fields[measure]; - if (type === "many2one") { - group_operator = "count_distinct"; - } - if (group_operator === undefined) { - throw new Error( - `No aggregate function has been provided for the measure '${measure}'` - ); - } - measures.push(`${measure}:${group_operator}`); - } - - const proms = []; - const numbering = {}; // used to avoid ambiguity with many2one with values with same labels: - // for instance [1, "ABC"] [3, "ABC"] should be distinguished. - domains.forEach((domain, originIndex) => { - proms.push( - this.orm - .webReadGroup( - resModel, - domain.arrayRepr, - measures, - groupBy.map((gb) => gb.spec), - { lazy: false }, // what is this thing??? - { fill_temporal: true, ...this.searchParams.context } - ) - .then((data) => { - const dataPoints = []; - for (const group of data.groups) { - const { __domain, __count } = group; - const labels = []; - - for (const gb of groupBy) { - let label; - const val = group[gb.spec]; - const fieldName = gb.fieldName; - const { type } = fields[fieldName]; - if (type === "boolean") { - label = `${val}`; // toUpperCase? - } else if (val === false) { - label = this.env._t("Undefined"); - } else if (type === "many2one") { - const [id, name] = val; - const key = JSON.stringify([fieldName, name]); - if (!numbering[key]) { - numbering[key] = {}; - } - const numbers = numbering[key]; - if (!numbers[id]) { - numbers[id] = Object.keys(numbers).length + 1; - } - const num = numbers[id]; - label = num === 1 ? name : `${name} (${num})`; - } else if (type === "selection") { - const selected = fields[fieldName].selection.find( - (s) => s[0] === val - ); - label = selected[1]; - } else { - label = val; - } - labels.push(label); - } - - let value = group[measure]; - if (value instanceof Array) { - // case where measure is a many2one and is used as groupBy - value = 1; - } - if (!Number.isInteger(value)) { - metaData.allIntegers = false; - } - dataPoints.push({ - count: __count, - domain: __domain, - value, - labels, - originIndex, - }); - } - return dataPoints; - }) - ); - }); - const promResults = await Promise.all(proms); - return promResults.flat(); - } - - /** - * Process metaData.groupBy in order to keep only the finest interval option for - * elements based on date/datetime field (e.g. 'date:year'). This means that - * 'week' is prefered to 'month'. The field stays at the place of its first occurence. - * For instance, - * ['foo', 'date:month', 'bar', 'date:week'] becomes ['foo', 'date:week', 'bar']. - * @protected - * @param {Object} metaData - */ - _normalize(metaData) { - const { fields } = metaData; - const groupBy = []; - for (const gb of metaData.groupBy) { - let ngb = gb; - if (typeof gb === "string") { - ngb = getGroupBy(gb, fields); - } - groupBy.push(ngb); - } - - const processedGroupBy = []; - for (const gb of groupBy) { - const { fieldName, interval } = gb; - const { store, type } = fields[fieldName]; - if ( - !store || - ["id", "__count"].includes(fieldName) || - !GROUPABLE_TYPES.includes(type) - ) { - continue; - } - const index = processedGroupBy.findIndex((gb) => gb.fieldName === fieldName); - if (index === -1) { - processedGroupBy.push(gb); - } else if (interval) { - const registeredInterval = processedGroupBy[index].interval; - if (rankInterval(registeredInterval) < rankInterval(interval)) { - processedGroupBy.splice(index, 1, gb); - } - } - } - metaData.groupBy = processedGroupBy; - - metaData.measure = processMeasure(metaData.measure); - } - - /** - * @protected - */ - async _prepareData() { - const processedDataPoints = this._getProcessedDataPoints(); - this.data = null; - if (this._isValidData(processedDataPoints)) { - this.data = this._getData(processedDataPoints); - } - } -} diff --git a/code_backend_theme/static/src/js/fields/graph_renderer.js b/code_backend_theme/static/src/js/fields/graph_renderer.js deleted file mode 100644 index 1c06b3eee..000000000 --- a/code_backend_theme/static/src/js/fields/graph_renderer.js +++ /dev/null @@ -1,627 +0,0 @@ -/** @odoo-module **/ - -import { _lt } from "@web/core/l10n/translation"; -import { BORDER_WHITE, DEFAULT_BG, getColor, hexToRGBA } from "./colors"; -import { formatFloat } from "@web/fields/formatters"; -import { SEP } from "./graph_model"; -import { sortBy } from "@web/core/utils/arrays"; -import { useAssets } from "@web/core/assets"; -import { useEffect } from "@web/core/utils/hooks"; - -const { Component, hooks } = owl; -const { useRef } = hooks; - -const NO_DATA = _lt("No data"); - -/** - * @param {Object} chartArea - * @returns {string} - */ -function getMaxWidth(chartArea) { - const { left, right } = chartArea; - return Math.floor((right - left) / 1.618) + "px"; -} - -/** - * Used to avoid too long legend items. - * @param {string|Strin} label - * @returns {string} shortened version of the input label - */ -function shortenLabel(label) { - // string returned could be wrong if a groupby value contain a " / "! - const groups = label.toString().split(SEP); - let shortLabel = groups.slice(0, 3).join(SEP); - if (shortLabel.length > 30) { - shortLabel = `${shortLabel.slice(0, 30)}...`; - } else if (groups.length > 3) { - shortLabel = `${shortLabel}${SEP}...`; - } - return shortLabel; -} - -export class GraphRenderer extends Component { - setup() { - this.model = this.props.model; - - this.canvasRef = useRef("canvas"); - this.containerRef = useRef("container"); - - this.chart = null; - this.tooltip = null; - this.legendTooltip = null; - - useAssets({ jsLibs: ["/web/static/lib/Chart/Chart.js"] }); - - useEffect(() => this.renderChart()); - } - - willUnmount() { - if (this.chart) { - this.chart.destroy(); - } - } - - /** - * This function aims to remove a suitable number of lines from the - * tooltip in order to make it reasonably visible. A message indicating - * the number of lines is added if necessary. - * @param {HTMLElement} tooltip - * @param {number} maxTooltipHeight this the max height in pixels of the tooltip - */ - adjustTooltipHeight(tooltip, maxTooltipHeight) { - const sizeOneLine = tooltip.querySelector("tbody tr").clientHeight; - const tbodySize = tooltip.querySelector("tbody").clientHeight; - const toKeep = Math.max( - 0, - Math.floor((maxTooltipHeight - (tooltip.clientHeight - tbodySize)) / sizeOneLine) - 1 - ); - const lines = tooltip.querySelectorAll("tbody tr"); - const toRemove = lines.length - toKeep; - if (toRemove > 0) { - for (let index = toKeep; index < lines.length; ++index) { - lines[index].remove(); - } - const tr = document.createElement("tr"); - const td = document.createElement("td"); - tr.classList.add("o_show_more"); - td.innerText = this.env._t("..."); - tr.appendChild(td); - tooltip.querySelector("tbody").appendChild(tr); - } - } - - /** - * Creates a custom HTML tooltip. - * @param {Object} data - * @param {Object} metaData - * @param {Object} tooltipModel see chartjs documentation - */ - customTooltip(data, metaData, tooltipModel) { - const { measure, measures, disableLinking, mode } = metaData; - this.el.style.cursor = ""; - this.removeTooltips(); - if (tooltipModel.opacity === 0 || tooltipModel.dataPoints.length === 0) { - return; - } - if (!disableLinking && mode !== "line") { - this.el.style.cursor = "pointer"; - } - const chartAreaTop = this.chart.chartArea.top; - const viewContentTop = this.el.getBoundingClientRect().top; - const innerHTML = this.env.qweb.renderToString("web.GraphRenderer.CustomTooltip", { - maxWidth: getMaxWidth(this.chart.chartArea), - measure: measures[measure].string, - tooltipItems: this.getTooltipItems(data, metaData, tooltipModel), - }); - const template = Object.assign(document.createElement("template"), { innerHTML }); - const tooltip = template.content.firstChild; - this.containerRef.el.prepend(tooltip); - - let top; - const tooltipHeight = tooltip.clientHeight; - const minTopAllowed = Math.floor(chartAreaTop); - const maxTopAllowed = Math.floor(window.innerHeight - (viewContentTop + tooltipHeight)) - 2; - const y = Math.floor(tooltipModel.y); - if (minTopAllowed <= maxTopAllowed) { - // Here we know that the full tooltip can fit in the screen. - // We put it in the position where Chart.js would put it - // if two conditions are respected: - // 1: the tooltip is not cut (because we know it is possible to not cut it) - // 2: the tooltip does not hide the legend. - // If it is not possible to use the Chart.js proposition (y) - // we use the best approximated value. - if (y <= maxTopAllowed) { - if (y >= minTopAllowed) { - top = y; - } else { - top = minTopAllowed; - } - } else { - top = maxTopAllowed; - } - } else { - // Here we know that we cannot satisfy condition 1 above, - // so we position the tooltip at the minimal position and - // cut it the minimum possible. - top = minTopAllowed; - const maxTooltipHeight = window.innerHeight - (viewContentTop + chartAreaTop) - 2; - this.adjustTooltipHeight(tooltip, maxTooltipHeight); - } - this.fixTooltipLeftPosition(tooltip, tooltipModel.x); - tooltip.style.top = Math.floor(top) + "px"; - - this.tooltip = tooltip; - } - - /** - * Sets best left position of a tooltip approaching the proposal x. - * @param {HTMLElement} tooltip - * @param {number} x - */ - fixTooltipLeftPosition(tooltip, x) { - let left; - const tooltipWidth = tooltip.clientWidth; - const minLeftAllowed = Math.floor(this.chart.chartArea.left + 2); - const maxLeftAllowed = Math.floor(this.chart.chartArea.right - tooltipWidth - 2); - x = Math.floor(x); - if (x < minLeftAllowed) { - left = minLeftAllowed; - } else if (x > maxLeftAllowed) { - left = maxLeftAllowed; - } else { - left = x; - } - tooltip.style.left = `${left}px`; - } - - /** - * Used to format correctly the values in tooltips and yAxes. - * @param {number} value - * @param {boolean} [allIntegers=true] - * @returns {string} - */ - formatValue(value, allIntegers = true) { - const largeNumber = Math.abs(value) >= 1000; - if (allIntegers && !largeNumber) { - return String(value); - } - if (largeNumber) { - return formatFloat(value, { humanReadable: true, decimals: 2, minDigits: 1 }); - } - return formatFloat(value); - } - - /** - * Returns the bar chart data - * @returns {Object} - */ - getBarChartData() { - // style data - const { domains, stacked } = this.model.metaData; - const data = this.model.data; - for (let index = 0; index < data.datasets.length; ++index) { - const dataset = data.datasets[index]; - // used when stacked - if (stacked) { - dataset.stack = domains[dataset.originIndex].description || ""; - } - // set dataset color - dataset.backgroundColor = getColor(index); - } - - return data; - } - - /** - * Returns the chart config. - * @returns {Object} - */ - getChartConfig() { - const { mode } = this.model.metaData; - let data; - switch (mode) { - case "bar": - data = this.getBarChartData(); - break; - case "line": - data = this.getLineChartData(); - break; - case "pie": - data = this.getPieChartData(); - } - const options = this.prepareOptions(); - return { data, options, type: mode }; - } - - /** - * Returns an object used to style chart elements independently from - * the datasets. - * @returns {Object} - */ - getElementOptions() { - const { mode } = this.model.metaData; - const elementOptions = {}; - if (mode === "bar") { - elementOptions.rectangle = { borderWidth: 1 }; - } else if (mode === "line") { - elementOptions.line = { fill: false, tension: 0 }; - } - return elementOptions; - } - - /** - * @returns {Object} - */ - getLegendOptions() { - const { mode } = this.model.metaData; - const data = this.model.data; - const refLength = mode === "pie" ? data.labels.length : data.datasets.length; - const legendOptions = { - display: refLength <= 20, - position: "top", - onHover: this.onlegendHover.bind(this), - onLeave: this.onLegendLeave.bind(this), - }; - if (mode === "line") { - legendOptions.onClick = this.onLegendClick.bind(this); - } - if (mode === "pie") { - legendOptions.labels = { - generateLabels: (chart) => { - const { data } = chart; - const metaData = data.datasets.map( - (_, index) => chart.getDatasetMeta(index).data - ); - const labels = data.labels.map((label, index) => { - const hidden = metaData.some((data) => data[index] && data[index].hidden); - const fullText = label; - const text = shortenLabel(fullText); - const fillStyle = label === NO_DATA ? DEFAULT_BG : getColor(index); - return { text, fullText, fillStyle, hidden, index }; - }); - return labels; - }, - }; - } else { - const referenceColor = mode === "bar" ? "backgroundColor" : "borderColor"; - legendOptions.labels = { - generateLabels: (chart) => { - const { data } = chart; - const labels = data.datasets.map((dataset, index) => { - return { - text: shortenLabel(dataset.label), - fullText: dataset.label, - fillStyle: dataset[referenceColor], - hidden: !chart.isDatasetVisible(index), - lineCap: dataset.borderCapStyle, - lineDash: dataset.borderDash, - lineDashOffset: dataset.borderDashOffset, - lineJoin: dataset.borderJoinStyle, - lineWidth: dataset.borderWidth, - strokeStyle: dataset[referenceColor], - pointStyle: dataset.pointStyle, - datasetIndex: index, - }; - }); - return labels; - }, - }; - } - return legendOptions; - } - - /** - * Returns line chart data. - * @returns {Object} - */ - getLineChartData() { - const { groupBy, domains } = this.model.metaData; - const data = this.model.data; - for (let index = 0; index < data.datasets.length; ++index) { - const dataset = data.datasets[index]; - if (groupBy.length <= 1 && domains.length > 1) { - if (dataset.originIndex === 0) { - dataset.fill = "origin"; - dataset.backgroundColor = hexToRGBA(getColor(0), 0.4); - dataset.borderColor = getColor(0); - } else if (dataset.originIndex === 1) { - dataset.borderColor = getColor(1); - } else { - dataset.borderColor = getColor(index); - } - } else { - dataset.borderColor = getColor(index); - } - if (data.labels.length === 1) { - // shift of the real value to right. This is done to - // center the points in the chart. See data.labels below in - // Chart parameters - dataset.data.unshift(undefined); - dataset.trueLabels.unshift(undefined); - dataset.domains.unshift(undefined); - } - dataset.pointBackgroundColor = dataset.borderColor; - dataset.pointBorderColor = "rgba(0,0,0,0.2)"; - } - if (data.datasets.length === 1) { - const dataset = data.datasets[0]; - dataset.fill = "origin"; - dataset.backgroundColor = hexToRGBA(getColor(0), 0.4); - } - // center the points in the chart (without that code they are put - // on the left and the graph seems empty) - data.labels = data.labels.length > 1 ? data.labels : ["", ...data.labels, ""]; - - return data; - } - - /** - * Returns pie chart data. - * @returns {Object} - */ - getPieChartData() { - const { domains } = this.model.metaData; - const data = this.model.data; - // style/complete data - // give same color to same groups from different origins - const colors = data.labels.map((_, index) => getColor(index)); - for (const dataset of data.datasets) { - dataset.backgroundColor = colors; - dataset.borderColor = BORDER_WHITE; - } - // make sure there is a zone associated with every origin - const representedOriginIndexes = new Set( - data.datasets.map((dataset) => dataset.originIndex) - ); - let addNoDataToLegend = false; - const fakeData = new Array(data.labels.length + 1); - fakeData[data.labels.length] = 1; - const fakeTrueLabels = new Array(data.labels.length + 1); - fakeTrueLabels[data.labels.length] = NO_DATA; - for (let index = 0; index < domains.length; ++index) { - if (!representedOriginIndexes.has(index)) { - data.datasets.push({ - label: domains[index].description, - data: fakeData, - trueLabels: fakeTrueLabels, - backgroundColor: [...colors, DEFAULT_BG], - borderColor: BORDER_WHITE, - }); - addNoDataToLegend = true; - } - } - if (addNoDataToLegend) { - data.labels.push(NO_DATA); - } - - return data; - } - - /** - * Returns the options used to generate the chart axes. - * @returns {Object} - */ - getScaleOptions() { - const { - allIntegers, - displayScaleLabels, - fields, - groupBy, - measure, - measures, - mode, - } = this.model.metaData; - if (mode === "pie") { - return {}; - } - const xAxe = { - type: "category", - scaleLabel: { - display: Boolean(groupBy.length && displayScaleLabels), - labelString: groupBy.length ? fields[groupBy[0].fieldName].string : "", - }, - }; - const yAxe = { - type: "linear", - scaleLabel: { - display: displayScaleLabels, - labelString: measures[measure].string, - }, - ticks: { - callback: (value) => this.formatValue(value, allIntegers), - suggestedMax: 0, - suggestedMin: 0, - }, - }; - return { xAxes: [xAxe], yAxes: [yAxe] }; - } - - /** - * This function extracts the information from the data points in - * tooltipModel.dataPoints (corresponding to datapoints over a given - * label determined by the mouse position) that will be displayed in a - * custom tooltip. - * @param {Object} data - * @param {Object} metaData - * @param {Object} tooltipModel see chartjs documentation - * @returns {Object[]} - */ - getTooltipItems(data, metaData, tooltipModel) { - const { allIntegers, domains, mode, groupBy } = metaData; - const sortedDataPoints = sortBy(tooltipModel.dataPoints, "yLabel", "desc"); - const items = []; - for (const item of sortedDataPoints) { - const id = item.index; - const dataset = data.datasets[item.datasetIndex]; - let label = dataset.trueLabels[id]; - let value = this.formatValue(dataset.data[id], allIntegers); - let boxColor; - if (mode === "pie") { - if (label === NO_DATA) { - value = this.formatValue(0, allIntegers); - } - if (domains.length > 1) { - label = `${dataset.label} / ${label}`; - } - boxColor = dataset.backgroundColor[id]; - } else { - if (groupBy.length > 1 || domains.length > 1) { - label = `${label} / ${dataset.label}`; - } - boxColor = mode === "bar" ? dataset.backgroundColor : dataset.borderColor; - } - items.push({ id, label, value, boxColor }); - } - return items; - } - - /** - * Returns the options used to generate chart tooltips. - * @returns {Object} - */ - getTooltipOptions() { - const { data, metaData } = this.model; - const { mode } = metaData; - const tooltipOptions = { - enabled: false, - custom: this.customTooltip.bind(this, data, metaData), - }; - if (mode === "line") { - tooltipOptions.mode = "index"; - tooltipOptions.intersect = false; - } - return tooltipOptions; - } - - /** - * If a group has been clicked on, display a view of its records. - * @param {MouseEvent} ev - */ - onGraphClicked(ev) { - const [activeElement] = this.chart.getElementAtEvent(ev); - if (!activeElement) { - return; - } - const { _datasetIndex, _index } = activeElement; - const { domains } = this.chart.data.datasets[_datasetIndex]; - if (domains) { - this.props.onGraphClicked(domains[_index]); - } - } - - /** - * Overrides the default legend 'onClick' behaviour. This is done to - * remove all existing tooltips right before updating the chart. - * @param {Event} ev - * @param {Object} legendItem - */ - onLegendClick(ev, legendItem) { - this.removeTooltips(); - // Default 'onClick' fallback. See web/static/lib/Chart/Chart.js#15138 - const index = legendItem.datasetIndex; - const meta = this.chart.getDatasetMeta(index); - meta.hidden = meta.hidden === null ? !this.chart.data.datasets[index].hidden : null; - this.chart.update(); - } - - /** - * If the text of a legend item has been shortened and the user mouse - * hovers that item (actually the event type is mousemove), a tooltip - * with the item full text is displayed. - * @param {Event} ev - * @param {Object} legendItem - */ - onlegendHover(ev, legendItem) { - this.canvasRef.el.style.cursor = "pointer"; - /** - * The string legendItem.text is an initial segment of legendItem.fullText. - * If the two coincide, no need to generate a tooltip. If a tooltip - * for the legend already exists, it is already good and does not - * need to be recreated. - */ - const { fullText, text } = legendItem; - if (this.legendTooltip || text === fullText) { - return; - } - const viewContentTop = this.el.getBoundingClientRect().top; - const legendTooltip = Object.assign(document.createElement("div"), { - className: "o_tooltip_legend", - innerText: fullText, - }); - legendTooltip.style.top = `${ev.clientY - viewContentTop}px`; - legendTooltip.style.maxWidth = getMaxWidth(this.chart.chartArea); - this.containerRef.el.appendChild(legendTooltip); - this.fixTooltipLeftPosition(legendTooltip, ev.clientX); - this.legendTooltip = legendTooltip; - } - - /** - * If there's a legend tooltip and the user mouse out of the - * corresponding legend item, the tooltip is removed. - */ - onLegendLeave() { - this.canvasRef.el.style.cursor = ""; - this.removeLegendTooltip(); - } - - /** - * Prepares options for the chart according to the current mode - * (= chart type). This function returns the parameter options used to - * instantiate the chart. - */ - prepareOptions() { - const { disableLinking, mode } = this.model.metaData; - const options = { - maintainAspectRatio: false, - scales: this.getScaleOptions(), - legend: this.getLegendOptions(), - tooltips: this.getTooltipOptions(), - elements: this.getElementOptions(), - }; - if (!disableLinking && mode !== "line") { - options.onClick = this.onGraphClicked.bind(this); - } - return options; - } - - /** - * Removes the legend tooltip (if any). - */ - removeLegendTooltip() { - if (this.legendTooltip) { - this.legendTooltip.remove(); - this.legendTooltip = null; - } - } - - /** - * Removes all existing tooltips (if any). - */ - removeTooltips() { - if (this.tooltip) { - this.tooltip.remove(); - this.tooltip = null; - } - this.removeLegendTooltip(); - } - - /** - * Instantiates a Chart (Chart.js lib) to render the graph according to - * the current config. - */ - renderChart() { - if (this.chart) { - this.chart.destroy(); - } - const config = this.getChartConfig(); - this.chart = new Chart(this.canvasRef.el, config); - // To perform its animations, ChartJS will perform each animation - // step in the next animation frame. The initial rendering itself - // is delayed for consistency. We can avoid this by manually - // advancing the animation service. - Chart.animationService.advance(); - } -} - -GraphRenderer.template = "web.GraphRenderer"; -GraphRenderer.props = ["model", "onGraphClicked"]; diff --git a/code_backend_theme/static/src/js/fields/graph_view.js b/code_backend_theme/static/src/js/fields/graph_view.js deleted file mode 100644 index da9e306b2..000000000 --- a/code_backend_theme/static/src/js/fields/graph_view.js +++ /dev/null @@ -1,160 +0,0 @@ -/** @odoo-module **/ - -import { _lt } from "@web/core/l10n/translation"; -import { registry } from "@web/core/registry"; -import { useService } from "@web/core/utils/hooks"; -import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu"; -import { standardViewProps } from "@web/views/helpers/standard_view_props"; -import { useSetupView } from "@web/views/helpers/view_hook"; -import { Layout } from "@web/views/layout"; -import { useModel } from "@web/views/helpers/model"; -import { GraphArchParser } from "./graph_arch_parser"; -import { GraphModel } from "./graph_model"; -import { GraphRenderer } from "./graph_renderer"; - -const viewRegistry = registry.category("views"); - -const { Component } = owl; - -export class GraphView extends Component { - setup() { - this.actionService = useService("action"); - - let modelParams; - if (this.props.state) { - modelParams = this.props.state.metaData; - } else { - const { arch, fields } = this.props; - const parser = new this.constructor.ArchParser(); - const archInfo = parser.parse(arch, fields); - modelParams = { - additionalMeasures: this.props.additionalMeasures, - disableLinking: Boolean(archInfo.disableLinking), - displayScaleLabels: this.props.displayScaleLabels, - fieldAttrs: archInfo.fieldAttrs, - fields: this.props.fields, - groupBy: archInfo.groupBy, - measure: archInfo.measure || "__count", - mode: archInfo.mode || "bar", - order: archInfo.order || null, - resModel: this.props.resModel, - stacked: "stacked" in archInfo ? archInfo.stacked : true, - title: archInfo.title || this.env._t("Untitled"), - }; - } - - this.model = useModel(this.constructor.Model, modelParams); - - useSetupView({ - getLocalState: () => { - return { metaData: this.model.metaData }; - }, - getContext: () => this.getContext(), - }); - } - - /** - * @returns {Object} - */ - getContext() { - // expand context object? change keys? - const { measure, groupBy, mode } = this.model.metaData; - return { - graph_measure: measure, - graph_mode: mode, - graph_groupbys: groupBy.map((gb) => gb.spec), - }; - } - - /** - * @param {string} domain the domain of the clicked area - */ - onGraphClicked(domain) { - const { context, resModel, title } = this.model.metaData; - - const views = {}; - for (const [viewId, viewType] of this.env.config.views || []) { - views[viewType] = viewId; - } - function getView(viewType) { - return [views[viewType] || false, viewType]; - } - const actionViews = [getView("list"), getView("form")]; - - this.actionService.doAction( - { - context, - domain, - name: title, - res_model: resModel, - target: "current", - type: "ir.actions.act_window", - views: actionViews, - }, - { - viewType: "list", - } - ); - } - - /** - * @param {CustomEvent} ev - */ - onMeasureSelected(ev) { - const { measure } = ev.detail.payload; - this.model.updateMetaData({ measure }); - } - - /** - * @param {"bar"|"line"|"pie"} mode - */ - onModeSelected(mode) { - this.model.updateMetaData({ mode }); - } - - /** - * @param {"ASC"|"DESC"} order - */ - toggleOrder(order) { - const { order: currentOrder } = this.model.metaData; - const nextOrder = currentOrder === order ? null : order; - this.model.updateMetaData({ order: nextOrder }); - } - - toggleStacked() { - const { stacked } = this.model.metaData; - this.model.updateMetaData({ stacked: !stacked }); - } -} - -GraphView.template = "web.GraphView"; -GraphView.buttonTemplate = "web.GraphView.Buttons"; - -GraphView.components = { GroupByMenu, Renderer: GraphRenderer, Layout }; - -GraphView.defaultProps = { - additionalMeasures: [], - displayGroupByMenu: false, - displayScaleLabels: true, -}; - -GraphView.props = { - ...standardViewProps, - additionalMeasures: { type: Array, elements: String, optional: true }, - displayGroupByMenu: { type: Boolean, optional: true }, - displayScaleLabels: { type: Boolean, optional: true }, -}; - -GraphView.type = "graph"; - -GraphView.display_name = _lt("Graph"); -GraphView.icon = "fa-bar-chart"; -GraphView.multiRecord = true; - -GraphView.Model = GraphModel; - -GraphView.ArchParser = GraphArchParser; - -GraphView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"]; - -viewRegistry.add("graph", GraphView); diff --git a/code_backend_theme/static/src/scss/login.scss b/code_backend_theme/static/src/scss/login.scss index 713b62768..5a32b1ed9 100644 --- a/code_backend_theme/static/src/scss/login.scss +++ b/code_backend_theme/static/src/scss/login.scss @@ -1,9 +1,9 @@ #wrapwrap > main { background: #f8f8fb; } -.navbar { - background: #fff !important; -} +// .navbar { +// background: #fff !important; +// } body { font-family: 'Poppins', sans-serif !important; }