Commit db232872 authored by Tomáš Peterka's avatar Tomáš Peterka

[renderjs_ui] MatrixBox field implementation

parent be7dd0a1
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title>ERP5 MatrixBox Field</title>
<script src="rsvp.js" type="text/javascript"></script>
<script src="renderjs.js" type="text/javascript"></script>
<script src="gadget_erp5_field_matrixbox.js" type="text/javascript"></script>
<script id="table-template" type="text/x-handlebars-template">
<thead>
<tr>
<th>{{table_title}}</th>
{{#each header}}
<th>{{this}}</th>
{{/each}}
</tr>
</thead>
</script>
</head>
<body>
<div class="document_table"></div>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Web Page" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>gadget_erp5_field_matrixbox.html</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>rjs_gadget_erp5_field_matrixbox_html</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value> <string>en</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Web Page</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Gadget ERP5 MatrixBox Field</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>001</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>document_publication_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>edit_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>processing_status_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>publish_alive</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1503410208.86</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>published_alive</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>edit</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>961.40643.9314.60433</string> </value>
</item>
<item>
<key> <string>state</string> </key>
<value> <string>current</string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1503910712.27</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
<record id="5" aka="AAAAAAAAAAU=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>detect_converted_file</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_processing_state</string> </key>
<value> <string>converted</string> </value>
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>0.0.0.0</string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1503409677.15</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
/*jslint indent: 2, maxerr: 3, nomen: true */
/*global window, document, rJS, RSVP, Handlebars, JSON*/
/** MatrixBox renders a N-dimensional cube of editable values based on axes description.
*
* Example JSON returned from HATEOAS where cell_range format is
* [<column-labels> <line1> <line2> ... where
* <column-labels>: tuple (relative-url, title) (ex. [("sale_supply_module/1/3", "Sale Supply Line")])
* <lineN>: array of [label, values...]
*
* Example:
*
"matrixbox": {
"title": "matrixbox",
"css_class": "",
"editable": 1,
"data": [[
[ <tabID>, "Col1 Title", "Col2 Title"],
["Row1 Title", <field_json>, null, null],
["50.0 <= Quantity < 100.0", , null, null], // example
]],
"key": "field_matrixbox",
"hidden": 0,
"type": "MatrixBox"
},
where <field_json> consists of
"field_id" - name of the template field for this cell
"key" - key in REQUEST (without 'field_' prefix)
"type" - meta type for RenderJS rendering
"default" - default value to render
"value" - current validated value
see around https://lab.nexedi.com/nexedi/erp5/blob/feature/renderjs-matrixbox/product/ERP5Form/MatrixBox.py#L427
*
*/
(function (window, document, rJS, RSVP, Handlebars, JSON) {
"use strict";
var gadget_klass = rJS(window),
table_template_source = gadget_klass.__template_element
.getElementById("table-template")
.innerHTML,
table_template = Handlebars.compile(table_template_source);
/** 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) {
return JSON.parse(JSON.stringify(obj));
}
rJS(window)
.ready(function () {
this.props = {
gadget_dict: {} // holds references to initialized gadgets
};
})
.setState({
data: '',
template_field_dict: '',
editable: undefined,
hidden: undefined,
key: ''
})
//////////////////////////////////////////////
// acquired method
//////////////////////////////////////////////
.declareAcquiredMethod("jio_allDocs", "jio_allDocs")
.declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("getUrlParameter", "getUrlParameter")
.declareAcquiredMethod("getFieldTypeGadgetUrl", "getFieldTypeGadgetUrl")
.declareAcquiredMethod("renderEditorPanel", "renderEditorPanel")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate")
/** Render constructs and saves gadgets into `props.gadget_dict` if they don not exist yet.
*/
.declareMethod('render', function (options) {
var gadget = this,
element = gadget.element.querySelector('div.document_table'),
data = options.field_json.data,
// note we make COPY of data in their original form - important since
// data.shift used later modify the structure inplace!
new_state = {
'data': JSON.stringify(options.field_json.data),
'template_field_dict': JSON.stringify(options.field_json.template_field_dict),
'editable': options.field_json.editable,
'hidden': options.field_json.hidden,
'key': options.field_json.key
};
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
table_body = document.createElement('tbody'),
table_element = document.createElement('table');
table_element.innerHTML = table_template({table_title: table_title, header: header});
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
var col_element = document.createElement('td');
return gadget.declareGadget('gadget_erp5_label_field.html', {
scope: column.key,
element: col_element,
sandbox: "public"
})
.push(function (sub_gadget) {
gadget.props.gadget_dict[column.key] = sub_gadget;
return col_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) {
row_element_list.forEach(function (row_element) {
table_body.appendChild(row_element);
});
table_element.appendChild(table_body);
return table_element;
});
}));
})
.push(function (table_element_list) {
table_element_list.forEach(function (table_element) {
element.appendChild(table_element);
});
return gadget.changeState(new_state);
});
})
/** Changes state of existing gadgets inside `props.gadget_dict`. */
.onStateChange(function (modification_dict) {
var gadget = this,
template_field_dict = JSON.parse(gadget.state.template_field_dict),
promise_queue = new RSVP.Queue(),
data;
if (modification_dict.hasOwnProperty('data')) {
data = JSON.parse(modification_dict.data);
if (is_empty_recursive(data)) {
return;
}
data.forEach(function (table, table_index) {
table.shift(); // drop the header
table.forEach(function (row, row_index) {
var row_id = gadget.state.key + 'T' + table_index + 'R' + row_index,
row_label_element = gadget.element.querySelector('tr#' + row_id + ' th');
row_label_element.textContent = row.shift() || ''; // pop-up the row label from data
// then handle all inputs within the row
row.forEach(function (column) {
promise_queue
.push(function () {
// Rendering of embedded field is prescribed by another field
// in the form (usually in "hidden" group). Therefor we have a
// reference for the template field included in state (field)
var template_field = template_field_dict[column.field_id],
field_json = copy(template_field),
sub_gadget = gadget.props.gadget_dict[column.key];
// we copy (unknown) structure of template_field and carefully
// add known attributes from `column`
field_json.value = column.value;
field_json.key = "field_" + column.key;
field_json.hidden = gadget.state.hidden || template_field.hidden; // any hidden will hide the element
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({
label: false,
field_type: column.type,
field_json: field_json
});
});
});
});
});
} // end: if modification_dict.data
return promise_queue;
})
.declareMethod("getContent", function (options) {
var gadget = this,
data = {}, // result dictionary with values
field_key_list = [],
field_key;
function extendData(field_data) {
var key;
for (key in field_data) {
if (field_data.hasOwnProperty(key) && !key.startsWith("_")) {
data[key] = field_data[key];
}
}
}
for (field_key in gadget.props.gadget_dict) {
if (gadget.props.gadget_dict.hasOwnProperty(field_key) && !field_key.startsWith("_")) {
field_key_list.push(field_key);
}
}
return new RSVP.Queue()
.push(function () {
return RSVP.all(field_key_list.map(function (field_key) {
return gadget.props.gadget_dict[field_key].getContent(options);
}));
})
.push(function (field_value_list) {
field_value_list.forEach(extendData);
return data;
});
})
.allowPublicAcquisition("notifyInvalid", function () {
return;
})
.allowPublicAcquisition("notifyValid", function () {
return;
});
}(window, document, rJS, RSVP, Handlebars, JSON));
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Web Script" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>gadget_erp5_field_matrixbox.js</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>rjs_gadget_erp5_field_matrixbox_js</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value> <string>en</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Web Script</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Gadget ERP5 Matrixbox Field HTML</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>001</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>document_publication_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>edit_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>processing_status_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>publish_alive</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1503409620.67</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>published_alive</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>edit</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>961.46727.895.52189</string> </value>
</item>
<item>
<key> <string>state</string> </key>
<value> <string>current</string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1504008626.15</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
<record id="5" aka="AAAAAAAAAAU=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>detect_converted_file</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_processing_state</string> </key>
<value> <string>converted</string> </value>
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>0.0.0.0</string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1502435250.74</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
...@@ -52,14 +52,12 @@ ...@@ -52,14 +52,12 @@
}); });
}) })
/** Return content even for non-editable cells - be backward compatible! */
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
if (this.state.editable) { return this.getDeclaredGadget('sub')
return this.getDeclaredGadget('sub') .push(function (gadget) {
.push(function (gadget) { return gadget.getContent();
return gadget.getContent(); });
});
}
return {};
}) })
.declareMethod('checkValidity', function () { .declareMethod('checkValidity', function () {
......
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>955.60919.29957.58146</string> </value> <value> <string>960.5523.58984.43537</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1482843372.92</float> <float>1503499581.59</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -56,6 +56,19 @@ ...@@ -56,6 +56,19 @@
element.removeChild(element.firstChild); element.removeChild(element.firstChild);
} }
element.appendChild(new_element); element.appendChild(new_element);
})
/** Because of meta-fields (controlling MatrixBox for example) we need to
* obtain value of readonly fields.
* In order to make it more developer-friendly, only named fields return their values.
*/
.declareMethod("getContent", function () {
var data = {};
if (!this.state.name) {
return data;
}
data[this.state.name] = this.state.text_content || this.state.inner_html || "";
return data;
}); });
}(window, document, rJS)); }(window, document, rJS));
\ No newline at end of file
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