合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
440
web_grid/static/src/js/grid_renderer.js
Normal file
440
web_grid/static/src/js/grid_renderer.js
Normal file
@@ -0,0 +1,440 @@
|
||||
odoo.define('web_grid.GridRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
const AbstractRenderer = require('web.AbstractRendererOwl');
|
||||
const fieldUtils = require('web.field_utils');
|
||||
const utils = require('web.utils');
|
||||
|
||||
const gridComponentRegistry = require('web_grid.component_registry');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
|
||||
const { onPatched, onWillUpdateProps, useRef, useState } = owl;
|
||||
|
||||
class GridRenderer extends AbstractRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.root = useRef("root");
|
||||
this.state = useState({
|
||||
editMode: false,
|
||||
currentPath: "",
|
||||
errors: {},
|
||||
});
|
||||
this.currentInput = useRef("currentInput");
|
||||
useListener('mouseover', 'td:not(:first-child), th:not(:first-child)', this._onMouseEnter);
|
||||
useListener('mouseout', 'td:not(:first-child), th:not(:first-child)', this._onMouseLeave);
|
||||
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
onPatched(this.onPatched);
|
||||
}
|
||||
|
||||
onWillUpdateProps(nextProps) {
|
||||
if (nextProps.data[0].next.grid_anchor !== this.props.data[0].next.grid_anchor) {
|
||||
//if we change the range of dates we are looking at,
|
||||
//the cells should not be in error state anymore
|
||||
this.state.errors = {};
|
||||
}
|
||||
}
|
||||
onPatched() {
|
||||
if (this.currentInput.el) {
|
||||
this.currentInput.el.select();
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Getters
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* returns the columns of the first gridgroup
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
get columns() {
|
||||
return this.props.data.length ? this.props.data[0].cols : [];
|
||||
}
|
||||
/**
|
||||
* returns a boolean expressing if the grid uses cellComponents
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
get component() {
|
||||
return gridComponentRegistry.get(this.props.cellComponent);
|
||||
}
|
||||
get gridAnchorNext() {
|
||||
return this.props.data[0].next.grid_anchor;
|
||||
}
|
||||
/**
|
||||
* As there have to be a minimum of 5 rows in an ungrouped grid,
|
||||
* this will return the number of empty rows to add
|
||||
* if there are not enough.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
get emptyRows() {
|
||||
const rowLength = this.props.isGrouped ? this.props.data.reduce((count, d) => count + d.rows.length + 1, 0) : this.props.data[0].rows.length;
|
||||
return Array.from({
|
||||
length: Math.max(5 - rowLength, 0)
|
||||
}, (_, i) => i);
|
||||
}
|
||||
/**
|
||||
* get the formatType needed for format and parse
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get formatType() {
|
||||
if (this.hasComponent) {
|
||||
return this.component.formatType;
|
||||
}
|
||||
return this.props.fields[this.props.cellField].type;
|
||||
}
|
||||
/**
|
||||
* returns a boolean expressing if the grid uses cellComponents
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
get hasComponent() {
|
||||
return gridComponentRegistry.contains(this.props.cellComponent);
|
||||
}
|
||||
/**
|
||||
* Get the information needed to display the total of a grid correctly
|
||||
* will contain the classMap and the value
|
||||
*
|
||||
* @returns {classmap: Object, value: number}
|
||||
*/
|
||||
get gridTotal() {
|
||||
if (this.props.totals.super) {
|
||||
const classMap = {
|
||||
'o_grid_super': true,
|
||||
'text-danger': this.props.totals.super < 0,
|
||||
};
|
||||
const value = this.props.totals.super;
|
||||
return {
|
||||
classMap,
|
||||
value
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
classMap: {
|
||||
o_grid_super: true
|
||||
},
|
||||
value: 0.0
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* returns the getMeasureLabels
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get measureLabel() {
|
||||
if (this.props.measureLabel) {
|
||||
return _.str.sprintf("%s", this.props.measureLabel);
|
||||
} else {
|
||||
return this.env._t("Total");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* returns the xml of the noContentHelper
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get noContentHelper() {
|
||||
return utils.json_node_to_xml(this.props.noContentHelper);
|
||||
}
|
||||
/**
|
||||
* returns a boolean expressing if yes or no the noContentHelp should be shown
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
get showNoContentHelp() {
|
||||
const stateRow = Array.isArray(this.props.data) ? this.props.data.find(data => data.rows[0]) : this.props.data.rows[0];
|
||||
return stateRow === undefined && !!this.props.noContentHelp;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Notifies the model that a cell has been edited.
|
||||
*
|
||||
* @private
|
||||
* @param {string} path path to the cell
|
||||
* @param {Number} value new value that we want to store in DB
|
||||
* @param {Function} doneCallback function to call after update
|
||||
*/
|
||||
_cellEdited(path, value, doneCallback) {
|
||||
const cell_path = path.split('.');
|
||||
const grid_path = cell_path.slice(0, -3);
|
||||
const row_path = grid_path.concat(['rows'], cell_path.slice(-2, -1));
|
||||
const col_path = grid_path.concat(['cols'], cell_path.slice(-1));
|
||||
this.trigger('cell-edited', {
|
||||
cell_path,
|
||||
row_path,
|
||||
col_path,
|
||||
value,
|
||||
doneCallback,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {any} value
|
||||
* @returns {string}
|
||||
*/
|
||||
_format(value) {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
const cellField = this.props.fields[this.props.cellField];
|
||||
return fieldUtils.format[this.formatType](value, cellField, this.props.cellComponentOptions);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} index
|
||||
* @returns {value: number, smallerThanZero: boolean, muted: boolean}
|
||||
*/
|
||||
_formatCellContentTotals(index) {
|
||||
if (this.props.totals) {
|
||||
return {
|
||||
value: this.props.totals.columns[index],
|
||||
smallerThanZero: this.props.totals.columns[index] < 0,
|
||||
muted: !this.props.totals.columns || !this.props.totals.columns[index]
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} cell
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getCellClassMap(cell) {
|
||||
// these are "hard-set" for correct grid behaviour
|
||||
const classmap = {
|
||||
o_grid_cell_container: true,
|
||||
o_grid_cell_empty: !cell.size,
|
||||
o_grid_cell_readonly: !this.props.editableCells || cell.readonly,
|
||||
};
|
||||
// merge in class info from the cell
|
||||
for (const cls of cell.classes || []) {
|
||||
// don't allow overwriting initial values
|
||||
if (!(cls in classmap)) {
|
||||
classmap[cls] = true;
|
||||
}
|
||||
}
|
||||
return classmap;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {string} value
|
||||
* @returns {*}
|
||||
*/
|
||||
_parse(value) {
|
||||
const cellField = this.props.fields[this.props.cellField];
|
||||
return fieldUtils.parse[this.formatType](value, cellField, this.props.cellComponentOptions);
|
||||
}
|
||||
/**
|
||||
* measure the height value of footer cell if hasBarChartTotal="true"
|
||||
* max height value is 90%
|
||||
*
|
||||
* @private
|
||||
* @param {number} index
|
||||
* @returns {number} height: to be used as css percentage
|
||||
*/
|
||||
_totalHeight(index) {
|
||||
const maxCount = Math.max(...Object.values(this.props.totals.columns));
|
||||
const factor = maxCount ? (90 / maxCount) : 0;
|
||||
return factor * this.props.totals.columns[index];
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onClickCreateInline() {
|
||||
this.trigger('create-inline');
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param path path to the cell
|
||||
*/
|
||||
_onClickCellInformation(path) {
|
||||
this.state.editMode = false;
|
||||
this.trigger('open-cell-information', {
|
||||
path
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {string} path
|
||||
*/
|
||||
_onFocusComponent(path) {
|
||||
this.state.editMode = true;
|
||||
this.state.currentPath = path;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {string} path path to the cell
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onFocusGridCell(path) {
|
||||
this.state.editMode = true;
|
||||
this.state.currentPath = path;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} cell
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onGridInputBlur(ev) {
|
||||
this.state.editMode = false;
|
||||
let hasError = false;
|
||||
let value = ev.target.value;
|
||||
try {
|
||||
value = this._parse(value);
|
||||
} catch (_) {
|
||||
hasError = true;
|
||||
}
|
||||
const path = this.state.currentPath;
|
||||
if (hasError) {
|
||||
this.state.errors[path] = value;
|
||||
} else {
|
||||
delete this.state.errors[path];
|
||||
this._cellEdited(path, value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Object}
|
||||
*/
|
||||
_onUpdateValue({ path, value, doneCallback }) {
|
||||
this.state.editMode = false;
|
||||
if (value !== undefined) {
|
||||
this._cellEdited(path, value, doneCallback);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Hover the column in which the mouse is.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onMouseEnter(ev) {
|
||||
const cellParent = ev.target.closest('td,th');
|
||||
const rowParent = ev.target.closest('tr');
|
||||
const index = [...rowParent.children].indexOf(cellParent) + 1;
|
||||
this.root.el.querySelectorAll(`td:nth-child(${index}), th:nth-child(${index})`)
|
||||
.forEach(el => {
|
||||
if (cellParent.querySelector('.o_grid_total_title')) {
|
||||
el.classList.add('o_cell_highlight');
|
||||
}
|
||||
el.classList.add('o_cell_hover');
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remove the hover on the columns.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onMouseLeave() {
|
||||
this.root.el.querySelectorAll('.o_cell_hover')
|
||||
.forEach(el => el.classList.remove('o_cell_hover', 'o_cell_highlight'));
|
||||
}
|
||||
}
|
||||
|
||||
GridRenderer.defaultProps = {
|
||||
cellComponentOptions: {},
|
||||
hasBarChartTotal: false,
|
||||
hideColumnTotal: false,
|
||||
hideLineTotal: false,
|
||||
};
|
||||
GridRenderer.props = {
|
||||
editableCells: {
|
||||
type: Boolean,
|
||||
optional: true
|
||||
},
|
||||
canCreate: Boolean,
|
||||
cellComponent: {
|
||||
type: String,
|
||||
optional: true
|
||||
},
|
||||
cellComponentOptions: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
cellField: String,
|
||||
colField: String,
|
||||
createInline: Boolean,
|
||||
displayEmpty: Boolean,
|
||||
fields: Object,
|
||||
groupBy: Array,
|
||||
hasBarChartTotal: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
hideColumnTotal: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
hideLineTotal: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
measureLabel: {
|
||||
type: String,
|
||||
optional: true
|
||||
},
|
||||
noContentHelp: {
|
||||
type: String,
|
||||
optional: true
|
||||
},
|
||||
range: String,
|
||||
context: Object,
|
||||
arch: Object,
|
||||
isEmbedded: Boolean,
|
||||
isGrouped: Boolean,
|
||||
data: [{
|
||||
cols: [{
|
||||
values: Object,
|
||||
domain: Array,
|
||||
is_current: Boolean,
|
||||
is_unavailable: Boolean,
|
||||
}],
|
||||
grid: [{
|
||||
size: Number,
|
||||
domain: Array,
|
||||
value: Number,
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
optional: true
|
||||
},
|
||||
is_current: Boolean,
|
||||
is_unavailable: Boolean,
|
||||
}],
|
||||
initial: Object,
|
||||
next: Object,
|
||||
prev: Object,
|
||||
rows: [{
|
||||
values: Object,
|
||||
domain: Array,
|
||||
project: Object,
|
||||
label: Array
|
||||
}],
|
||||
totals: {
|
||||
columns: Object,
|
||||
rows: Object,
|
||||
super: Number
|
||||
},
|
||||
__label: Array
|
||||
}],
|
||||
totals: Object,
|
||||
};
|
||||
GridRenderer.template = 'web_grid.GridRenderer';
|
||||
|
||||
return GridRenderer;
|
||||
});
|
||||
Reference in New Issue
Block a user