[IMP] stock,mrp: add lazy column in tree view
It allows to read some fields (slow computation) after the render of the list without blocking the UI. task-2579011 Part-of: odoo/odoo#77092
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<field name="name">mrp.production.tree</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Manufacturing Orders" default_order="priority desc, date_planned_start desc" multi_edit="1" sample="1">
|
||||
<tree string="Manufacturing Orders" js_class="lazy_column_list" default_order="priority desc, date_planned_start desc" multi_edit="1" sample="1">
|
||||
<header>
|
||||
<button name="button_plan" type="object" string="Plan"/>
|
||||
<button name="do_unreserve" type="object" string="Unreserve"/>
|
||||
@@ -22,8 +22,8 @@
|
||||
<field name="activity_ids" widget="list_activity" optional="show"/>
|
||||
<field name="origin" optional="show"/>
|
||||
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
|
||||
<field name="components_availability_state" invisible="1"/>
|
||||
<field name="components_availability"
|
||||
<field name="components_availability_state" invisible="1" options='{"lazy": true}'/>
|
||||
<field name="components_availability" options='{"lazy": true}'
|
||||
attrs="{'invisible': [('state', 'not in', ['confirmed', 'progress'])]}"
|
||||
optional="show"
|
||||
decoration-success="components_availability_state == 'available'"
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
'stock/static/src/js/inventory_singleton_list_view.js',
|
||||
'stock/static/src/js/report_stock_forecasted.js',
|
||||
'stock/static/src/js/report_stock_reception.js',
|
||||
'stock/static/src/js/lazy_column_list.js',
|
||||
'stock/static/src/js/stock_orderpoint_list_controller.js',
|
||||
'stock/static/src/js/stock_orderpoint_list_model.js',
|
||||
'stock/static/src/js/stock_orderpoint_list_view.js',
|
||||
@@ -120,14 +121,17 @@
|
||||
'stock/static/src/js/counted_quantity_widget.js',
|
||||
'stock/static/src/js/basic_model.js',
|
||||
'stock/static/src/js/stock_rescheduling_popover.js',
|
||||
'stock/static/tests/tours/stock_report_tests.js',
|
||||
'stock/static/src/scss/forecast_widget.scss',
|
||||
'stock/static/src/scss/stock_traceability_report.scss',
|
||||
'stock/static/src/scss/stock_empty_screen.scss',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'stock/static/tests/tours/stock_report_tests.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'stock/static/tests/singleton_list_tests.js',
|
||||
'stock/static/tests/popover_widget_tests.js',
|
||||
'stock/static/tests/lazy_column_list_tests.js',
|
||||
'stock/static/tests/stock_traceability_report_backend_tests.js',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import viewRegistry from "web.view_registry";
|
||||
import ListModel from "web.ListModel";
|
||||
import ListRenderer from "web.ListRenderer";
|
||||
import ListView from "web.ListView";
|
||||
|
||||
function _findRecordsInState(entrypoint, result = []) {
|
||||
if (entrypoint && entrypoint.type === "list") {
|
||||
for (const element of entrypoint.data.values()) {
|
||||
_findRecordsInState(element, result);
|
||||
}
|
||||
} else if (entrypoint && entrypoint.type === "record") {
|
||||
result.push(entrypoint);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function _bindLazyColumnToState(entrypoint, lazyColumns, values) {
|
||||
let records = _findRecordsInState(entrypoint);
|
||||
for (const recordData of records.values()) {
|
||||
for (const lazyCol of lazyColumns) {
|
||||
recordData.data[lazyCol] =
|
||||
(values && values[recordData.data.id] && values[recordData.data.id][lazyCol]) ||
|
||||
false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _getLazyColumns(fieldsInfo) {
|
||||
const lazyColumns = [];
|
||||
for (const [key, value] of Object.entries(fieldsInfo)) {
|
||||
if (value.options && value.options.lazy) {
|
||||
lazyColumns.push(key);
|
||||
}
|
||||
}
|
||||
return lazyColumns;
|
||||
}
|
||||
|
||||
const LazyColumnListModel = ListModel.extend({
|
||||
/**
|
||||
* Avoid to fetch lazy fields by filter-out for the model
|
||||
* (`_searchReadUngroupedList`), and let the controller manage it.
|
||||
*
|
||||
* @param {Object} element an element from the localData
|
||||
* @param {Object} [options]
|
||||
* @param {Object} [options.viewType] current viewType. If not set, we will
|
||||
* assume main viewType from the record
|
||||
* @returns {string[]} the list of field names
|
||||
*/
|
||||
_getFieldNames: function (element, options) {
|
||||
const res = this._super.apply(this, arguments);
|
||||
const fieldsInfo = element.fieldsInfo;
|
||||
const viewType = (options && options.viewType) || element.viewType;
|
||||
const lazyColumns = _getLazyColumns(fieldsInfo[viewType]);
|
||||
return res.filter((column) => !lazyColumns.includes(column));
|
||||
},
|
||||
|
||||
_makeDataPoint: function () {
|
||||
// Should put the lazy column to work with decoration
|
||||
const dataPoint = this._super.apply(this, arguments);
|
||||
if (dataPoint.type === "record" && dataPoint.fieldsInfo) {
|
||||
const fieldsInfo = dataPoint.fieldsInfo[dataPoint.viewType];
|
||||
const lazyColumn = _getLazyColumns(fieldsInfo);
|
||||
_bindLazyColumnToState(dataPoint, lazyColumn);
|
||||
}
|
||||
return dataPoint;
|
||||
},
|
||||
});
|
||||
|
||||
const LazyColumnListRenderer = ListRenderer.extend({
|
||||
init: function () {
|
||||
this.currentNbLazyRpc = 0;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
_lazyLoad: async function (state, lazyColumns) {
|
||||
const res = await this._rpc(
|
||||
{
|
||||
model: state.model,
|
||||
method: "read",
|
||||
args: [state.res_ids, lazyColumns],
|
||||
},
|
||||
{ shadow: true }
|
||||
);
|
||||
const resById = {};
|
||||
for (const element of res.values()) {
|
||||
resById[element.id] = {};
|
||||
for (const lazyCol of lazyColumns) {
|
||||
resById[element.id][lazyCol] = element[lazyCol];
|
||||
}
|
||||
}
|
||||
_bindLazyColumnToState(this.state, lazyColumns, resById);
|
||||
this.currentNbLazyRpc -= 1;
|
||||
// Render only thr
|
||||
for (const lazyCol of lazyColumns) {
|
||||
const node = this.columns.find((colAttribute) => colAttribute.attrs.name === lazyCol);
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
const indexNode = this.columns.findIndex(
|
||||
(colAttribute) => colAttribute.attrs.name === lazyCol
|
||||
);
|
||||
const $columnHeader = this.$el.find(`th[data-name='${lazyCol}']`);
|
||||
$columnHeader.replaceWith(this._renderHeaderCell(node));
|
||||
const $cellsLazy = this.$el.find(`.o_data_cell[name='${lazyCol}']`);
|
||||
for (let i = 0; i <= $cellsLazy.length; i++) {
|
||||
const $cell = $($cellsLazy[i]);
|
||||
const idRecord = $cell.parent().data("id");
|
||||
const record = _findRecordsInState(this.state).find((rec) => rec.id === idRecord);
|
||||
if (record) {
|
||||
$cell.replaceWith(
|
||||
this._renderBodyCell(record, node, indexNode, { mode: "readonly" })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async _render() {
|
||||
const lazyColumns = _getLazyColumns(this.state.fieldsInfo[this.state.type]);
|
||||
const lazyVisible = lazyColumns.some((col) => {
|
||||
return !!this.columns.find((colAttribute) => colAttribute.attrs.name === col);
|
||||
});
|
||||
const lazyLoad = lazyVisible && this.state.res_ids.length > 0;
|
||||
if (lazyLoad) {
|
||||
this.currentNbLazyRpc += 1;
|
||||
}
|
||||
const res = await this._super(...arguments);
|
||||
if (lazyLoad) {
|
||||
this._lazyLoad(this.state, lazyColumns);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
_isLazyNode: function (node) {
|
||||
try {
|
||||
if (JSON.parse(node.attrs.options).lazy) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_renderBodyCell: function (record, node) {
|
||||
const $res = this._super.apply(this, arguments);
|
||||
const lazyColumns = _getLazyColumns(this.state.fieldsInfo[this.state.type]);
|
||||
if (this._isLazyNode(node)) {
|
||||
// Decorations doesn't work for lazy column because the evalContext isn't updated and it is a readonly attribute
|
||||
// Then a lazy fields can be only used in a other lazy fields decoration.
|
||||
const extraData = {};
|
||||
for (const col of lazyColumns) {
|
||||
extraData[col] = record.data[col];
|
||||
}
|
||||
const evalContext = Object.assign({}, record.evalContext, extraData);
|
||||
for (const [cssClass, expr] of Object.entries(this.fieldDecorations[node.attrs.name])) {
|
||||
$res.toggleClass(cssClass, py.PY_isTrue(py.evaluate(expr, evalContext)));
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
},
|
||||
|
||||
_renderHeaderCell: function (node) {
|
||||
const $res = this._super.apply(this, arguments);
|
||||
if (this._isLazyNode(node)) {
|
||||
const $spin = $('<i class="fa fa-refresh fa-spin m-1"/>');
|
||||
if (this.currentNbLazyRpc == 0) {
|
||||
$spin.addClass("invisible");
|
||||
}
|
||||
$res.find(":last").before($spin);
|
||||
}
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
const LazyColumnList = ListView.extend({
|
||||
config: Object.assign({}, ListView.prototype.config, {
|
||||
Renderer: LazyColumnListRenderer,
|
||||
Model: LazyColumnListModel,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add("lazy_column_list", LazyColumnList);
|
||||
|
||||
export default LazyColumnList;
|
||||
@@ -0,0 +1,91 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import testUtils from "web.test_utils";
|
||||
import LazyColumnList from "../src/js/lazy_column_list";
|
||||
import { makeDeferred, nextTick } from "../../../web/static/tests/helpers/utils";
|
||||
|
||||
const createView = testUtils.createView;
|
||||
|
||||
QUnit.module(
|
||||
"Views",
|
||||
{
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
person: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
slowField: { string: "Hard to Compute", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Daniel Fortesque" },
|
||||
{ id: 2, name: "Samuel Oak" },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
function () {
|
||||
QUnit.module("LazyColumnList");
|
||||
|
||||
QUnit.test("Loading correctly", async function (assert) {
|
||||
assert.expect(12);
|
||||
const promiseRead = makeDeferred();
|
||||
const list = await createView({
|
||||
View: LazyColumnList,
|
||||
model: "person",
|
||||
data: this.data,
|
||||
arch:
|
||||
'<tree editable="top" js_class="lazy_column_list">' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="slowField" options=\'{"lazy": true}\'/>' +
|
||||
"</tree>",
|
||||
mockRPC: async function (route, args) {
|
||||
console.log(route);
|
||||
if (route === "/web/dataset/call_kw/person/read") {
|
||||
const fields = args.args[1];
|
||||
const records = [];
|
||||
for (const d of this.data.person.records) {
|
||||
let reco = { id: d.id };
|
||||
for (const f of fields) {
|
||||
if (f === "slowField") {
|
||||
reco[f] = `${d.name} ${d.id}`;
|
||||
} else {
|
||||
reco[f] = d[f];
|
||||
}
|
||||
}
|
||||
records.push(reco);
|
||||
}
|
||||
await promiseRead;
|
||||
return Promise.resolve(records);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
// Checks we have initially 2 records
|
||||
assert.containsN(list, ".o_data_row", 2, "should have 2 records");
|
||||
|
||||
let $spin = list.$el.find("th[data-name='slowField'] .fa.fa-spin");
|
||||
let $results = list.$el.find("td[name='slowField']");
|
||||
assert.equal($spin.length, 1);
|
||||
assert.equal($spin.hasClass("invisible"), false);
|
||||
assert.equal($results.length, 2);
|
||||
assert.equal($results.first().text(), "");
|
||||
assert.equal($results.last().text(), "");
|
||||
|
||||
promiseRead.resolve();
|
||||
await nextTick();
|
||||
|
||||
$spin = list.$el.find("th[data-name='slowField'] .fa.fa-spin");
|
||||
$results = list.$el.find("td[name='slowField']");
|
||||
assert.equal($spin.length, 1);
|
||||
assert.equal($spin.hasClass("invisible"), true);
|
||||
assert.equal($results.length, 2);
|
||||
assert.equal($results.first().text(), "Daniel Fortesque 1");
|
||||
assert.equal($results.last().text(), "Samuel Oak 2");
|
||||
|
||||
assert.containsN(list, ".o_data_row", 2, "should have 2 records");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -189,7 +189,7 @@
|
||||
<field name="name">stock.picking.tree</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Picking list" multi_edit="1" sample="1">
|
||||
<tree string="Picking list" js_class="lazy_column_list" multi_edit="1" sample="1">
|
||||
<header>
|
||||
<button name="do_unreserve" type="object" string="Unreserve"/>
|
||||
<button name="action_assign" type="object" string="Check Availability"/>
|
||||
@@ -203,8 +203,8 @@
|
||||
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
|
||||
<field name="scheduled_date" optional="show" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="picking_type_code" invisible="1"/>
|
||||
<field name="products_availability_state" invisible="1"/>
|
||||
<field name="products_availability"
|
||||
<field name="products_availability_state" invisible="1" options='{"lazy": true}'/>
|
||||
<field name="products_availability" options='{"lazy": true}'
|
||||
optional="hide"
|
||||
attrs="{'invisible': ['|', ('picking_type_code', '!=', 'outgoing'), ('state', 'not in', ['confirmed', 'waiting', 'assigned'])]}"
|
||||
decoration-warning="products_availability_state == 'expected'"
|
||||
|
||||
Reference in New Issue
Block a user