Browse Source

[FIX] Bug Fixed 'code_backend_theme'

pull/195/head
Ajmal Cybro 4 years ago
parent
commit
51362ccb1d
  1. 9
      code_backend_theme/__manifest__.py
  2. 30
      code_backend_theme/static/src/js/fields/colors.js
  3. 82
      code_backend_theme/static/src/js/fields/graph_arch_parser.js
  4. 537
      code_backend_theme/static/src/js/fields/graph_model.js
  5. 627
      code_backend_theme/static/src/js/fields/graph_renderer.js
  6. 160
      code_backend_theme/static/src/js/fields/graph_view.js
  7. 6
      code_backend_theme/static/src/scss/login.scss

9
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': [

30
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]
}

82
code_backend_theme/static/src/js/fields/graph_arch_parser.js

@ -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;
}
}

537
code_backend_theme/static/src/js/fields/graph_model.js

@ -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);
}
}
}

627
code_backend_theme/static/src/js/fields/graph_renderer.js

@ -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"];

160
code_backend_theme/static/src/js/fields/graph_view.js

@ -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);

6
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;
}

Loading…
Cancel
Save