mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-30 09:20:58 +01:00
e556d20577
Summary: A few minor fixes: - When we build a tag with `"meta" => null`, strip the attribute like we do for all other attributes. Previously, we would actually set the metadata to `null`. This happened with the Conpherence form. - Just respond to the draft request with an empty (but valid) response, instead of building a dialog. - `PhabricatorShapedRequest` is confusingly named and I should have caught this in review, but the basic shape of it is: - You make one object. - You call `trigger()` when stuff changes (e.g., a keystroke). - It manages making a small number of requests (e.g., one request after the user stops typing for a moment). - The way it was being used previously would incorrectly send a request for every keystroke. I think I'm going to simplify `ShapedRequest` and merge it into some larger queue for T430. Test Plan: Typed some text, no longer saw a flurry of requests. Reloaded page, still saw draft text. Reviewers: btrahan, chad Reviewed By: chad CC: aran, chad Differential Revision: https://secure.phabricator.com/D8380
555 lines
16 KiB
JavaScript
555 lines
16 KiB
JavaScript
/**
|
|
* @provides javelin-behavior-conpherence-menu
|
|
* @requires javelin-behavior
|
|
* javelin-dom
|
|
* javelin-util
|
|
* javelin-stratcom
|
|
* javelin-workflow
|
|
* javelin-behavior-device
|
|
* javelin-history
|
|
* javelin-vector
|
|
* phabricator-shaped-request
|
|
*/
|
|
|
|
JX.behavior('conpherence-menu', function(config) {
|
|
|
|
/**
|
|
* State for displayed thread.
|
|
*/
|
|
var _thread = {
|
|
selected: null,
|
|
visible: null,
|
|
node: null
|
|
};
|
|
|
|
/**
|
|
* Current role of this behavior. The two possible roles are to show a 'list'
|
|
* of threads or a specific 'thread'. On devices, this behavior stays in the
|
|
* 'list' role indefinitely, treating clicks normally and the next page
|
|
* loads the behavior with role = 'thread'. On desktop, this behavior
|
|
* auto-loads a thread as part of the 'list' role. As the thread loads the
|
|
* role is changed to 'thread'.
|
|
*/
|
|
var _currentRole = null;
|
|
|
|
/**
|
|
* When _oldDevice is null the code is executing for the first time.
|
|
*/
|
|
var _oldDevice = null;
|
|
|
|
/**
|
|
* Initializes this behavior based on all the configuraton jonx and the
|
|
* result of JX.Device.getDevice();
|
|
*/
|
|
function init() {
|
|
_currentRole = config.role;
|
|
|
|
if (_currentRole == 'thread') {
|
|
markThreadsLoading(true);
|
|
} else {
|
|
markThreadLoading(true);
|
|
}
|
|
markWidgetLoading(true);
|
|
onDeviceChange();
|
|
}
|
|
init();
|
|
|
|
/**
|
|
* Selecting threads
|
|
*/
|
|
JX.Stratcom.listen(
|
|
'conpherence-selectthread',
|
|
null,
|
|
function (e) {
|
|
selectThreadByID(e.getData().id);
|
|
}
|
|
);
|
|
|
|
function selectThreadByID(id, update_page_data) {
|
|
var thread = JX.$(id);
|
|
selectThread(thread, update_page_data);
|
|
}
|
|
|
|
function selectThread(node, update_page_data) {
|
|
if (_thread.node) {
|
|
JX.DOM.alterClass(_thread.node, 'conpherence-selected', false);
|
|
// keep the unread-count hidden still. big TODO once we ajax in updates
|
|
// to threads to make this work right and move threads between read /
|
|
// unread
|
|
}
|
|
|
|
JX.DOM.alterClass(node, 'conpherence-selected', true);
|
|
JX.DOM.alterClass(node, 'hide-unread-count', true);
|
|
|
|
_thread.node = node;
|
|
|
|
var data = JX.Stratcom.getData(node);
|
|
_thread.selected = data.threadID;
|
|
|
|
if (update_page_data) {
|
|
updatePageData(data);
|
|
}
|
|
|
|
redrawThread();
|
|
}
|
|
|
|
function updatePageData(data) {
|
|
var uri_suffix = _thread.selected + '/';
|
|
if (data.use_base_uri) {
|
|
uri_suffix = '';
|
|
}
|
|
JX.History.replace(config.baseURI + uri_suffix);
|
|
if (data.title) {
|
|
document.title = data.title;
|
|
} else if (_thread.node) {
|
|
var threadData = JX.Stratcom.getData(_thread.node);
|
|
document.title = threadData.title;
|
|
}
|
|
}
|
|
|
|
JX.Stratcom.listen(
|
|
'conpherence-update-page-data',
|
|
null,
|
|
function (e) {
|
|
updatePageData(e.getData());
|
|
}
|
|
);
|
|
|
|
function redrawThread() {
|
|
if (!_thread.node) {
|
|
return;
|
|
}
|
|
|
|
if (_thread.visible == _thread.selected) {
|
|
return;
|
|
}
|
|
|
|
var data = JX.Stratcom.getData(_thread.node);
|
|
|
|
if (_thread.visible !== null || !config.hasThread) {
|
|
markThreadLoading(true);
|
|
var uri = config.baseURI + data.threadID + '/';
|
|
new JX.Workflow(uri, {})
|
|
.setHandler(JX.bind(null, onLoadThreadResponse, data.threadID))
|
|
.start();
|
|
} else if (config.hasThread) {
|
|
_scrollMessageWindow();
|
|
} else {
|
|
didRedrawThread();
|
|
}
|
|
|
|
if (_thread.visible !== null || !config.hasWidgets) {
|
|
reloadWidget(data);
|
|
} else {
|
|
JX.Stratcom.invoke(
|
|
'conpherence-update-widgets',
|
|
null,
|
|
{
|
|
widget : getDefaultWidget(),
|
|
buildSelectors : false,
|
|
toggleWidget : true,
|
|
threadID : _thread.selected
|
|
});
|
|
}
|
|
|
|
_thread.visible = _thread.selected;
|
|
}
|
|
|
|
function markThreadsLoading(loading) {
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var menu = JX.DOM.find(root, 'div', 'conpherence-menu-pane');
|
|
JX.DOM.alterClass(menu, 'loading', loading);
|
|
}
|
|
|
|
function markThreadLoading(loading) {
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane');
|
|
var messages_root = JX.DOM.find(root, 'div', 'conpherence-message-pane');
|
|
var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
|
|
|
|
JX.DOM.alterClass(header_root, 'loading', loading);
|
|
JX.DOM.alterClass(messages_root, 'loading', loading);
|
|
JX.DOM.alterClass(form_root, 'loading', loading);
|
|
|
|
try {
|
|
var textarea = JX.DOM.find(form, 'textarea');
|
|
textarea.disabled = loading;
|
|
var button = JX.DOM.find(form, 'button');
|
|
button.disabled = loading;
|
|
} catch (ex) {
|
|
// haven't loaded it yet!
|
|
}
|
|
}
|
|
|
|
function markWidgetLoading(loading) {
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widget-pane');
|
|
|
|
JX.DOM.alterClass(widgets_root, 'loading', loading);
|
|
}
|
|
|
|
function reloadWidget(data) {
|
|
markWidgetLoading(true);
|
|
if (!data.widget) {
|
|
data.widget = getDefaultWidget();
|
|
}
|
|
var widget_uri = config.baseURI + 'widget/' + data.threadID + '/';
|
|
new JX.Workflow(widget_uri, {})
|
|
.setHandler(JX.bind(null, onWidgetResponse, data.threadID, data.widget))
|
|
.start();
|
|
}
|
|
JX.Stratcom.listen(
|
|
'conpherence-reload-widget',
|
|
null,
|
|
function (e) {
|
|
var data = e.getData();
|
|
if (data.threadID != _thread.selected) {
|
|
return;
|
|
}
|
|
reloadWidget(data);
|
|
}
|
|
);
|
|
|
|
function onWidgetResponse(thread_id, widget, response) {
|
|
// we got impatient and this is no longer the right answer :/
|
|
if (_thread.selected != thread_id) {
|
|
return;
|
|
}
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widgets-holder');
|
|
JX.DOM.setContent(widgets_root, JX.$H(response.widgets));
|
|
|
|
JX.Stratcom.invoke(
|
|
'conpherence-update-widgets',
|
|
null,
|
|
{
|
|
widget : widget,
|
|
buildSelectors : true,
|
|
toggleWidget : true,
|
|
threadID : _thread.selected
|
|
});
|
|
|
|
markWidgetLoading(false);
|
|
}
|
|
|
|
function getDefaultWidget() {
|
|
var device = JX.Device.getDevice();
|
|
var widget = 'conpherence-message-pane';
|
|
if (device == 'desktop') {
|
|
widget = 'widgets-people';
|
|
}
|
|
return widget;
|
|
}
|
|
|
|
function onLoadThreadResponse(thread_id, response) {
|
|
// we got impatient and this is no longer the right answer :/
|
|
if (_thread.selected != thread_id) {
|
|
return;
|
|
}
|
|
var header = JX.$H(response.header);
|
|
var messages = JX.$H(response.messages);
|
|
var form = JX.$H(response.form);
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane');
|
|
var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages');
|
|
var form_root = JX.DOM.find(root, 'div', 'conpherence-form');
|
|
JX.DOM.setContent(header_root, header);
|
|
JX.DOM.setContent(messages_root, messages);
|
|
JX.DOM.setContent(form_root, form);
|
|
|
|
markThreadLoading(false);
|
|
|
|
didRedrawThread(true);
|
|
}
|
|
|
|
/**
|
|
* This function is a wee bit tricky. Internally, we want to scroll the
|
|
* message window and let other stuff - notably widgets - redraw / build if
|
|
* necessary. Externally, we want a hook to scroll the message window
|
|
* - notably when the widget selector is used to invoke the message pane.
|
|
* The following three functions get 'er done.
|
|
*/
|
|
function didRedrawThread(build_device_widget_selector) {
|
|
_scrollMessageWindow();
|
|
JX.Stratcom.invoke(
|
|
'conpherence-did-redraw-thread',
|
|
null,
|
|
{
|
|
widget : getDefaultWidget(),
|
|
threadID : _thread.selected,
|
|
buildDeviceWidgetSelector : build_device_widget_selector
|
|
});
|
|
}
|
|
function _scrollMessageWindow() {
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages');
|
|
messages_root.scrollTop = messages_root.scrollHeight;
|
|
}
|
|
JX.Stratcom.listen(
|
|
'conpherence-redraw-thread',
|
|
null,
|
|
function (e) {
|
|
_scrollMessageWindow();
|
|
}
|
|
);
|
|
|
|
JX.Stratcom.listen(
|
|
'click',
|
|
'conpherence-menu-click',
|
|
function(e) {
|
|
if (!e.isNormalClick()) {
|
|
return;
|
|
}
|
|
|
|
// On devices, just follow the link normally.
|
|
if (JX.Device.getDevice() != 'desktop') {
|
|
return;
|
|
}
|
|
|
|
e.kill();
|
|
selectThread(e.getNode('conpherence-menu-click'), true);
|
|
});
|
|
|
|
JX.Stratcom.listen('click', 'conpherence-edit-metadata', function (e) {
|
|
e.kill();
|
|
var root = e.getNode('conpherence-layout');
|
|
var form = JX.DOM.find(root, 'form', 'conpherence-pontificate');
|
|
var data = e.getNodeData('conpherence-edit-metadata');
|
|
var header = JX.DOM.find(root, 'div', 'conpherence-header-pane');
|
|
var messages = JX.DOM.find(root, 'div', 'conpherence-messages');
|
|
|
|
new JX.Workflow.newFromForm(form, data)
|
|
.setHandler(JX.bind(this, function(r) {
|
|
JX.DOM.appendContent(messages, JX.$H(r.transactions));
|
|
messages.scrollTop = messages.scrollHeight;
|
|
|
|
JX.DOM.setContent(
|
|
header,
|
|
JX.$H(r.header)
|
|
);
|
|
|
|
try {
|
|
// update the menu entry
|
|
JX.DOM.replace(
|
|
JX.$(r.conpherence_phid + '-nav-item'),
|
|
JX.$H(r.nav_item)
|
|
);
|
|
JX.Stratcom.invoke(
|
|
'conpherence-selectthread',
|
|
null,
|
|
{ id : r.conpherence_phid + '-nav-item' }
|
|
);
|
|
} catch (ex) {
|
|
// Ignore; this view may not have a menu.
|
|
}
|
|
}))
|
|
.start();
|
|
});
|
|
|
|
var _loadingTransactionID = null;
|
|
JX.Stratcom.listen('click', 'show-older-messages', function(e) {
|
|
e.kill();
|
|
var data = e.getNodeData('show-older-messages');
|
|
if (data.oldest_transaction_id == _loadingTransactionID) {
|
|
return;
|
|
}
|
|
_loadingTransactionID = data.oldest_transaction_id;
|
|
var node = e.getNode('show-older-messages');
|
|
JX.DOM.setContent(node, 'Loading...');
|
|
JX.DOM.alterClass(node, 'conpherence-show-older-messages-loading', true);
|
|
|
|
var conf_id = _thread.selected;
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages');
|
|
new JX.Workflow(config.baseURI + conf_id + '/', data)
|
|
.setHandler(function(r) {
|
|
JX.DOM.remove(node);
|
|
var messages = JX.$H(r.messages);
|
|
JX.DOM.prependContent(
|
|
messages_root,
|
|
JX.$H(messages));
|
|
}).start();
|
|
});
|
|
|
|
/**
|
|
* On devices, we just show a thread list, so we don't want to automatically
|
|
* select or load any threads. On desktop, we automatically select the first
|
|
* thread, changing the _currentRole from list to thread.
|
|
*/
|
|
function onDeviceChange() {
|
|
var new_device = JX.Device.getDevice();
|
|
if (new_device === _oldDevice) {
|
|
return;
|
|
}
|
|
|
|
if (_oldDevice === null) {
|
|
_oldDevice = new_device;
|
|
if (_currentRole == 'list') {
|
|
if (new_device != 'desktop') {
|
|
return;
|
|
}
|
|
} else {
|
|
loadThreads();
|
|
return;
|
|
}
|
|
}
|
|
var update_toggled_widget =
|
|
new_device == 'desktop' || _oldDevice == 'desktop';
|
|
_oldDevice = new_device;
|
|
|
|
if (_thread.visible !== null && update_toggled_widget) {
|
|
JX.Stratcom.invoke(
|
|
'conpherence-did-redraw-thread',
|
|
null,
|
|
{
|
|
widget : getDefaultWidget(),
|
|
threadID : _thread.selected
|
|
});
|
|
}
|
|
|
|
if (_currentRole == 'list' && new_device == 'desktop') {
|
|
// this selects a thread and loads it
|
|
didLoadThreads();
|
|
_currentRole = 'thread';
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
JX.DOM.alterClass(root, 'conpherence-role-list', false);
|
|
JX.DOM.alterClass(root, 'conpherence-role-thread', true);
|
|
}
|
|
}
|
|
JX.Stratcom.listen('phabricator-device-change', null, onDeviceChange);
|
|
|
|
function loadThreads() {
|
|
markThreadsLoading(true);
|
|
var uri = config.baseURI + 'thread/' + config.selectedThreadID + '/';
|
|
new JX.Workflow(uri)
|
|
.setHandler(onLoadThreadsResponse)
|
|
.start();
|
|
}
|
|
|
|
function onLoadThreadsResponse(r) {
|
|
var layout = JX.$(config.layoutID);
|
|
var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane');
|
|
JX.DOM.setContent(menu, JX.$H(r));
|
|
|
|
config.selectedID && selectThreadByID(config.selectedID);
|
|
|
|
_thread.node.scrollIntoView();
|
|
|
|
markThreadsLoading(false);
|
|
}
|
|
|
|
function didLoadThreads() {
|
|
// If there's no thread selected yet, select the current thread or the
|
|
// first thread.
|
|
if (!_thread.selected) {
|
|
if (config.selectedID) {
|
|
selectThreadByID(config.selectedID, true);
|
|
} else {
|
|
var layout = JX.$(config.layoutID);
|
|
var threads = JX.DOM.scry(layout, 'a', 'conpherence-menu-click');
|
|
if (threads.length) {
|
|
selectThread(threads[0]);
|
|
} else {
|
|
var nothreads = JX.DOM.find(layout, 'div', 'conpherence-no-threads');
|
|
nothreads.style.display = 'block';
|
|
markThreadLoading(false);
|
|
markWidgetLoading(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var handleThreadScrollers = function (e) {
|
|
e.kill();
|
|
|
|
var data = e.getNodeData('conpherence-menu-scroller');
|
|
var scroller = e.getNode('conpherence-menu-scroller');
|
|
JX.DOM.alterClass(scroller, 'loading', true);
|
|
JX.DOM.setContent(scroller.firstChild, 'Loading...');
|
|
new JX.Workflow(scroller.href, data)
|
|
.setHandler(
|
|
JX.bind(null, threadScrollerResponse, scroller, data.direction))
|
|
.start();
|
|
};
|
|
|
|
var threadScrollerResponse = function (scroller, direction, r) {
|
|
var html = JX.$H(r.html);
|
|
|
|
var thread_phids = r.phids;
|
|
var reselect_id = null;
|
|
// remove any threads that are in the list that we just got back
|
|
// in the result set; things have changed and they'll be in the
|
|
// right place soon
|
|
for (var ii = 0; ii < thread_phids.length; ii++) {
|
|
try {
|
|
var node_id = thread_phids[ii] + '-nav-item';
|
|
var node = JX.$(node_id);
|
|
var node_data = JX.Stratcom.getData(node);
|
|
if (node_data.id == _thread.selected) {
|
|
reselect_id = node_id;
|
|
}
|
|
JX.DOM.remove(node);
|
|
} catch (ex) {
|
|
// ignore , just haven't seen this thread yet
|
|
}
|
|
}
|
|
|
|
var root = JX.DOM.find(document, 'div', 'conpherence-layout');
|
|
var menu_root = JX.DOM.find(root, 'div', 'conpherence-menu-pane');
|
|
var scroll_y = 0;
|
|
// we have to do some hyjinx in the up case to make the menu scroll to
|
|
// where it should
|
|
if (direction == 'up') {
|
|
var style = {
|
|
position: 'absolute',
|
|
left: '-10000px'
|
|
};
|
|
var test_size = JX.$N('div', {style: style}, html);
|
|
document.body.appendChild(test_size);
|
|
var html_size = JX.Vector.getDim(test_size);
|
|
JX.DOM.remove(test_size);
|
|
scroll_y = html_size.y;
|
|
}
|
|
JX.DOM.replace(scroller, html);
|
|
menu_root.scrollTop += scroll_y;
|
|
|
|
if (reselect_id) {
|
|
JX.Stratcom.invoke(
|
|
'conpherence-selectthread',
|
|
null,
|
|
{ id : reselect_id }
|
|
);
|
|
}
|
|
};
|
|
|
|
JX.Stratcom.listen(
|
|
['click'],
|
|
'conpherence-menu-scroller',
|
|
handleThreadScrollers
|
|
);
|
|
|
|
var onkeydownDraft = function (e) {
|
|
var form = e.getNode('tag:form');
|
|
var data = e.getNodeData('tag:form');
|
|
|
|
if (!data.preview) {
|
|
var uri = config.baseURI + 'update/' + _thread.selected + '/';
|
|
data.preview = new JX.PhabricatorShapedRequest(
|
|
uri,
|
|
JX.bag,
|
|
function () {
|
|
var data = JX.DOM.convertFormToDictionary(form);
|
|
data.action = 'draft';
|
|
return data;
|
|
});
|
|
}
|
|
|
|
data.preview.trigger();
|
|
};
|
|
|
|
JX.Stratcom.listen(
|
|
['keydown'],
|
|
'conpherence-pontificate',
|
|
onkeydownDraft);
|
|
|
|
});
|