2013-01-22 19:32:26 +01:00
|
|
|
/**
|
|
|
|
* @requires javelin-stratcom
|
|
|
|
* javelin-request
|
|
|
|
* javelin-dom
|
|
|
|
* javelin-vector
|
|
|
|
* javelin-install
|
|
|
|
* javelin-util
|
|
|
|
* javelin-mask
|
|
|
|
* javelin-uri
|
Provide a global router for Ajax requests
Summary:
Fixes T430. Fixes T4834. Obsoletes D7641. Currently, we do some things less-well than we could:
- We just let the browser queue and prioritize requests, so if you load a revision with 50 changes and then click "Award Token", the action blocks until the changes load in most/all browsers. It would be better to prioritize this action and queue it immediately.
- Similarly, changes tend to load in order, even if the user has clicked to a specific file. When the user expresses a preference for a specific file, we should prioritize it.
- We show a spinning GIF when waiting on requests. This is appropriate for some types of reuqests, but distracting for others.
To fix this:
- Queue all (or, at least, most) requests into a new queue in JX.Router.
- JX.Router handles prioritizing the requests. Principally:
- You can submit a request with a specific priority (500 = general content loading, 1000 = default, 2000 = explicit user action) and JX.Router will get the higher stuff fired off sooner.
- You can name requests and then adjust their prorities later, if the user expresses an interest in specific results.
- Only use the spinner gif for "workflow" requests, which is bascially when the user clicked something and we're waiting on the server. I think it's useful and not-annoying in this case.
- Don't show any status for draft requests.
- For content requests, show a subtle hipster-style top loading bar.
Test Plan:
- Viewed a diff with 93 changes, and clicked award token.
- Prior to this patch, the action took many many seconds to resolve.
- After this patch, it resolves quickly.
- Viewed a diff with 93 changes and saw a pleasant subtle hipster-style loading bar.
- Viewed a diff with 93 changes and typed some draft text. Previews populated fairly quickly and there was no spinner.
- Viewed a diff with 93 changes and clicked something with workflow, saw a spinner after a moment.
- Viewed a diff with 93 changes and clicked a file in the table of contents near the end of the list.
- Prior to this patch, it took a long time to show up.
- After this patch, it loads directly.
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T430, T4834
Differential Revision: https://secure.phabricator.com/D8979
2014-05-05 19:57:42 +02:00
|
|
|
* javelin-routable
|
2013-01-22 19:32:26 +01:00
|
|
|
* @provides javelin-workflow
|
|
|
|
* @javelin
|
|
|
|
*/
|
|
|
|
|
|
|
|
JX.install('Workflow', {
|
|
|
|
construct : function(uri, data) {
|
|
|
|
if (__DEV__) {
|
|
|
|
if (!uri || uri == '#') {
|
|
|
|
JX.$E(
|
|
|
|
'new JX.Workflow(<?>, ...): '+
|
|
|
|
'bogus URI provided when creating workflow.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.setURI(uri);
|
|
|
|
this.setData(data || {});
|
|
|
|
},
|
|
|
|
|
|
|
|
events : ['error', 'finally', 'submit'],
|
|
|
|
|
|
|
|
statics : {
|
|
|
|
_stack : [],
|
|
|
|
newFromForm : function(form, data) {
|
|
|
|
var pairs = JX.DOM.convertFormToListOfPairs(form);
|
|
|
|
for (var k in data) {
|
|
|
|
pairs.push([k, data[k]]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Disable form elements during the request
|
|
|
|
var inputs = [].concat(
|
|
|
|
JX.DOM.scry(form, 'input'),
|
|
|
|
JX.DOM.scry(form, 'button'),
|
|
|
|
JX.DOM.scry(form, 'textarea'));
|
|
|
|
for (var ii = 0; ii < inputs.length; ii++) {
|
|
|
|
if (inputs[ii].disabled) {
|
|
|
|
delete inputs[ii];
|
|
|
|
} else {
|
|
|
|
inputs[ii].disabled = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var workflow = new JX.Workflow(form.getAttribute('action'), {});
|
|
|
|
workflow.setDataWithListOfPairs(pairs);
|
|
|
|
workflow.setMethod(form.getAttribute('method'));
|
|
|
|
workflow.listen('finally', function() {
|
|
|
|
// Re-enable form elements
|
|
|
|
for (var ii = 0; ii < inputs.length; ii++) {
|
|
|
|
inputs[ii] && (inputs[ii].disabled = false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return workflow;
|
|
|
|
},
|
|
|
|
newFromLink : function(link) {
|
|
|
|
var workflow = new JX.Workflow(link.href);
|
|
|
|
return workflow;
|
|
|
|
},
|
|
|
|
_push : function(workflow) {
|
|
|
|
JX.Mask.show();
|
|
|
|
JX.Workflow._stack.push(workflow);
|
|
|
|
},
|
|
|
|
_pop : function() {
|
|
|
|
var dialog = JX.Workflow._stack.pop();
|
|
|
|
(dialog.getCloseHandler() || JX.bag)();
|
|
|
|
dialog._destroy();
|
|
|
|
JX.Mask.hide();
|
|
|
|
},
|
|
|
|
disable : function() {
|
|
|
|
JX.Workflow._disabled = true;
|
|
|
|
},
|
|
|
|
_onbutton : function(event) {
|
|
|
|
|
|
|
|
if (JX.Stratcom.pass()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (JX.Workflow._disabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-02-15 16:47:14 +01:00
|
|
|
// Get the button (which is sometimes actually another tag, like an <a />)
|
|
|
|
// which triggered the event. In particular, this makes sure we get the
|
|
|
|
// right node if there is a <button> with an <img /> inside it or
|
|
|
|
// or something similar.
|
|
|
|
var t = event.getNode('jx-workflow-button') ||
|
|
|
|
event.getNode('tag:button');
|
2014-03-13 02:17:11 +01:00
|
|
|
|
|
|
|
// If this button disables workflow (normally, because it is a file
|
|
|
|
// download button) let the event through without modification.
|
|
|
|
if (JX.Stratcom.getData(t).disableWorkflow) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.prevent();
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
if (t.name == '__cancel__' || t.name == '__close__') {
|
|
|
|
JX.Workflow._pop();
|
|
|
|
} else {
|
|
|
|
var form = event.getNode('jx-dialog');
|
2014-05-03 03:01:16 +02:00
|
|
|
JX.Workflow._dosubmit(form, t);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_onsyntheticsubmit : function(e) {
|
|
|
|
if (JX.Stratcom.pass()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (JX.Workflow._disabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
e.prevent();
|
|
|
|
var form = e.getNode('jx-dialog');
|
|
|
|
var button = JX.DOM.find(form, 'button', '__default__');
|
|
|
|
JX.Workflow._dosubmit(form, button);
|
|
|
|
},
|
|
|
|
_dosubmit : function(form, button) {
|
|
|
|
// Issue a DOM event first, so form-oriented handlers can act.
|
|
|
|
var dom_event = JX.DOM.invoke(form, 'didWorkflowSubmit');
|
|
|
|
if (dom_event.getPrevented()) {
|
|
|
|
return;
|
|
|
|
}
|
2013-10-14 20:58:38 +02:00
|
|
|
|
2014-05-03 03:01:16 +02:00
|
|
|
var data = JX.DOM.convertFormToListOfPairs(form);
|
|
|
|
data.push([button.name, button.value || true]);
|
|
|
|
|
|
|
|
var active = JX.Workflow._getActiveWorkflow();
|
|
|
|
var e = active.invoke('submit', {form: form, data: data});
|
|
|
|
if (!e.getStopped()) {
|
|
|
|
active._destroy();
|
|
|
|
active
|
|
|
|
.setURI(form.getAttribute('action') || active.getURI())
|
|
|
|
.setDataWithListOfPairs(data)
|
|
|
|
.start();
|
2013-01-22 19:32:26 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
_getActiveWorkflow : function() {
|
|
|
|
var stack = JX.Workflow._stack;
|
|
|
|
return stack[stack.length - 1];
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
members : {
|
|
|
|
_root : null,
|
|
|
|
_pushed : false,
|
|
|
|
_data : null,
|
|
|
|
_onload : function(r) {
|
|
|
|
// It is permissible to send back a falsey redirect to force a page
|
|
|
|
// reload, so we need to take this branch if the key is present.
|
|
|
|
if (r && (typeof r.redirect != 'undefined')) {
|
|
|
|
JX.$U(r.redirect).go();
|
|
|
|
} else if (r && r.dialog) {
|
|
|
|
this._push();
|
|
|
|
this._root = JX.$N(
|
|
|
|
'div',
|
|
|
|
{className: 'jx-client-dialog'},
|
|
|
|
JX.$H(r.dialog));
|
|
|
|
JX.DOM.listen(
|
|
|
|
this._root,
|
|
|
|
'click',
|
|
|
|
[['jx-workflow-button'], ['tag:button']],
|
|
|
|
JX.Workflow._onbutton);
|
2014-05-03 03:01:16 +02:00
|
|
|
JX.DOM.listen(
|
|
|
|
this._root,
|
|
|
|
'didSyntheticSubmit',
|
|
|
|
[],
|
|
|
|
JX.Workflow._onsyntheticsubmit);
|
2015-01-27 16:11:20 +01:00
|
|
|
|
2015-01-29 16:10:14 +01:00
|
|
|
// Note that even in the presence of a content frame, we're doing
|
|
|
|
// everything here at top level: dialogs are fully modal and cover
|
|
|
|
// the entire window.
|
|
|
|
|
|
|
|
document.body.appendChild(this._root);
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
var d = JX.Vector.getDim(this._root);
|
2015-01-29 16:10:14 +01:00
|
|
|
var v = JX.Vector.getViewport();
|
|
|
|
var s = JX.Vector.getScroll();
|
2013-05-31 03:55:25 +02:00
|
|
|
|
|
|
|
// Normally, we position dialogs 100px from the top of the screen.
|
|
|
|
// Use more space if the dialog is large (at least roughly the size
|
|
|
|
// of the viewport).
|
|
|
|
var offset = Math.min(Math.max(20, (v.y - d.y) / 2), 100);
|
|
|
|
JX.$V((v.x - d.x) / 2, s.y + offset).setPos(this._root);
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
try {
|
|
|
|
JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__'));
|
|
|
|
var inputs = JX.DOM.scry(this._root, 'input')
|
|
|
|
.concat(JX.DOM.scry(this._root, 'textarea'));
|
|
|
|
var miny = Number.POSITIVE_INFINITY;
|
|
|
|
var target = null;
|
|
|
|
for (var ii = 0; ii < inputs.length; ++ii) {
|
|
|
|
if (inputs[ii].type != 'hidden') {
|
|
|
|
// Find the topleft-most displayed element.
|
|
|
|
var p = JX.$V(inputs[ii]);
|
|
|
|
if (p.y < miny) {
|
|
|
|
miny = p.y;
|
|
|
|
target = inputs[ii];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
target && JX.DOM.focus(target);
|
|
|
|
} catch (_ignored) {}
|
2013-05-31 03:55:25 +02:00
|
|
|
|
|
|
|
// The `focus()` call may have scrolled the window. Scroll it back to
|
|
|
|
// where it was before -- we want to focus the control, but not adjust
|
|
|
|
// the scroll position.
|
2015-01-29 16:10:14 +01:00
|
|
|
|
|
|
|
// Dialogs are window-level, so scroll the window explicitly.
|
|
|
|
window.scrollTo(s.x, s.y);
|
2013-05-31 03:55:25 +02:00
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
} else if (this.getHandler()) {
|
|
|
|
this.getHandler()(r);
|
|
|
|
this._pop();
|
|
|
|
} else if (r) {
|
|
|
|
if (__DEV__) {
|
|
|
|
JX.$E('Response to workflow request went unhandled.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_push : function() {
|
|
|
|
if (!this._pushed) {
|
|
|
|
this._pushed = true;
|
|
|
|
JX.Workflow._push(this);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_pop : function() {
|
|
|
|
if (this._pushed) {
|
|
|
|
this._pushed = false;
|
|
|
|
JX.Workflow._pop();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_destroy : function() {
|
|
|
|
if (this._root) {
|
|
|
|
JX.DOM.remove(this._root);
|
|
|
|
this._root = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
start : function() {
|
|
|
|
var uri = this.getURI();
|
|
|
|
var method = this.getMethod();
|
|
|
|
var r = new JX.Request(uri, JX.bind(this, this._onload));
|
|
|
|
var list_of_pairs = this._data;
|
|
|
|
list_of_pairs.push(['__wflow__', true]);
|
|
|
|
r.setDataWithListOfPairs(list_of_pairs);
|
|
|
|
r.setDataSerializer(this.getDataSerializer());
|
|
|
|
if (method) {
|
|
|
|
r.setMethod(method);
|
|
|
|
}
|
|
|
|
r.listen('finally', JX.bind(this, this.invoke, 'finally'));
|
|
|
|
r.listen('error', JX.bind(this, function(error) {
|
|
|
|
var e = this.invoke('error', error);
|
|
|
|
if (e.getStopped()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// TODO: Default error behavior? On Facebook Lite, we just shipped the
|
|
|
|
// user to "/error/". We could emit a blanket 'workflow-failed' type
|
|
|
|
// event instead.
|
|
|
|
}));
|
|
|
|
r.send();
|
|
|
|
},
|
|
|
|
|
Provide a global router for Ajax requests
Summary:
Fixes T430. Fixes T4834. Obsoletes D7641. Currently, we do some things less-well than we could:
- We just let the browser queue and prioritize requests, so if you load a revision with 50 changes and then click "Award Token", the action blocks until the changes load in most/all browsers. It would be better to prioritize this action and queue it immediately.
- Similarly, changes tend to load in order, even if the user has clicked to a specific file. When the user expresses a preference for a specific file, we should prioritize it.
- We show a spinning GIF when waiting on requests. This is appropriate for some types of reuqests, but distracting for others.
To fix this:
- Queue all (or, at least, most) requests into a new queue in JX.Router.
- JX.Router handles prioritizing the requests. Principally:
- You can submit a request with a specific priority (500 = general content loading, 1000 = default, 2000 = explicit user action) and JX.Router will get the higher stuff fired off sooner.
- You can name requests and then adjust their prorities later, if the user expresses an interest in specific results.
- Only use the spinner gif for "workflow" requests, which is bascially when the user clicked something and we're waiting on the server. I think it's useful and not-annoying in this case.
- Don't show any status for draft requests.
- For content requests, show a subtle hipster-style top loading bar.
Test Plan:
- Viewed a diff with 93 changes, and clicked award token.
- Prior to this patch, the action took many many seconds to resolve.
- After this patch, it resolves quickly.
- Viewed a diff with 93 changes and saw a pleasant subtle hipster-style loading bar.
- Viewed a diff with 93 changes and typed some draft text. Previews populated fairly quickly and there was no spinner.
- Viewed a diff with 93 changes and clicked something with workflow, saw a spinner after a moment.
- Viewed a diff with 93 changes and clicked a file in the table of contents near the end of the list.
- Prior to this patch, it took a long time to show up.
- After this patch, it loads directly.
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T430, T4834
Differential Revision: https://secure.phabricator.com/D8979
2014-05-05 19:57:42 +02:00
|
|
|
getRoutable: function() {
|
|
|
|
var routable = new JX.Routable();
|
|
|
|
routable.listen('start', JX.bind(this, function() {
|
|
|
|
// Pass the event to allow other listeners to "start" to configure this
|
|
|
|
// workflow before it fires.
|
|
|
|
JX.Stratcom.pass(JX.Stratcom.context());
|
|
|
|
this.start();
|
|
|
|
}));
|
|
|
|
this.listen('finally', JX.bind(routable, routable.done));
|
|
|
|
return routable;
|
|
|
|
},
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
setData : function(dictionary) {
|
|
|
|
this._data = [];
|
|
|
|
for (var k in dictionary) {
|
|
|
|
this._data.push([k, dictionary[k]]);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
setDataWithListOfPairs : function(list_of_pairs) {
|
|
|
|
this._data = list_of_pairs;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
properties : {
|
|
|
|
handler : null,
|
|
|
|
closeHandler : null,
|
|
|
|
dataSerializer : null,
|
|
|
|
method : null,
|
|
|
|
URI : null
|
|
|
|
},
|
|
|
|
|
|
|
|
initialize : function() {
|
|
|
|
|
|
|
|
function close_dialog_when_user_presses_escape(e) {
|
|
|
|
if (e.getSpecialKey() != 'esc') {
|
|
|
|
// Some key other than escape.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (JX.Workflow._disabled) {
|
|
|
|
// Workflows are disabled on this page.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (JX.Stratcom.pass()) {
|
|
|
|
// Something else swallowed the event.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var active = JX.Workflow._getActiveWorkflow();
|
|
|
|
if (!active) {
|
|
|
|
// No active workflow.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: the cancel button is actually an <a /> tag.
|
|
|
|
var buttons = JX.DOM.scry(active._root, 'a', 'jx-workflow-button');
|
|
|
|
if (!buttons.length) {
|
|
|
|
// No buttons in the dialog.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var cancel = null;
|
|
|
|
for (var ii = 0; ii < buttons.length; ii++) {
|
|
|
|
if (buttons[ii].name == '__cancel__') {
|
|
|
|
cancel = buttons[ii];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cancel) {
|
|
|
|
// No 'Cancel' button.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
JX.Workflow._pop();
|
|
|
|
e.prevent();
|
2013-05-19 02:04:22 +02:00
|
|
|
}
|
2013-01-22 19:32:26 +01:00
|
|
|
|
|
|
|
JX.Stratcom.listen('keydown', null, close_dialog_when_user_presses_escape);
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|