Commit 6352d285 authored by Tomáš Peterka's avatar Tomáš Peterka Committed by Tomáš Peterka

[renderjs_ui] Move Form submission logic into one place - Page Form and add...

[renderjs_ui] Move Form submission logic into one place - Page Form and add constants to Form Definition in [hal_json_style]
parent 878e4d3e
...@@ -1170,6 +1170,20 @@ def renderFormDefinition(form, response_dict): ...@@ -1170,6 +1170,20 @@ def renderFormDefinition(form, response_dict):
field_list.append((field.id, renderRawField(field))) field_list.append((field.id, renderRawField(field)))
group_list.append((group['gid'], field_list)) group_list.append((group['gid'], field_list))
# some forms might not have any fields so we put empty bottom group
if not group_list:
group_list = [('bottom', [])]
# each form has hidden attribute `form_id`
group_list[-1][1].append(('form_id', {'meta_type': 'StringField'}))
if form.pt == "form_dialog":
# every form dialog has its dialog_id and meta (control) attributes in extra_param_json
group_list[-1][1].extend([
('dialog_id', {'meta_type': 'StringField'}),
])
response_dict["group_list"] = group_list response_dict["group_list"] = group_list
response_dict["title"] = Base_translateString(form.getTitle()) response_dict["title"] = Base_translateString(form.getTitle())
response_dict["pt"] = form.pt response_dict["pt"] = form.pt
......
...@@ -761,6 +761,28 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin): ...@@ -761,6 +761,28 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
self.assertFalse(result_dict['_embedded']['_view'].has_key('_actions')) self.assertFalse(result_dict['_embedded']['_view'].has_key('_actions'))
@simulate('Base_getRequestHeader', '*args, **kwargs',
'return "application/hal+json"')
@createIndexedDocument()
@changeSkin('Hal')
def test_getHateoasForm_dialog_constants(self, document):
fake_request = do_fake_request("GET")
result = self.portal.web_site_module.hateoas.ERP5Document_getHateoas(
REQUEST=fake_request, mode="traverse", relative_url="portal_skins/erp5_ui_test/Foo_viewDummyDialog")
self.assertEquals(fake_request.RESPONSE.status, 200)
self.assertEquals(fake_request.RESPONSE.getHeader('Content-Type'),
"application/hal+json"
)
result_dict = json.loads(result)
_, group_fields = result_dict['group_list'][-1]
field_names = [field_name for field_name, field_type in group_fields]
self.assertIn("form_id", field_names)
self.assertIn("dialog_id", field_names)
# no need for dialog_method because that one is hardcoded in javascript
@simulate('Base_getRequestUrl', '*args, **kwargs', @simulate('Base_getRequestUrl', '*args, **kwargs',
'return "http://example.org/bar"') 'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs', @simulate('Base_getRequestHeader', '*args, **kwargs',
......
<!DOCTYPE html> <!DOCTYPE html>
<!--
data-i18n=Encountered an unknown error. Try to resubmit.
data-i18n=Input data has errors.
data-i18n=You do not have the permissions to edit the object.
data-i18n=You are offline.
data-i18n=Action succeeded.
data-i18n=Data received.
-->
<html> <html>
<head> <head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>961.19210.8471.60620</string> </value> <value> <string>966.41656.42235.61815</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1518685706.64</float> <float>1523287594.73</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
/*global window, rJS, URI, RSVP, asBoolean */ /*global window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean */
/*jslint nomen: true, indent: 2, maxerr: 3 */ /*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, URI, RSVP, asBoolean) { /**
Page Form is a top-level gadget (a "Page") taking care of rendering form
and handling data send&receive.
*/
(function (window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean) {
"use strict"; "use strict";
/** Return local modifications to editable form fields after leaving the form /** Return local modifications to editable form fields after leaving the form
for a while - for example selecting a related object. for a while - for example selecting a related object.
We use the fact that selecting a related object is still rendered by page_form
thus gadget.state acts as a persistent storage.
@argument result is possible current field value @argument result is possible current field value
*/ */
function loadFormContent(gadget, result) { function loadFormContent(gadget, result) {
...@@ -44,9 +51,14 @@ ...@@ -44,9 +51,14 @@
// Acquired methods // Acquired methods
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
.declareAcquiredMethod("jio_getAttachment", "jio_getAttachment") .declareAcquiredMethod("jio_getAttachment", "jio_getAttachment")
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("redirect", "redirect") .declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("jio_allDocs", "jio_allDocs") .declareAcquiredMethod("jio_allDocs", "jio_allDocs")
.declareAcquiredMethod("updatePanel", "updatePanel") .declareAcquiredMethod("updatePanel", "updatePanel")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// Proxy methods to the child gadget // Proxy methods to the child gadget
...@@ -63,8 +75,12 @@ ...@@ -63,8 +75,12 @@
return declared_gadget.checkValidity(); return declared_gadget.checkValidity();
}); });
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
return this.getDeclaredGadget('fg') var gadget = this;
// no need to add runtime information in general for forms ...
// each Form Page Template handles that on their own
return gadget.getDeclaredGadget('fg')
.push(function (declared_gadget) { .push(function (declared_gadget) {
return declared_gadget.getContent(); return declared_gadget.getContent();
}); });
...@@ -163,7 +179,7 @@ ...@@ -163,7 +179,7 @@
.onStateChange(function (modification_dict) { .onStateChange(function (modification_dict) {
var queue, var queue,
gadget = this, gadget = this,
options = this.state.options, options = gadget.state.options,
page_template_gadget, page_template_gadget,
erp5_document = JSON.parse(gadget.state.erp5_document), erp5_document = JSON.parse(gadget.state.erp5_document),
erp5_form = JSON.parse(gadget.state.erp5_form); erp5_form = JSON.parse(gadget.state.erp5_form);
...@@ -216,30 +232,279 @@ ...@@ -216,30 +232,279 @@
} }
}); });
}) })
.allowPublicAcquisition("displayFormulatorValidationError", function (param_list) { /** SubmitContent should be called by the gadget which renders submit button
var erp5_document = JSON.parse(this.state.erp5_document); thus should handle the submit event.
erp5_document._embedded._view = param_list[0]; It calls getContent on the child gadget and submits those data to given
// Force refresh jio_key and URL using JIO putAttachment call.
erp5_document._now = Date.now(); This function handles parsing the server response, showing error/success
messages and re-rendering the form if obtained (in success and failure case).
Your .thenable will either receive string jio key to redirect to or undefined|null
in case no redirect should be issued.
Returns: on success it returns a Promise with {string} JIO key
on failure it throws an error with the invalid response
*/
.allowPublicAcquisition("submitContent", function (param_list) {
var gadget = this,
jio_key = param_list[0],
target_url = param_list[1],
content_dict = param_list[2];
return gadget.notifySubmitting()
.push(function () {
return gadget.jio_putAttachment(jio_key, target_url, content_dict);
})
.push(function (attachment) {
if (attachment.target.response.type === "application/json") {
// successful form save returns simple redirect and an answer as JSON
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
return gadget.notifySubmitted({
"message": response.portal_status_message,
"status": response.portal_status_level || "success"
});
})
.push(function () {
// here we figure out where to go after form submit - indicated
// by X-Location HTTP header placed by Base_redirect script
var redirect_jio_key = new URI(
attachment.target.getResponseHeader("X-Location")
).segment(2);
return redirect_jio_key;
});
}
if (attachment.target.response.type === "application/hal+json") {
// we have received a view definition thus we need to redirect
// this will happen only in report/export when "Format" is unspecified
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
var erp5_document = JSON.parse(gadget.state.erp5_document),
response_view = JSON.parse(response_text.target.result),
options = gadget.state.options;
erp5_document._embedded._view = response_view;
erp5_document._now = Date.now(); // force refresh
// We choose render instead of changeState because the new form can use
// different page_template (reports are setup in form_dialog but rendered
// in report_view).
// Validation provides document updated for error texts but uses the same
// form thus the same view thus the same url - no DOM modifications
//
// We modify inplace state.options because render method uses and removes
// erp5_document hidden in its options.
options.erp5_document = erp5_document;
return new RSVP.Queue()
.push(function () {
if (response_view._notification === undefined) {
return gadget.translate("Data received.");
}
return response_view._notification.message;
})
.push(function (translated_message) {
return gadget.notifySubmitted({
"message": translated_message,
"status": response_view._notification ? response_view._notification.status : "success"
});
})
.push(function () {
/* We do not need to remove _notification because we
* force-reload by putting _now into "hashed" document
if (response_view._notification !== undefined) {
delete response_view._notification;
}
*/
return gadget.render(options);
})
.push(function () {
// Make sure to return nothing (previous render can return
// something) so the successfull handler does not receive
// anything which it could consider as redirect jio key.
return;
});
});
}
// response status > 200 (e.g. 202 "Accepted" or 204 "No Content")
// means a sucessful execution of the action but does not carry any data
// XMLHttpRequest automatically inserts Content-Type="text/xml" thus
// we cannot test based on that
if (attachment.target.response.size === 0 &&
attachment.target.status > 200 &&
attachment.target.status < 400) {
return gadget.translate("Action succeeded.")
.push(function (translated_message) {
return gadget.notifySubmitted({
"message": translated_message,
"status": "success"
});
})
.push(function () {
return jio_key;
});
}
// any other attachment type we force to download because it is most
// likely product of export/report (thus PDF, ODT ...)
return gadget.translate("Data received.")
.push(function (translated_message) {
return gadget.notifySubmitted({
"message": translated_message,
"status": "success"
});
})
.push(function () {
return gadget.forceDownload(attachment);
})
// we could redirect back after download which was not possible
// in the old UI but it will be a change of behaviour
// Nicolas required this feature to be allowed
.push(function () {
return jio_key;
});
})
.push(null, function (error) {
/** Fail branch of the JIO call. */
var error_text = 'Encountered an unknown error. Try to resubmit.';
if (error instanceof RSVP.CancellationError) {
// CancellationError is thrown on "redirect" to cancel any pending
// promises. Since it is not a failure we rethrow.
throw error;
}
return this.changeState({erp5_document: JSON.stringify(erp5_document)}); if (error === undefined || error.target === undefined) {
return gadget.translate('Encountered an unknown error. Try to resubmit.')
.push(function (translated_message) {
return gadget.notifySubmitted({
'message': translated_message,
'status': 'error'
});
})
.push(function () {
return; // error was handled
});
}
// Let's display notification about the error to the user if possible
if (error.target.status === 400) {
error_text = 'Input data has errors.';
} else if (error.target.status === 403) {
error_text = 'You do not have the permissions to edit the object.';
} else if (error.target.status === 0) {
error_text = 'You are offline.';
}
// If the response is JSON, then look for the translated message sent
// by the portal and display it to the user
if (error.target.response.type === 'application/json' ||
error.target.response.type === 'application/hal+json') {
return gadget.notifySubmitted()
.push(function () {
return jIO.util.readBlobAsText(error.target.response);
})
// Translated error description must be part of the response
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
if (error.target.response.type === 'application/json') {
// pure JSON carries only the message (deprecated)
// so we parse it out and return
return gadget.notifyChange({
"message": response.portal_status_message,
"status": "error"
});
}
if (error.target.response.type === 'application/hal+json') {
// HAL+JSON carries whole form definition with optional message
return new RSVP.Queue()
.push(function () {
if (!response._notification || !response._notification.message) {
// return error text from HTTP Status CODE and translate
return gadget.translate(error_text);
}
return response._notification.message;
})
.push(function (translated_message) {
return gadget.notifyChange({
"message": translated_message,
"status": response._notification ? response._notification.status : "error"
});
})
.push(function () {
var erp5_document = JSON.parse(gadget.state.erp5_document);
erp5_document._embedded._view = response;
erp5_document._now = Date.now();
return gadget.changeState({erp5_document: JSON.stringify(erp5_document)});
});
}
})
.push(function () {
return; // error was handled
});
}
// If the response in empty with only HTTP Status code then we display
// our static translated error_text to the user
return gadget.notifySubmitted()
.push(function () {
return gadget.translate(error_text);
})
.push(function (message) {
return gadget.notifyChange({
"message": message,
"status": "error"
});
})
.push(function () {
return; // error was handled
});
});
}) })
/** Re-render whole form page with completely new form. */
.allowPublicAcquisition("updateForm", function (args, subgadget_id) { /** The only way how to force download from javascript (working everywhere)
var erp5_document = JSON.parse(this.state.erp5_document), * is unfortunately constructing <a> and clicking on it
options = this.state.options; */
erp5_document._embedded._view = args[0]; .declareJob("forceDownload", function (attachment) {
erp5_document._now = Date.now(); // force refresh var attachment_data = attachment.target.response,
// We choose render instead of changeState because the new form can use filename = /(?:^|;)\s*filename\s*=\s*"?([^";]+)/i.exec(
// different page_template (reports are setup in form_dialog but rendered attachment.target.getResponseHeader("Content-Disposition") || ""
// in report_view). ),
// Validation provides document updated for error texts but uses the same a_tag = document.createElement("a");
// form thus the same view thus the same url - no DOM modifications
// if (attachment.target.responseType !== "blob") {
// We modify inplace state.options because render method uses and removes attachment_data = new Blob(
// erp5_document hidden in its options. [attachment.target.response],
options.erp5_document = erp5_document; {type: attachment.target.getResponseHeader("Content-Type")}
return this.render(options); );
}
a_tag.style = "display: none";
a_tag.href = URL.createObjectURL(attachment_data);
a_tag.download = filename ? filename[1].trim() : "untitled";
document.body.appendChild(a_tag);
a_tag.click();
return new RSVP.Queue()
.push(function () {
return RSVP.delay(10);
})
.push(function () {
URL.revokeObjectURL(a_tag.href);
document.body.removeChild(a_tag);
});
}); });
}(window, rJS, URI, RSVP, asBoolean));
\ No newline at end of file }(window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean));
\ No newline at end of file
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>966.2419.32114.61405</string> </value> <value> <string>966.61650.63360.24661</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>1523291888.74</float> <float>1524060387.5</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<!--
data-i18n=Input data has errors
data-i18n=You do not have the permissions to edit the object
data-i18n=Document was not saved! Resubmit when you are online or the document accessible
data-i18n=Encountered an unknown error. Try to resubmit
data-i18n=Data received
data-i18n=Action succeeded
-->
<head> <head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" /> <meta name="viewport" content="width=device-width, user-scalable=no" />
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>965.10309.12229.56627</string> </value> <value> <string>965.12118.35525.1655</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1517304075.06</float> <float>1524057259.75</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
/*jslint nomen: true, indent: 2, maxerr: 3 */ /*jslint nomen: true, indent: 2, maxerr: 3 */
/*global window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray */ /*global window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray */
(function (window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray) { (function (window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray) {
"use strict"; "use strict";
function submitDialog(gadget, submit_action_id, is_update_method) { function submitDialog(gadget, is_updating) {
var form_gadget = gadget,
action = form_gadget.state.erp5_document._embedded._view._actions.put,
form_id = form_gadget.state.erp5_document._embedded._view.form_id,
dialog_id = form_gadget.state.erp5_document._embedded._view.dialog_id,
redirect_to_parent;
return form_gadget.notifySubmitting() return gadget.getContent()
.push(function () {
return form_gadget.getDeclaredGadget("erp5_form");
})
.push(function (erp5_form) {
return erp5_form.getContent();
})
.push(function (content_dict) { .push(function (content_dict) {
var data = {}, var data = {},
key; key;
// In dialog form, dialog_id is mandatory and form_id is optional // create a copy of sub_data so we do not modify them in-place
data.dialog_id = dialog_id['default'];
if (form_id !== undefined) {
data.form_id = form_id['default'];
}
data.dialog_method = form_gadget.state.form_definition[submit_action_id];
if (is_update_method) {
data.update_method = data.dialog_method;
}
//XXX hack for redirect, difined in form
redirect_to_parent = content_dict.field_your_redirect_to_parent;
for (key in content_dict) { for (key in content_dict) {
if (content_dict.hasOwnProperty(key)) { if (content_dict.hasOwnProperty(key)) {
data[key] = content_dict[key]; data[key] = content_dict[key];
} }
} }
// ERP5 expects target Script name in dialog_method field
data.dialog_method = gadget.state.form_definition.action;
// For Update Action - override the default value from "action"
if (is_updating) {
data.dialog_method = gadget.state.form_definition.update_action;
data.update_method = gadget.state.form_definition.update_action;
}
return form_gadget.jio_putAttachment( return data;
form_gadget.state.jio_key, })
action.href, .push(function (data) {
return gadget.submitContent(
gadget.state.jio_key,
gadget.state.erp5_document._embedded._view._actions.put.href, // most likely points to Base_callDialogMethod
data data
); );
}) })
.push(function (attachment) { .push(function (jio_key) { // success redirect handler
var splitted_jio_key_list,
if (attachment.target.response.type === "application/json") { splitted_current_jio_key_list,
// successful form save returns simple redirect and answer as JSON command,
// validation errors are handled in failure branch on bottom i;
return new RSVP.Queue() if (is_updating) {
.push(function () { return;
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
return form_gadget.notifySubmitted({
"message": response.portal_status_message,
"status": "success"
});
})
.push(function () {
// here we figure out where to go after form submit - indicated
// by X-Location HTTP header placed by Base_redirect script
var jio_key = new URI(
attachment.target.getResponseHeader("X-Location")
).segment(2),
splitted_jio_key_list,
splitted_current_jio_key_list,
command,
i;
if (redirect_to_parent) {
return form_gadget.redirect({command: 'history_previous'});
}
if (form_gadget.state.jio_key === jio_key) {
// don't update navigation history when not really redirecting
return form_gadget.redirect({
command: 'change',
options: {
"jio_key": jio_key,
"view": "view",
"page": undefined
// do not mingle with editable because it isn't necessary
}
});
}
// Check if the redirection goes to a same parent's subdocument.
// In this case, do not add current document to the history
// example: when cloning, do not keep the original document in history
splitted_jio_key_list = jio_key.split('/');
splitted_current_jio_key_list = form_gadget.state.jio_key.split('/');
command = 'display_with_history';
if (splitted_jio_key_list.length === splitted_current_jio_key_list.length) {
for (i = 0; i < splitted_jio_key_list.length - 1; i += 1) {
if (splitted_jio_key_list[i] !== splitted_current_jio_key_list[i]) {
command = 'push_history';
}
}
} else {
command = 'push_history';
}
// forced document change thus we update history
return form_gadget.redirect({
command: command,
options: {
"jio_key": jio_key
// do not mingle with editable because it isn't necessary
}
});
});
}
if (attachment.target.response.type === "application/hal+json") {
// we have received a view definition thus we need to redirect
// this will happen only in report/export when "Format" is unspecified
return new RSVP.Queue()
.push(function () {
return form_gadget.notifySubmitted({
"message": "Data received",
"status": "success"
});
})
.push(function () {
return jIO.util.readBlobAsText(attachment.target.response);
})
.push(function (response_text) {
return form_gadget.updateForm(JSON.parse(response_text.target.result));
});
} }
// response status > 200 (e.g. 202 "Accepted" or 204 "No Content") if (!jio_key || gadget.state.redirect_to_parent) {
// mean sucessful execution of an action but does not carry any data return gadget.redirect({command: 'history_previous'});
// XMLHttpRequest automatically inserts Content-Type="text/xml" thus
// we cannot test based on that
if (attachment.target.response.size === 0 &&
attachment.target.status > 200 &&
attachment.target.status < 400) {
return new RSVP.Queue()
.push(function () {
return form_gadget.notifySubmitted({
"message": "Action succeeded",
"status": "success"
});
})
.push(function () {
if (redirect_to_parent) {
return form_gadget.redirect({command: 'history_previous'});
}
return form_gadget.redirect({
command: 'change',
options: {
"jio_key": form_gadget.state.jio_key,
"view": "view",
"page": undefined
// do not mingle with editable because it isn't necessary
}
});
});
} }
if (gadget.state.jio_key === jio_key) {
// any other attachment type we force to download because it is most // don't update navigation history when not really redirecting
// likely product of export/report (thus PDF, ODT ...) return gadget.redirect({
return new RSVP.Queue() command: 'change',
.push(function () { options: {
return form_gadget.notifySubmitted({ "jio_key": jio_key,
"message": "Data received", "view": "view",
"status": "success" "page": undefined
}); // do not mingle with editable because it isn't necessary
}) }
.push(function () {
return form_gadget.forceDownload(attachment);
}); });
}) }
.push(undefined, function (error) { // Check if the redirection goes to a same parent's subdocument.
if (error !== undefined && error.target !== undefined) { // In this case, do not add current document to the history
var error_text = 'Encountered an unknown error. Try to resubmit', // example: when cloning, do not keep the original document in history
promise_queue = new RSVP.Queue(); splitted_jio_key_list = jio_key.split('/');
// if we know what the error was, try to precise it for the user splitted_current_jio_key_list = gadget.state.jio_key.split('/');
if (error.target.status === 400) { command = 'display_with_history';
error_text = 'Input data has errors'; if (splitted_jio_key_list.length === splitted_current_jio_key_list.length) {
} else if (error.target.status === 403) { for (i = 0; i < splitted_jio_key_list.length - 1; i += 1) {
error_text = 'You do not have the permissions to edit the object'; if (splitted_jio_key_list[i] !== splitted_current_jio_key_list[i]) {
} else if (error.target.status === 0) { command = 'push_history';
error_text = 'Document was not saved! Resubmit when you are online or the document accessible'; }
}
// if the response type is json, then look for the status message
// sent from the portal. We prefer to have portal_status_message in
// all cases when we have error
if (error.target.response.type === 'application/json') {
promise_queue
.push(function () {
return jIO.util.readBlobAsText(error.target.response);
})
// Get the error_text from portal_status_message, if there is no
// portal_status_message, then use the default error_text
.push(function (response_text) {
var response = JSON.parse(response_text.target.result);
// If there is no portal_status_message, use the default
// error_text
error_text = response.portal_status_message || error_text;
});
}
// display translated error_text to user
promise_queue
.push(function () {
return form_gadget.notifySubmitted();
})
.push(function () {
return form_gadget.translate(error_text);
})
.push(function (message) {
return form_gadget.notifyChange({
"message": message + '.',
"status": "error"
});
});
// if server validation of form data failed (indicated by response code 400)
// we parse out field errors and display them to the user
if (error.target.status === 400 &&
error.target.response.type === 'application/hal+json') {
promise_queue
.push(function () {
// when the server-side validation returns the error description
if (error.target.responseType === "blob") {
return jIO.util.readBlobAsText(error.target.response);
}
// otherwise return (most-likely) textual response of the server
return {target: {result: error.target.response}};
})
.push(function (event) {
return form_gadget.displayFormulatorValidationError(JSON.parse(event.target.result));
});
} }
return promise_queue; } else {
command = 'push_history';
} }
throw error;
// forced document change thus we update history
return gadget.redirect({
command: command,
options: {
"jio_key": jio_key
// do not mingle with editable because it isn't necessary
}
});
}); });
// We do not handle submit failures because Page Form handles them well
// If any error bubbles here we do not know what to do with it anyway
} }
...@@ -249,20 +93,20 @@ ...@@ -249,20 +93,20 @@
dialog_button_template = Handlebars.compile(dialog_button_source); dialog_button_template = Handlebars.compile(dialog_button_source);
gadget_klass gadget_klass
.setState({
'redirect_to_parent': false, // set by a presence of special field
'has_update_action': undefined // default "submit" issue update in case of its presence
})
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// acquisition // acquisition
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("redirect", "redirect") .declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("getUrlFor", "getUrlFor") .declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("updateHeader", "updateHeader") .declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
.declareAcquiredMethod("translate", "translate") .declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("translateHtml", "translateHtml") .declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("notifyChange", "notifyChange") .declareAcquiredMethod("submitContent", "submitContent")
.declareAcquiredMethod("updateForm", "updateForm")
.declareAcquiredMethod("displayFormulatorValidationError", "displayFormulatorValidationError")
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// Proxy methods to the child gadget // Proxy methods to the child gadget
...@@ -273,12 +117,14 @@ ...@@ -273,12 +117,14 @@
return declared_gadget.checkValidity(); return declared_gadget.checkValidity();
}); });
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
return this.getDeclaredGadget("erp5_form") return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) { .push(function (sub_gadget) {
return declared_gadget.getContent(); return sub_gadget.getContent();
}); });
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// declared methods // declared methods
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -287,23 +133,27 @@ ...@@ -287,23 +133,27 @@
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
.declareMethod('render', function (options) { .declareMethod('render', function (options) {
var gadget = this;
// copy out wanted items from options and pass it to `changeState` // copy out wanted items from options and pass it to `changeState`
return this.changeState({ return gadget.changeState({
jio_key: options.jio_key, jio_key: options.jio_key,
view: options.view, view: options.view,
// ignore options.editable because dialog is always editable // ignore options.editable because dialog is always editable
erp5_document: options.erp5_document, erp5_document: options.erp5_document,
form_definition: options.form_definition, form_definition: options.form_definition,
erp5_form: options.erp5_form || {}, erp5_form: options.erp5_form || {},
// ignore global editable state (be always editable) // editable: true, // ignore global editable state (be always editable)
show_update_button: Boolean(options.form_definition.update_action) has_update_action: Boolean(options.form_definition.update_action),
// XXX Hack of ERP5 how to express redirect to parent after success
redirect_to_parent: options.erp5_document._embedded._view.field_your_redirect_to_parent !== undefined
}); });
}) })
.onStateChange(function (modification_dict) { .onStateChange(function (modification_dict) {
var form_gadget = this, var form_gadget = this,
selector = form_gadget.element.querySelector("h3"), selector = form_gadget.element.querySelector("h3"),
view_list = ensureArray(this.state.erp5_document._links.action_workflow), view_list,
icon, icon,
title, title,
i; i;
...@@ -339,9 +189,9 @@ ...@@ -339,9 +189,9 @@
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
// Set the dialog button // Set the dialog button
if (modification_dict.hasOwnProperty('show_update_button')) { if (modification_dict.hasOwnProperty('has_update_action')) {
return form_gadget.translateHtml(dialog_button_template({ return form_gadget.translateHtml(dialog_button_template({
show_update_button: form_gadget.state.show_update_button show_update_button: form_gadget.state.has_update_action
})) }))
.push(function (html) { .push(function (html) {
form_gadget.element.querySelector('.dialog_button_container') form_gadget.element.querySelector('.dialog_button_container')
...@@ -393,54 +243,23 @@ ...@@ -393,54 +243,23 @@
}); });
}) })
/** The only way how to force download from javascript (working everywhere)
* is unfortunately constructing <a> and clicking on it
*/
.declareJob("forceDownload", function (attachment) {
var attachment_data = attachment.target.response,
filename = /(?:^|;)\s*filename\s*=\s*"?([^";]+)/i.exec(
attachment.target.getResponseHeader("Content-Disposition") || ""
),
a_tag = document.createElement("a");
if (attachment.target.responseType !== "blob") {
attachment_data = new Blob(
[attachment.target.response],
{type: attachment.target.getResponseHeader("Content-Type")}
);
}
a_tag.style = "display: none";
a_tag.href = URL.createObjectURL(attachment_data);
a_tag.download = filename ? filename[1].trim() : "untitled";
document.body.appendChild(a_tag);
a_tag.click();
return new RSVP.Queue()
.push(function () {
return RSVP.delay(10);
})
.push(function () {
URL.revokeObjectURL(a_tag.href);
document.body.removeChild(a_tag);
});
})
.onEvent('submit', function () { .onEvent('submit', function () {
if (this.state.has_update_action === true) { if (this.state.has_update_action === true) {
return submitDialog(this, "update_action", true); // default action on submit is update in case of its existence
return submitDialog(this, true);
} }
return submitDialog(this, "action"); return submitDialog(this, false);
}, false, true) }, false, true)
.onEvent('click', function (evt) { .onEvent('click', function (evt) {
if (evt.target.name === "action_confirm") { if (evt.target.name === "action_confirm") {
evt.preventDefault(); evt.preventDefault();
return submitDialog(this, "action"); return submitDialog(this, false);
} }
if (evt.target.name === "action_update") { if (evt.target.name === "action_update") {
evt.preventDefault(); evt.preventDefault();
return submitDialog(this, "update_action", true); return submitDialog(this, true);
} }
}, false, false); }, false, false);
}(window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray)); }(window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray));
\ No newline at end of file
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<!--
data-i18n=Input data has errors
data-i18n=You do not have the permissions to edit the object
data-i18n=Document was not saved! Resubmit when you are online or the document accessible
data-i18n=Encountered an unknown error. Try to resubmit
-->
<head> <head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" /> <meta name="viewport" content="width=device-width, user-scalable=no" />
...@@ -21,7 +15,7 @@ ...@@ -21,7 +15,7 @@
</head> </head>
<body> <body>
<!--div data-gadget-url="gadget_erp5_tab_list.html" <!--div data-gadget-url="gadget_erp5_tab_list.html"
data-gadget-scope="erp5_tab" data-gadget-scope="erp5_tab"
data-gadget-sandbox="public"> data-gadget-sandbox="public">
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>960.5523.58984.43537</string> </value> <value> <string>960.56020.52206.40328</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1499432130.91</float> <float>1524057285.74</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
/*global window, rJS, RSVP, calculatePageTitle, jIO */ /*global window, rJS, RSVP, calculatePageTitle */
/*jslint nomen: true, indent: 2, maxerr: 3 */ /*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, calculatePageTitle, jIO) { (function (window, rJS, RSVP, calculatePageTitle) {
"use strict"; "use strict";
rJS(window) rJS(window)
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment") .declareAcquiredMethod("submitContent", "submitContent")
.declareAcquiredMethod("getUrlFor", "getUrlFor") .declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("redirect", "redirect") .declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate") .declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("updateHeader", "updateHeader") .declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
.declareAcquiredMethod("notifyChange", "notifyChange") .declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("displayFormulatorValidationError",
"displayFormulatorValidationError")
.declareAcquiredMethod('isDesktopMedia', 'isDesktopMedia') .declareAcquiredMethod('isDesktopMedia', 'isDesktopMedia')
.declareAcquiredMethod('getUrlParameter', 'getUrlParameter') .declareAcquiredMethod('getUrlParameter', 'getUrlParameter')
.allowPublicAcquisition("notifyChange", function () { .allowPublicAcquisition("notifyChange", function () {
...@@ -29,12 +25,14 @@ ...@@ -29,12 +25,14 @@
return declared_gadget.checkValidity(); return declared_gadget.checkValidity();
}); });
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
return this.getDeclaredGadget("erp5_form") return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) { .push(function (declared_gadget) {
return declared_gadget.getContent(); return declared_gadget.getContent();
}); });
}, {mutex: 'changestate'}) }, {mutex: 'changestate'})
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// declared methods // declared methods
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -64,44 +62,44 @@ ...@@ -64,44 +62,44 @@
}) })
.onStateChange(function () { .onStateChange(function () {
var form_gadget = this; var gadget = this;
// render the erp5 form // render the erp5 form
return form_gadget.getDeclaredGadget("erp5_form") return gadget.getDeclaredGadget("erp5_form")
.push(function (erp5_form) { .push(function (sub_gadget) {
var form_options = form_gadget.state.erp5_form; var form_options = gadget.state.erp5_form;
form_options.erp5_document = form_gadget.state.erp5_document; form_options.erp5_document = gadget.state.erp5_document;
form_options.form_definition = form_gadget.state.form_definition; form_options.form_definition = gadget.state.form_definition;
form_options.view = form_gadget.state.view; form_options.view = gadget.state.view;
form_options.jio_key = form_gadget.state.jio_key; form_options.jio_key = gadget.state.jio_key;
form_options.editable = 1; form_options.editable = 1;
return erp5_form.render(form_options); return sub_gadget.render(form_options);
}) })
// render the header // render the header
.push(function () { .push(function () {
return RSVP.all([ return RSVP.all([
form_gadget.getUrlFor({command: 'change', options: {page: "tab"}}), gadget.getUrlFor({command: 'change', options: {page: "tab"}}),
form_gadget.getUrlFor({command: 'change', options: {page: "action"}}), gadget.getUrlFor({command: 'change', options: {page: "action"}}),
form_gadget.state.erp5_document._links.action_object_new_content_action ? gadget.state.erp5_document._links.action_object_new_content_action ?
form_gadget.getUrlFor({command: 'change', options: { gadget.getUrlFor({command: 'change', options: {
view: form_gadget.state.erp5_document._links.action_object_new_content_action.href, view: gadget.state.erp5_document._links.action_object_new_content_action.href,
editable: true editable: true
}}) : }}) :
"", "",
form_gadget.getUrlFor({command: 'history_previous'}), gadget.getUrlFor({command: 'history_previous'}),
form_gadget.getUrlFor({command: 'selection_previous'}), gadget.getUrlFor({command: 'selection_previous'}),
form_gadget.getUrlFor({command: 'selection_next'}), gadget.getUrlFor({command: 'selection_next'}),
calculatePageTitle(form_gadget, form_gadget.state.erp5_document), calculatePageTitle(gadget, gadget.state.erp5_document),
form_gadget.isDesktopMedia(), gadget.isDesktopMedia(),
(form_gadget.state.erp5_document._links.action_object_jio_report || (gadget.state.erp5_document._links.action_object_jio_report ||
form_gadget.state.erp5_document._links.action_object_jio_exchange || gadget.state.erp5_document._links.action_object_jio_exchange ||
form_gadget.state.erp5_document._links.action_object_jio_print) ? gadget.state.erp5_document._links.action_object_jio_print) ?
form_gadget.getUrlFor({command: 'change', options: {page: "export"}}) : gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
"", "",
form_gadget.getUrlParameter('selection_index') gadget.getUrlParameter('selection_index')
]); ]);
}) })
.push(function (all_result) { .push(function (all_result) {
...@@ -117,13 +115,13 @@ ...@@ -117,13 +115,13 @@
page_title: all_result[6] page_title: all_result[6]
}, },
is_desktop = all_result[7]; is_desktop = all_result[7];
if (form_gadget.state.save_action === true) { if (gadget.state.save_action === true) {
header_dict.save_action = true; header_dict.save_action = true;
} }
if (is_desktop) { if (is_desktop) {
header_dict.export_url = all_result[8]; header_dict.export_url = all_result[8];
} }
return form_gadget.updateHeader(header_dict); return gadget.updateHeader(header_dict);
}); });
}) })
...@@ -134,103 +132,36 @@ ...@@ -134,103 +132,36 @@
return; return;
} }
var form_gadget = this, var gadget = this,
erp5_form, action = gadget.state.erp5_document._embedded._view._actions.put;
form_id = this.state.erp5_document._embedded._view.form_id,
action = form_gadget.state.erp5_document._embedded._view._actions.put;
return form_gadget.getDeclaredGadget("erp5_form") return gadget.getDeclaredGadget("erp5_form")
.push(function (gadget) { .push(function (sub_gadget) {
erp5_form = gadget; return sub_gadget.checkValidity();
return erp5_form.checkValidity();
}) })
.push(function (validity) { .push(function (is_valid) {
if (validity) { if (!is_valid) {
return form_gadget.notifySubmitting() return null;
.push(function () {
// try to send the form data over the network to jIO storage
return erp5_form.getContent();
})
.push(function (data) {
data[form_id.key] = form_id['default'];
return form_gadget.jio_putAttachment(
form_gadget.state.jio_key,
action.href,
data
);
})
// handle response from the server
.push(function (result) {
if (result.target.responseType === "blob") {
return jIO.util.readBlobAsText(result.target.response);
}
return {target: {result: result.target.response}};
})
.push(function (event) {
var message;
try {
message = JSON.parse(event.target.result).portal_status_message;
} catch (ignore) {
}
return form_gadget.notifySubmitted({
"message": message,
"status": "success"
});
})
.push(function () {
return form_gadget.redirect({command: 'reload'});
})
.push(undefined, function (error) {
if (error.target !== undefined) {
var error_text = 'Encountered an unknown error. Try to resubmit',
promise;
// improve error message if we can
if (error.target.status === 400) {
error_text = 'Input data has errors';
} else if (error.target.status === 403) {
error_text = 'You do not have the permissions to edit the object';
} else if (error.target.status === 0) {
// no/default=0 status means a network connection problem
error_text = 'Document was not saved! Resubmit when you are online or the document accessible';
}
// display translated error_text to user
promise = form_gadget.notifySubmitted()
.push(function () {
return form_gadget.translate(error_text);
})
.push(function (message) {
return form_gadget.notifyChange({
'message': message + '.',
'status': 'error'
});
});
// if server validation of form data failed (indicated by response code 400)
// we parse out field errors and display them to the user
if (error.target.status === 400) {
promise
.push(function () {
// when the server-side validation returns the error description
if (error.target.responseType === "blob") {
return jIO.util.readBlobAsText(error.target.response);
}
// otherwise return (most-likely) textual response of the server
return {target: {result: error.target.response}};
})
.push(function (event) {
return form_gadget.displayFormulatorValidationError(JSON.parse(event.target.result));
});
}
return promise;
}
// throwing an error is the last desperate option
throw error;
});
} }
}); return gadget.getContent();
})
.push(function (content_dict) {
if (content_dict === null) {
return;
}
return gadget.submitContent(
gadget.state.jio_key,
action.href,
content_dict
);
})
.push(function (jio_key) {
if (jio_key) {
// success redirect callback receives jio_key
return gadget.redirect({command: 'reload'});
}
}); // page form handles failures well enough
}, false, true); }, false, true);
}(window, rJS, RSVP, calculatePageTitle, jIO)); }(window, rJS, RSVP, calculatePageTitle));
\ No newline at end of file \ No newline at end of file
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
</item> </item>
<item> <item>
<key> <string>actor</string> </key> <key> <string>actor</string> </key>
<value> <string>zope</string> </value> <value> <string>superkato</string> </value>
</item> </item>
<item> <item>
<key> <string>comment</string> </key> <key> <string>comment</string> </key>
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>966.32967.51097.665</string> </value> <value> <string>966.61712.44180.13021</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>1522335846.96</float> <float>1524060534.16</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