Commit 9a5ed805 authored by Romain Courteaud's avatar Romain Courteaud

erp5_web_renderjs_ui: rewrite matrixbox

Always recalculate the full matrixbox content if one change is found.

Add mutex to prevent raise condition between render and getContent.
parent 3454c143
/*jslint indent: 2, maxerr: 3, nomen: true */ /*jslint indent: 2, maxerr: 3, nomen: true */
/*global window, document, rJS, RSVP, domsugar, JSON*/ /*global window, rJS, RSVP, domsugar, JSON*/
/** MatrixBox renders a N-dimensional cube of editable values based on axes description. /** MatrixBox renders a N-dimensional cube of editable values based on axes description.
* *
* Example JSON returned from HATEOAS where cell_range format is * Example JSON returned from HATEOAS where cell_range format is
...@@ -33,36 +33,15 @@ ...@@ -33,36 +33,15 @@
see around https://lab.nexedi.com/nexedi/erp5/blob/feature/renderjs-matrixbox/product/ERP5Form/MatrixBox.py#L427 see around https://lab.nexedi.com/nexedi/erp5/blob/feature/renderjs-matrixbox/product/ERP5Form/MatrixBox.py#L427
* *
*/ */
(function (window, document, rJS, RSVP, domsugar, JSON) { (function (window, rJS, RSVP, domsugar, JSON) {
"use strict"; "use strict";
/** Recursively introspect an object if it is empty */
function is_empty_recursive(data) {
var item;
if (typeof data === 'object') {
for (item in data) {
if (data.hasOwnProperty(item) && !item.startsWith("_")) {
if (is_empty_recursive(data[item]) === false) {return false; } // one non-empty element is enough
}
}
return true;
}
return !data && true; // convert basic types to boolean
}
function copy(obj) { function copy(obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
rJS(window) rJS(window)
.ready(function () {
this.props = {
gadget_dict: {} // holds references to initialized gadgets
};
})
.setState({ .setState({
data: '', data: '',
template_field_dict: '', template_field_dict: '',
...@@ -71,178 +50,146 @@ ...@@ -71,178 +50,146 @@
key: '' key: ''
}) })
/** Render constructs and saves gadgets into `props.gadget_dict` if they don not exist yet.
*/
.declareMethod('render', function (options) { .declareMethod('render', function (options) {
var gadget = this, return this.changeState({
data = options.field_json.data, 'table_list': JSON.stringify(options.field_json.data),
// note we make COPY of data in their original form - important since 'template_field_dict': JSON.stringify(options.field_json.template_field_dict),
// data.shift used later modify the structure inplace! 'editable': options.field_json.editable,
new_state = { 'hidden': options.field_json.hidden,
'data': JSON.stringify(options.field_json.data), 'key': options.field_json.key
'template_field_dict': JSON.stringify(options.field_json.template_field_dict), });
'editable': options.field_json.editable, }, {mutex: 'render'})
'hidden': options.field_json.hidden,
'key': options.field_json.key .onStateChange(function () {
};
if (is_empty_recursive(data)) {
return;
}
if (!is_empty_recursive(gadget.props.gadget_dict)) {
return this.changeState(new_state);
}
return new RSVP.Queue()
.push(function () {
return RSVP.all(data.map(function (table, table_index) {
var header = table.shift(), // first item of table is the header
table_title = header.shift(); // first item of header is the table (tab) title
return new RSVP.Queue()
.push(function () {
return RSVP.all(table.map(function (row, row_index) {
var row_element = document.createElement('tr'),
row_id = new_state.key + "T" + table_index + "R" + row_index;
row.shift(); // drop the row label definition because it is not usable now
row_element.setAttribute('id', row_id);
row_element.appendChild(document.createElement('th'));
return new RSVP.Queue()
.push(function () {
return RSVP.all(row.map(function (column) {
// transform all cell-definitions into actual gadgets
return gadget.declareGadget('gadget_erp5_label_field.html', {
scope: column.key,
element: 'td',
sandbox: "public"
})
.push(function (sub_gadget) {
gadget.props.gadget_dict[column.key] = sub_gadget;
return sub_gadget.element;
});
}));
})
.push(function (column_element_list) {
column_element_list.forEach(function (column_element) {
row_element.appendChild(column_element);
});
return row_element;
});
}));
})
.push(function (row_element_list) {
var th_dom_list = [
domsugar('th', {text: table_title})
],
i;
for (i = 0; i < header.length; i += 1) {
th_dom_list.push(domsugar('th', {html: header[i]}));
}
return domsugar('table', [
domsugar('thead', [
domsugar('tr', th_dom_list)
]),
domsugar('tbody', row_element_list)
]);
});
}));
})
.push(function (table_element_list) {
domsugar(gadget.element.querySelector('div.document_table'),
table_element_list);
return gadget.changeState(new_state);
});
})
/** Changes state of existing gadgets inside `props.gadget_dict`. */
.onStateChange(function (modification_dict) {
var gadget = this, var gadget = this,
template_field_dict = JSON.parse(gadget.state.template_field_dict), template_field_dict = JSON.parse(gadget.state.template_field_dict),
promise_queue = new RSVP.Queue(), table_list = JSON.parse(gadget.state.table_list);
data;
return new RSVP.Queue(RSVP.all(table_list.map(function (table,
if (modification_dict.hasOwnProperty('data')) { table_index) {
data = JSON.parse(modification_dict.data); // first item of table is the header
if (is_empty_recursive(data)) { var header_list = table.shift(),
return; // first item of header is the table (tab) title
} table_title = header_list.shift();
data.forEach(function (table, table_index) {
table.shift(); // drop the header return new RSVP.Queue(RSVP.all(table.map(function (row, row_index) {
table.forEach(function (row, row_index) { // drop the row label definition because it is not usable now
var row_id = gadget.state.key + 'T' + table_index + 'R' + row_index, var row_label = row.shift();
row_label_element = gadget.element.querySelector('tr#' + row_id + ' th');
row_label_element.textContent = row.shift() || ''; // pop-up the row label from data return new RSVP.Queue(RSVP.all(row.map(function (column) {
// transform all cell-definitions into actual gadgets
// then handle all inputs within the row return gadget.declareGadget('gadget_erp5_label_field.html', {
row.forEach(function (column) { scope: column.key,
promise_queue element: 'td',
.push(function () { sandbox: "public"
// Rendering of embedded field is prescribed by another field })
// in the form (usually in "hidden" group). Therefor we have a .push(function (sub_gadget) {
// reference for the template field included in state (field) // Rendering of embedded field is prescribed by another field
var template_field = template_field_dict[column.field_id], // in the form (usually in "hidden" group). Therefor we have a
field_json = copy(template_field), // reference for the template field included in state (field)
sub_gadget = gadget.props.gadget_dict[column.key]; var template_field = template_field_dict[column.field_id],
field_json = copy(template_field);
// we copy (unknown) structure of template_field and carefully
// add known attributes from `column` // we copy (unknown) structure of template_field and carefully
field_json.default = column.value; // add known attributes from `column`
field_json.key = "field_" + column.key; field_json['default'] = column.value;
field_json.hidden = gadget.state.hidden || template_field.hidden; // any hidden will hide the element field_json.key = "field_" + column.key;
field_json.editable = gadget.state.editable && template_field.editable; // any non-editable will disable editation field_json.hidden = gadget.state.hidden || template_field.hidden; // any hidden will hide the element
field_json.error_text = column.error_text; field_json.editable = gadget.state.editable && template_field.editable; // any non-editable will disable editation
field_json.error_text = column.error_text;
return sub_gadget.render({
return RSVP.hash({
_: sub_gadget.render({
label: false, label: false,
development_link: false, development_link: false,
field_type: column.type, field_type: column.type,
field_json: field_json field_json: field_json
}); }),
sub_gadget: sub_gadget
}); });
})
.push(function (hash) {
return hash.sub_gadget.element;
});
})))
.push(function (column_element_list) {
// return row_element
return domsugar('tr', {
id: gadget.state.key + "T" + table_index + "R" + row_index
}, [
domsugar('th', {text: row_label}),
domsugar(null, column_element_list)
]);
}); });
})))
.push(function (row_element_list) {
var th_dom_list = [
domsugar('th', {text: table_title})
],
i;
for (i = 0; i < header_list.length; i += 1) {
// XXX used to be html instead of text
// But as unsecure, try to restrict
th_dom_list.push(domsugar('th', {text: header_list[i]}));
}
return domsugar('table', [
domsugar('thead', [
domsugar('tr', th_dom_list)
]),
domsugar('tbody', row_element_list)
]);
}); });
})))
.push(function (table_element_list) {
domsugar(gadget.element.querySelector('div.document_table'),
table_element_list);
}); });
} // end: if modification_dict.data
return promise_queue;
}) })
.declareMethod("getContent", function (options) { .declareMethod("getContent", function (options) {
var gadget = this, var gadget = this,
data = {}, // result dictionary with values table_list = JSON.parse(gadget.state.table_list),
field_key_list = [], promise_list = [],
field_key; // result dictionary with values
result_dict = {};
function extendData(field_data) {
var key; table_list.map(function (table) {
for (key in field_data) { // first item of table is the header
if (field_data.hasOwnProperty(key) && !key.startsWith("_")) { table.shift();
data[key] = field_data[key];
} table.map(function (row) {
} // drop the row label definition because it is not usable now
} row.shift();
for (field_key in gadget.props.gadget_dict) { row.map(function (column) {
if (gadget.props.gadget_dict.hasOwnProperty(field_key) && !field_key.startsWith("_")) { var field_key = column.key;
field_key_list.push(field_key); if (field_key.startsWith("_")) {
} return;
} }
promise_list.push(
gadget.getDeclaredGadget(field_key)
.push(function (sub_gadget) {
return sub_gadget.getContent(options);
})
.push(function (field_data) {
var key;
for (key in field_data) {
if (field_data.hasOwnProperty(key) && !key.startsWith("_")) {
result_dict[key] = field_data[key];
}
}
})
);
});
});
});
return new RSVP.Queue() return new RSVP.Queue(RSVP.all(promise_list))
.push(function () { .push(function () {
return RSVP.all(field_key_list.map(function (field_key) { return result_dict;
return gadget.props.gadget_dict[field_key].getContent(options);
}));
})
.push(function (field_value_list) {
field_value_list.forEach(extendData);
return data;
}); });
}) }, {mutex: 'render'})
.allowPublicAcquisition("notifyInvalid", function () { .allowPublicAcquisition("notifyInvalid", function () {
return; return;
...@@ -254,6 +201,6 @@ ...@@ -254,6 +201,6 @@
.declareMethod("checkValidity", function () { .declareMethod("checkValidity", function () {
return true; return true;
}); }, {mutex: 'render'});
}(window, document, rJS, RSVP, domsugar, JSON)); }(window, rJS, RSVP, domsugar, JSON));
...@@ -240,7 +240,7 @@ ...@@ -240,7 +240,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>990.15674.6574.27784</string> </value> <value> <string>999.25959.13616.26538</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -258,7 +258,7 @@ ...@@ -258,7 +258,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1614297317.85</float> <float>1649344500.27</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment