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):
field_list.append((field.id, renderRawField(field)))
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["title"] = Base_translateString(form.getTitle())
response_dict["pt"] = form.pt
......
......@@ -761,6 +761,28 @@ class TestERP5Document_getHateoas_mode_traverse(ERP5HALJSONStyleSkinsMixin):
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',
'return "http://example.org/bar"')
@simulate('Base_getRequestHeader', '*args, **kwargs',
......
<!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>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
......
......@@ -220,7 +220,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>961.19210.8471.60620</string> </value>
<value> <string>966.41656.42235.61815</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1518685706.64</float>
<float>1523287594.73</float>
<string>UTC</string>
</tuple>
</state>
......
/*global window, rJS, URI, RSVP, asBoolean */
/*global window, document, rJS, URI, RSVP, jIO, Blob, URL, asBoolean */
/*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";
/** Return local modifications to editable form fields after leaving the form
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
*/
function loadFormContent(gadget, result) {
......@@ -44,9 +51,14 @@
// Acquired methods
/////////////////////////////////////////////////////////////////
.declareAcquiredMethod("jio_getAttachment", "jio_getAttachment")
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("jio_allDocs", "jio_allDocs")
.declareAcquiredMethod("updatePanel", "updatePanel")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
/////////////////////////////////////////////////////////////////
// Proxy methods to the child gadget
......@@ -63,8 +75,12 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.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) {
return declared_gadget.getContent();
});
......@@ -163,7 +179,7 @@
.onStateChange(function (modification_dict) {
var queue,
gadget = this,
options = this.state.options,
options = gadget.state.options,
page_template_gadget,
erp5_document = JSON.parse(gadget.state.erp5_document),
erp5_form = JSON.parse(gadget.state.erp5_form);
......@@ -216,30 +232,279 @@
}
});
})
.allowPublicAcquisition("displayFormulatorValidationError", function (param_list) {
var erp5_document = JSON.parse(this.state.erp5_document);
erp5_document._embedded._view = param_list[0];
// Force refresh
erp5_document._now = Date.now();
/** SubmitContent should be called by the gadget which renders submit button
thus should handle the submit event.
It calls getContent on the child gadget and submits those data to given
jio_key and URL using JIO putAttachment call.
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) {
var erp5_document = JSON.parse(this.state.erp5_document),
options = this.state.options;
erp5_document._embedded._view = args[0];
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 this.render(options);
/** 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);
});
});
}(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 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>966.2419.32114.61405</string> </value>
<value> <string>966.61650.63360.24661</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1523291888.74</float>
<float>1524060387.5</float>
<string>UTC</string>
</tuple>
</state>
......
<!DOCTYPE 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>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
......
......@@ -220,7 +220,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>965.10309.12229.56627</string> </value>
<value> <string>965.12118.35525.1655</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1517304075.06</float>
<float>1524057259.75</float>
<string>UTC</string>
</tuple>
</state>
......
/*jslint nomen: true, indent: 2, maxerr: 3 */
/*global window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray */
(function (window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray) {
/*global window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray */
(function (window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray) {
"use strict";
function submitDialog(gadget, submit_action_id, is_update_method) {
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;
function submitDialog(gadget, is_updating) {
return form_gadget.notifySubmitting()
.push(function () {
return form_gadget.getDeclaredGadget("erp5_form");
})
.push(function (erp5_form) {
return erp5_form.getContent();
})
return gadget.getContent()
.push(function (content_dict) {
var data = {},
key;
// In dialog form, dialog_id is mandatory and form_id is optional
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;
// create a copy of sub_data so we do not modify them in-place
for (key in content_dict) {
if (content_dict.hasOwnProperty(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(
form_gadget.state.jio_key,
action.href,
return data;
})
.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
);
})
.push(function (attachment) {
if (attachment.target.response.type === "application/json") {
// successful form save returns simple redirect and answer as JSON
// validation errors are handled in failure branch on bottom
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 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));
});
.push(function (jio_key) { // success redirect handler
var splitted_jio_key_list,
splitted_current_jio_key_list,
command,
i;
if (is_updating) {
return;
}
// response status > 200 (e.g. 202 "Accepted" or 204 "No Content")
// mean sucessful execution of an 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 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 (!jio_key || gadget.state.redirect_to_parent) {
return gadget.redirect({command: 'history_previous'});
}
// any other attachment type we force to download because it is most
// likely product of export/report (thus PDF, ODT ...)
return new RSVP.Queue()
.push(function () {
return form_gadget.notifySubmitted({
"message": "Data received",
"status": "success"
});
})
.push(function () {
return form_gadget.forceDownload(attachment);
if (gadget.state.jio_key === jio_key) {
// don't update navigation history when not really redirecting
return gadget.redirect({
command: 'change',
options: {
"jio_key": jio_key,
"view": "view",
"page": undefined
// do not mingle with editable because it isn't necessary
}
});
})
.push(undefined, function (error) {
if (error !== undefined && error.target !== undefined) {
var error_text = 'Encountered an unknown error. Try to resubmit',
promise_queue = new RSVP.Queue();
// if we know what the error was, try to precise it for the user
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 = '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));
});
}
// 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 = 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';
}
}
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 @@
dialog_button_template = Handlebars.compile(dialog_button_source);
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
/////////////////////////////////////////////////////////////////
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("translateHtml", "translateHtml")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("updateForm", "updateForm")
.declareAcquiredMethod("displayFormulatorValidationError", "displayFormulatorValidationError")
.declareAcquiredMethod("submitContent", "submitContent")
/////////////////////////////////////////////////////////////////
// Proxy methods to the child gadget
......@@ -273,12 +117,14 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.declareMethod('getContent', function () {
return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) {
return declared_gadget.getContent();
.push(function (sub_gadget) {
return sub_gadget.getContent();
});
}, {mutex: 'changestate'})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
......@@ -287,23 +133,27 @@
}, {mutex: 'changestate'})
.declareMethod('render', function (options) {
var gadget = this;
// copy out wanted items from options and pass it to `changeState`
return this.changeState({
return gadget.changeState({
jio_key: options.jio_key,
view: options.view,
// ignore options.editable because dialog is always editable
erp5_document: options.erp5_document,
form_definition: options.form_definition,
erp5_form: options.erp5_form || {},
// ignore global editable state (be always editable)
show_update_button: Boolean(options.form_definition.update_action)
// editable: true, // ignore global editable state (be always editable)
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) {
var form_gadget = this,
selector = form_gadget.element.querySelector("h3"),
view_list = ensureArray(this.state.erp5_document._links.action_workflow),
view_list,
icon,
title,
i;
......@@ -339,9 +189,9 @@
return new RSVP.Queue()
.push(function () {
// 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({
show_update_button: form_gadget.state.show_update_button
show_update_button: form_gadget.state.has_update_action
}))
.push(function (html) {
form_gadget.element.querySelector('.dialog_button_container')
......@@ -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 () {
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)
.onEvent('click', function (evt) {
if (evt.target.name === "action_confirm") {
evt.preventDefault();
return submitDialog(this, "action");
return submitDialog(this, false);
}
if (evt.target.name === "action_update") {
evt.preventDefault();
return submitDialog(this, "update_action", true);
return submitDialog(this, true);
}
}, false, false);
}(window, rJS, RSVP, URI, calculatePageTitle, Blob, URL, document, jIO, Handlebars, ensureArray));
\ No newline at end of file
}(window, rJS, RSVP, calculatePageTitle, Handlebars, ensureArray));
<!DOCTYPE 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>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
......@@ -21,7 +15,7 @@
</head>
<body>
<!--div data-gadget-url="gadget_erp5_tab_list.html"
data-gadget-scope="erp5_tab"
data-gadget-sandbox="public">
......
......@@ -220,7 +220,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>960.5523.58984.43537</string> </value>
<value> <string>960.56020.52206.40328</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1499432130.91</float>
<float>1524057285.74</float>
<string>UTC</string>
</tuple>
</state>
......
/*global window, rJS, RSVP, calculatePageTitle, jIO */
/*global window, rJS, RSVP, calculatePageTitle */
/*jslint nomen: true, indent: 2, maxerr: 3 */
(function (window, rJS, RSVP, calculatePageTitle, jIO) {
(function (window, rJS, RSVP, calculatePageTitle) {
"use strict";
rJS(window)
.declareAcquiredMethod("jio_putAttachment", "jio_putAttachment")
.declareAcquiredMethod("submitContent", "submitContent")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("translate", "translate")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("notifySubmitting", "notifySubmitting")
.declareAcquiredMethod("notifySubmitted", "notifySubmitted")
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareAcquiredMethod("displayFormulatorValidationError",
"displayFormulatorValidationError")
.declareAcquiredMethod('isDesktopMedia', 'isDesktopMedia')
.declareAcquiredMethod('getUrlParameter', 'getUrlParameter')
.allowPublicAcquisition("notifyChange", function () {
......@@ -29,12 +25,14 @@
return declared_gadget.checkValidity();
});
}, {mutex: 'changestate'})
.declareMethod('getContent', function () {
return this.getDeclaredGadget("erp5_form")
.push(function (declared_gadget) {
return declared_gadget.getContent();
});
}, {mutex: 'changestate'})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
......@@ -64,44 +62,44 @@
})
.onStateChange(function () {
var form_gadget = this;
var gadget = this;
// render the erp5 form
return form_gadget.getDeclaredGadget("erp5_form")
.push(function (erp5_form) {
var form_options = form_gadget.state.erp5_form;
form_options.erp5_document = form_gadget.state.erp5_document;
form_options.form_definition = form_gadget.state.form_definition;
form_options.view = form_gadget.state.view;
form_options.jio_key = form_gadget.state.jio_key;
return gadget.getDeclaredGadget("erp5_form")
.push(function (sub_gadget) {
var form_options = gadget.state.erp5_form;
form_options.erp5_document = gadget.state.erp5_document;
form_options.form_definition = gadget.state.form_definition;
form_options.view = gadget.state.view;
form_options.jio_key = gadget.state.jio_key;
form_options.editable = 1;
return erp5_form.render(form_options);
return sub_gadget.render(form_options);
})
// render the header
.push(function () {
return RSVP.all([
form_gadget.getUrlFor({command: 'change', options: {page: "tab"}}),
form_gadget.getUrlFor({command: 'change', options: {page: "action"}}),
form_gadget.state.erp5_document._links.action_object_new_content_action ?
form_gadget.getUrlFor({command: 'change', options: {
view: form_gadget.state.erp5_document._links.action_object_new_content_action.href,
gadget.getUrlFor({command: 'change', options: {page: "tab"}}),
gadget.getUrlFor({command: 'change', options: {page: "action"}}),
gadget.state.erp5_document._links.action_object_new_content_action ?
gadget.getUrlFor({command: 'change', options: {
view: gadget.state.erp5_document._links.action_object_new_content_action.href,
editable: true
}}) :
"",
form_gadget.getUrlFor({command: 'history_previous'}),
form_gadget.getUrlFor({command: 'selection_previous'}),
form_gadget.getUrlFor({command: 'selection_next'}),
calculatePageTitle(form_gadget, form_gadget.state.erp5_document),
form_gadget.isDesktopMedia(),
(form_gadget.state.erp5_document._links.action_object_jio_report ||
form_gadget.state.erp5_document._links.action_object_jio_exchange ||
form_gadget.state.erp5_document._links.action_object_jio_print) ?
form_gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
gadget.getUrlFor({command: 'history_previous'}),
gadget.getUrlFor({command: 'selection_previous'}),
gadget.getUrlFor({command: 'selection_next'}),
calculatePageTitle(gadget, gadget.state.erp5_document),
gadget.isDesktopMedia(),
(gadget.state.erp5_document._links.action_object_jio_report ||
gadget.state.erp5_document._links.action_object_jio_exchange ||
gadget.state.erp5_document._links.action_object_jio_print) ?
gadget.getUrlFor({command: 'change', options: {page: "export"}}) :
"",
form_gadget.getUrlParameter('selection_index')
gadget.getUrlParameter('selection_index')
]);
})
.push(function (all_result) {
......@@ -117,13 +115,13 @@
page_title: all_result[6]
},
is_desktop = all_result[7];
if (form_gadget.state.save_action === true) {
if (gadget.state.save_action === true) {
header_dict.save_action = true;
}
if (is_desktop) {
header_dict.export_url = all_result[8];
}
return form_gadget.updateHeader(header_dict);
return gadget.updateHeader(header_dict);
});
})
......@@ -134,103 +132,36 @@
return;
}
var form_gadget = this,
erp5_form,
form_id = this.state.erp5_document._embedded._view.form_id,
action = form_gadget.state.erp5_document._embedded._view._actions.put;
var gadget = this,
action = gadget.state.erp5_document._embedded._view._actions.put;
return form_gadget.getDeclaredGadget("erp5_form")
.push(function (gadget) {
erp5_form = gadget;
return erp5_form.checkValidity();
return gadget.getDeclaredGadget("erp5_form")
.push(function (sub_gadget) {
return sub_gadget.checkValidity();
})
.push(function (validity) {
if (validity) {
return form_gadget.notifySubmitting()
.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;
});
.push(function (is_valid) {
if (!is_valid) {
return null;
}
});
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);
}(window, rJS, RSVP, calculatePageTitle, jIO));
\ No newline at end of file
}(window, rJS, RSVP, calculatePageTitle));
\ No newline at end of file
......@@ -216,7 +216,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
<value> <string>superkato</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>966.32967.51097.665</string> </value>
<value> <string>966.61712.44180.13021</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1522335846.96</float>
<float>1524060534.16</float>
<string>UTC</string>
</tuple>
</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