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 || {});
|
|
|
|
},
|
|
|
|
|
2016-05-20 15:20:35 +02:00
|
|
|
events : ['error', 'finally', 'submit', 'start'],
|
2013-01-22 19:32:26 +01:00
|
|
|
|
|
|
|
statics : {
|
|
|
|
_stack : [],
|
2015-03-14 20:00:17 +01:00
|
|
|
newFromForm : function(form, data, keep_enabled) {
|
2013-01-22 19:32:26 +01:00
|
|
|
var pairs = JX.DOM.convertFormToListOfPairs(form);
|
|
|
|
for (var k in data) {
|
|
|
|
pairs.push([k, data[k]]);
|
|
|
|
}
|
|
|
|
|
2015-03-14 20:00:17 +01:00
|
|
|
var inputs;
|
|
|
|
if (keep_enabled) {
|
|
|
|
inputs = [];
|
|
|
|
} else {
|
|
|
|
// Disable form elements during the request
|
|
|
|
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;
|
|
|
|
}
|
2013-01-22 19:32:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var workflow = new JX.Workflow(form.getAttribute('action'), {});
|
2016-05-20 15:20:35 +02:00
|
|
|
|
|
|
|
workflow._form = form;
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
workflow.setDataWithListOfPairs(pairs);
|
|
|
|
workflow.setMethod(form.getAttribute('method'));
|
2018-03-21 19:42:58 +01:00
|
|
|
|
|
|
|
var onfinally = JX.bind(workflow, function() {
|
|
|
|
if (!this._keepControlsDisabled) {
|
|
|
|
for (var ii = 0; ii < inputs.length; ii++) {
|
|
|
|
inputs[ii] && (inputs[ii].disabled = false);
|
|
|
|
}
|
2013-01-22 19:32:26 +01:00
|
|
|
}
|
|
|
|
});
|
2018-03-21 19:42:58 +01:00
|
|
|
workflow.listen('finally', onfinally);
|
2015-03-14 20:00:17 +01:00
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
return workflow;
|
|
|
|
},
|
|
|
|
newFromLink : function(link) {
|
|
|
|
var workflow = new JX.Workflow(link.href);
|
|
|
|
return workflow;
|
|
|
|
},
|
When a user clicks a navigation link in a dialog, close the dialog
Summary:
Ref T13302. Currently, if you enable Quicksand (by clicking "Persistent Chat"), open a dialog with links in it (like "Create Subtask" with multiple available subtypes), and then follow a navigation link, the page content reloads behind the dialog but the dialog stays in the foreground.
Fix this by closing dialogs when users click navigation links inside them.
Test Plan:
With Quicksand enabled and disabled, clicked a subtask type in the "Create Subtask" dialog.
- Before, Quicksand Disabled: Dialog stays on screen, then navigation occurs.
- After, Quicksand Disabled: Dialog vanishes, then navigation occurs.
- Before, Quicksand Enabled: Dialog stays on screen, navigation occurs behind it.
- After, Quicksand Enabled: Dialog vanishes, then navigation occurs.
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13302
Differential Revision: https://secure.phabricator.com/D20573
2019-06-06 03:42:42 +02:00
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
_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();
|
|
|
|
},
|
When a user clicks a navigation link in a dialog, close the dialog
Summary:
Ref T13302. Currently, if you enable Quicksand (by clicking "Persistent Chat"), open a dialog with links in it (like "Create Subtask" with multiple available subtypes), and then follow a navigation link, the page content reloads behind the dialog but the dialog stays in the foreground.
Fix this by closing dialogs when users click navigation links inside them.
Test Plan:
With Quicksand enabled and disabled, clicked a subtask type in the "Create Subtask" dialog.
- Before, Quicksand Disabled: Dialog stays on screen, then navigation occurs.
- After, Quicksand Disabled: Dialog vanishes, then navigation occurs.
- Before, Quicksand Enabled: Dialog stays on screen, navigation occurs behind it.
- After, Quicksand Enabled: Dialog vanishes, then navigation occurs.
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13302
Differential Revision: https://secure.phabricator.com/D20573
2019-06-06 03:42:42 +02:00
|
|
|
_onlink: function(event) {
|
|
|
|
// See T13302. When a user clicks a link in a dialog and that link
|
|
|
|
// triggers a navigation event, we want to close the dialog as though
|
|
|
|
// they had pressed a button.
|
|
|
|
|
|
|
|
// When Quicksand is enabled, this is particularly relevant because
|
|
|
|
// the dialog will stay in the foreground while the page content changes
|
|
|
|
// in the background if we do not dismiss the dialog.
|
|
|
|
|
|
|
|
// If this is a Command-Click, the link will open in a new window.
|
|
|
|
var is_command = !!event.getRawEvent().metaKey;
|
|
|
|
if (is_command) {
|
|
|
|
return;
|
|
|
|
}
|
2013-01-22 19:32:26 +01:00
|
|
|
|
When a user clicks a navigation link in a dialog, close the dialog
Summary:
Ref T13302. Currently, if you enable Quicksand (by clicking "Persistent Chat"), open a dialog with links in it (like "Create Subtask" with multiple available subtypes), and then follow a navigation link, the page content reloads behind the dialog but the dialog stays in the foreground.
Fix this by closing dialogs when users click navigation links inside them.
Test Plan:
With Quicksand enabled and disabled, clicked a subtask type in the "Create Subtask" dialog.
- Before, Quicksand Disabled: Dialog stays on screen, then navigation occurs.
- After, Quicksand Disabled: Dialog vanishes, then navigation occurs.
- Before, Quicksand Enabled: Dialog stays on screen, navigation occurs behind it.
- After, Quicksand Enabled: Dialog vanishes, then navigation occurs.
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13302
Differential Revision: https://secure.phabricator.com/D20573
2019-06-06 03:42:42 +02:00
|
|
|
var link = event.getNode('tag:a');
|
|
|
|
|
|
|
|
// If the link is an anchor, or does not go anywhere, ignore the event.
|
2019-06-28 17:40:10 +02:00
|
|
|
var href = link.getAttribute('href');
|
|
|
|
if (typeof href !== 'string') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
When a user clicks a navigation link in a dialog, close the dialog
Summary:
Ref T13302. Currently, if you enable Quicksand (by clicking "Persistent Chat"), open a dialog with links in it (like "Create Subtask" with multiple available subtypes), and then follow a navigation link, the page content reloads behind the dialog but the dialog stays in the foreground.
Fix this by closing dialogs when users click navigation links inside them.
Test Plan:
With Quicksand enabled and disabled, clicked a subtask type in the "Create Subtask" dialog.
- Before, Quicksand Disabled: Dialog stays on screen, then navigation occurs.
- After, Quicksand Disabled: Dialog vanishes, then navigation occurs.
- Before, Quicksand Enabled: Dialog stays on screen, navigation occurs behind it.
- After, Quicksand Enabled: Dialog vanishes, then navigation occurs.
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13302
Differential Revision: https://secure.phabricator.com/D20573
2019-06-06 03:42:42 +02:00
|
|
|
if (!href.length || href[0] === '#') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This link will open in a new window.
|
|
|
|
if (link.target === '_blank') {
|
2013-01-22 19:32:26 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-07-03 17:11:53 +02:00
|
|
|
// This link is really a dialog button which we'll handle elsewhere.
|
|
|
|
if (JX.Stratcom.hasSigil(link, 'jx-workflow-button')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
When a user clicks a navigation link in a dialog, close the dialog
Summary:
Ref T13302. Currently, if you enable Quicksand (by clicking "Persistent Chat"), open a dialog with links in it (like "Create Subtask" with multiple available subtypes), and then follow a navigation link, the page content reloads behind the dialog but the dialog stays in the foreground.
Fix this by closing dialogs when users click navigation links inside them.
Test Plan:
With Quicksand enabled and disabled, clicked a subtask type in the "Create Subtask" dialog.
- Before, Quicksand Disabled: Dialog stays on screen, then navigation occurs.
- After, Quicksand Disabled: Dialog vanishes, then navigation occurs.
- Before, Quicksand Enabled: Dialog stays on screen, navigation occurs behind it.
- After, Quicksand Enabled: Dialog vanishes, then navigation occurs.
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13302
Differential Revision: https://secure.phabricator.com/D20573
2019-06-06 03:42:42 +02:00
|
|
|
// Close the dialog.
|
|
|
|
JX.Workflow._pop();
|
|
|
|
},
|
|
|
|
_onbutton : function(event) {
|
|
|
|
|
|
|
|
if (JX.Stratcom.pass()) {
|
2013-01-22 19:32:26 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
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();
|
2016-05-20 15:20:35 +02:00
|
|
|
|
|
|
|
active._form = form;
|
|
|
|
|
2014-05-03 03:01:16 +02:00
|
|
|
var e = active.invoke('submit', {form: form, data: data});
|
|
|
|
if (!e.getStopped()) {
|
2016-05-20 15:20:35 +02:00
|
|
|
// NOTE: Don't remove the current dialog yet because additional
|
|
|
|
// handlers may still want to access the nodes.
|
|
|
|
|
2018-03-21 19:51:12 +01:00
|
|
|
// Disable whatever button the user clicked to prevent duplicate
|
2018-03-22 18:58:56 +01:00
|
|
|
// submission mistakes when you accidentally click a button multiple
|
|
|
|
// times. See T11145.
|
2018-03-21 19:51:12 +01:00
|
|
|
button.disabled = true;
|
|
|
|
|
2014-05-03 03:01:16 +02:00
|
|
|
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];
|
2016-06-21 02:29:56 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
_onresizestart: function(e) {
|
|
|
|
var self = JX.Workflow;
|
|
|
|
if (self._resizing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var workflow = self._getActiveWorkflow();
|
|
|
|
if (!workflow) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
e.kill();
|
|
|
|
|
|
|
|
var form = JX.DOM.find(workflow._root, 'div', 'jx-dialog');
|
|
|
|
var resize = e.getNodeData('jx-dialog-resize');
|
|
|
|
var node_y = JX.$(resize.resizeY);
|
|
|
|
|
|
|
|
var dim = JX.Vector.getDim(form);
|
|
|
|
dim.y = JX.Vector.getDim(node_y).y;
|
|
|
|
|
|
|
|
if (!form._minimumSize) {
|
|
|
|
form._minimumSize = dim;
|
|
|
|
}
|
|
|
|
|
|
|
|
self._resizing = {
|
|
|
|
min: form._minimumSize,
|
|
|
|
form: form,
|
|
|
|
startPos: JX.$V(e),
|
|
|
|
startDim: dim,
|
|
|
|
resizeY: node_y,
|
|
|
|
resizeX: resize.resizeX
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
_onmousemove: function(e) {
|
|
|
|
var self = JX.Workflow;
|
|
|
|
if (!self._resizing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var spec = self._resizing;
|
|
|
|
var form = spec.form;
|
|
|
|
var min = spec.min;
|
|
|
|
|
|
|
|
var delta = JX.$V(e).add(-spec.startPos.x, -spec.startPos.y);
|
|
|
|
var src_dim = spec.startDim;
|
|
|
|
var dst_dim = JX.$V(src_dim.x + delta.x, src_dim.y + delta.y);
|
|
|
|
|
|
|
|
if (dst_dim.x < min.x) {
|
|
|
|
dst_dim.x = min.x;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dst_dim.y < min.y) {
|
|
|
|
dst_dim.y = min.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (spec.resizeX) {
|
|
|
|
JX.$V(dst_dim.x, null).setDim(form);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (spec.resizeY) {
|
|
|
|
JX.$V(null, dst_dim.y).setDim(spec.resizeY);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_onmouseup: function() {
|
|
|
|
var self = JX.Workflow;
|
|
|
|
if (!self._resizing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self._resizing = false;
|
2013-01-22 19:32:26 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
members : {
|
|
|
|
_root : null,
|
|
|
|
_pushed : false,
|
|
|
|
_data : null,
|
2016-05-20 15:20:35 +02:00
|
|
|
|
|
|
|
_form: null,
|
|
|
|
_paused: 0,
|
|
|
|
_nextCallback: null,
|
2018-03-21 19:42:58 +01:00
|
|
|
_keepControlsDisabled: false,
|
2016-05-20 15:20:35 +02:00
|
|
|
|
|
|
|
getSourceForm: function() {
|
|
|
|
return this._form;
|
|
|
|
},
|
|
|
|
|
|
|
|
pause: function() {
|
|
|
|
this._paused++;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
resume: function() {
|
|
|
|
if (!this._paused) {
|
|
|
|
JX.$E('Resuming a workflow which is not paused!');
|
|
|
|
}
|
|
|
|
|
|
|
|
this._paused--;
|
|
|
|
|
|
|
|
if (!this._paused) {
|
|
|
|
var next = this._nextCallback;
|
|
|
|
this._nextCallback = null;
|
|
|
|
if (next) {
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
_onload : function(r) {
|
2016-05-20 15:20:35 +02:00
|
|
|
this._destroy();
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
// 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')) {
|
Never generate file download forms which point to the CDN domain, tighten "form-action" CSP
Summary:
Depends on D19155. Ref T13094. Ref T4340.
We can't currently implement a strict `form-action 'self'` content security policy because some file downloads rely on a `<form />` which sometimes POSTs to the CDN domain.
Broadly, stop generating these forms. We just redirect instead, and show an interstitial confirm dialog if no CDN domain is configured. This makes the UX for installs with no CDN domain a little worse and the UX for everyone else better.
Then, implement the stricter Content-Security-Policy.
This also removes extra confirm dialogs for downloading Harbormaster build logs and data exports.
Test Plan:
- Went through the plain data export, data export with bulk jobs, ssh key generation, calendar ICS download, Diffusion data, Paste data, Harbormaster log data, and normal file data download workflows with a CDN domain.
- Went through all those workflows again without a CDN domain.
- Grepped for affected symbols (`getCDNURI()`, `getDownloadURI()`).
- Added an evil form to a page, tried to submit it, was rejected.
- Went through the ReCaptcha and Stripe flows again to see if they're submitting any forms.
Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam
Maniphest Tasks: T13094, T4340
Differential Revision: https://secure.phabricator.com/D19156
2018-03-01 00:22:10 +01:00
|
|
|
// Before we redirect to file downloads, we close the dialog. These
|
|
|
|
// redirects aren't real navigation events so we end up stuck in the
|
|
|
|
// dialog otherwise.
|
|
|
|
if (r.close) {
|
|
|
|
this._pop();
|
|
|
|
}
|
|
|
|
|
2018-03-21 19:42:58 +01:00
|
|
|
// If we're redirecting, don't re-enable for controls.
|
|
|
|
this._keepControlsDisabled = true;
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
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
|
|
|
|
When a user clicks a navigation link in a dialog, close the dialog
Summary:
Ref T13302. Currently, if you enable Quicksand (by clicking "Persistent Chat"), open a dialog with links in it (like "Create Subtask" with multiple available subtypes), and then follow a navigation link, the page content reloads behind the dialog but the dialog stays in the foreground.
Fix this by closing dialogs when users click navigation links inside them.
Test Plan:
With Quicksand enabled and disabled, clicked a subtask type in the "Create Subtask" dialog.
- Before, Quicksand Disabled: Dialog stays on screen, then navigation occurs.
- After, Quicksand Disabled: Dialog vanishes, then navigation occurs.
- Before, Quicksand Enabled: Dialog stays on screen, navigation occurs behind it.
- After, Quicksand Enabled: Dialog vanishes, then navigation occurs.
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13302
Differential Revision: https://secure.phabricator.com/D20573
2019-06-06 03:42:42 +02:00
|
|
|
var onlink = JX.Workflow._onlink;
|
|
|
|
JX.DOM.listen(this._root, 'click', 'tag:a', onlink);
|
|
|
|
|
2016-06-21 02:29:56 +02:00
|
|
|
JX.DOM.listen(
|
|
|
|
this._root,
|
|
|
|
'mousedown',
|
|
|
|
'jx-dialog-resize',
|
|
|
|
JX.Workflow._onresizestart);
|
|
|
|
|
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);
|
2016-03-26 20:23:07 +01:00
|
|
|
JX.$V(0, s.y + offset).setPos(this._root);
|
2013-05-31 03:55:25 +02:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
},
|
2016-05-20 15:20:35 +02:00
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
start : function() {
|
2016-05-20 15:20:35 +02:00
|
|
|
var next = JX.bind(this, this._send);
|
|
|
|
|
|
|
|
this.pause();
|
|
|
|
this._nextCallback = next;
|
|
|
|
|
|
|
|
this.invoke('start', this);
|
|
|
|
|
|
|
|
this.resume();
|
|
|
|
},
|
|
|
|
|
|
|
|
_send: function() {
|
2013-01-22 19:32:26 +01:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2016-05-20 15:20:35 +02:00
|
|
|
addData: function(key, value) {
|
|
|
|
this._data.push([key, value]);
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2013-01-22 19:32:26 +01:00
|
|
|
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.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);
|
2016-06-21 02:29:56 +02:00
|
|
|
|
|
|
|
JX.Stratcom.listen('mousemove', null, JX.Workflow._onmousemove);
|
|
|
|
JX.Stratcom.listen('mouseup', null, JX.Workflow._onmouseup);
|
2013-01-22 19:32:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
});
|