mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 08:52:39 +01:00
acc1fa1655
Summary: Ref T13513. Currently, "View as Document Type..." lists every available engine. This is hard to get completely right because we can't always rebuild the document ref accurately in the endpoint, but try harder to fake something reasonable. Test Plan: Used "View as Document Type..." on Jupyter notebooks, was given "Jupyter" and "Source" as options. Maniphest Tasks: T13513 Differential Revision: https://secure.phabricator.com/D21241
1052 lines
28 KiB
JavaScript
1052 lines
28 KiB
JavaScript
/**
|
|
* @provides phabricator-diff-changeset
|
|
* @requires javelin-dom
|
|
* javelin-util
|
|
* javelin-stratcom
|
|
* javelin-install
|
|
* javelin-workflow
|
|
* javelin-router
|
|
* javelin-behavior-device
|
|
* javelin-vector
|
|
* phabricator-diff-inline
|
|
* phabricator-diff-path-view
|
|
* phuix-button-view
|
|
* @javelin
|
|
*/
|
|
|
|
JX.install('DiffChangeset', {
|
|
|
|
construct : function(node) {
|
|
this._node = node;
|
|
|
|
var data = this._getNodeData();
|
|
|
|
this._renderURI = data.renderURI;
|
|
this._ref = data.ref;
|
|
this._loaded = data.loaded;
|
|
this._treeNodeID = data.treeNodeID;
|
|
|
|
this._leftID = data.left;
|
|
this._rightID = data.right;
|
|
|
|
this._displayPath = JX.$H(data.displayPath);
|
|
this._pathParts = data.pathParts;
|
|
this._icon = data.icon;
|
|
|
|
this._editorURI = data.editorURI;
|
|
this._editorConfigureURI = data.editorConfigureURI;
|
|
this._showPathURI = data.showPathURI;
|
|
this._showDirectoryURI = data.showDirectoryURI;
|
|
|
|
this._pathIconIcon = data.pathIconIcon;
|
|
this._pathIconColor = data.pathIconColor;
|
|
this._isLowImportance = data.isLowImportance;
|
|
this._isOwned = data.isOwned;
|
|
this._isLoading = true;
|
|
|
|
this._inlines = [];
|
|
|
|
if (data.changesetState) {
|
|
this._loadChangesetState(data.changesetState);
|
|
}
|
|
|
|
var onselect = JX.bind(this, this._onClickHeader);
|
|
JX.DOM.listen(this._node, 'mousedown', 'changeset-header', onselect);
|
|
},
|
|
|
|
members: {
|
|
_node: null,
|
|
_loaded: false,
|
|
_sequence: 0,
|
|
_stabilize: false,
|
|
|
|
_renderURI: null,
|
|
_ref: null,
|
|
_rendererKey: null,
|
|
_highlight: null,
|
|
_requestDocumentEngineKey: null,
|
|
_responseDocumentEngineKey: null,
|
|
_availableDocumentEngineKeys: null,
|
|
_characterEncoding: null,
|
|
_undoTemplates: null,
|
|
|
|
_leftID: null,
|
|
_rightID: null,
|
|
|
|
_inlines: null,
|
|
_visible: true,
|
|
|
|
_displayPath: null,
|
|
|
|
_changesetList: null,
|
|
_icon: null,
|
|
|
|
_editorURI: null,
|
|
_editorConfigureURI: null,
|
|
_showPathURI: null,
|
|
_showDirectoryURI: null,
|
|
|
|
_pathView: null,
|
|
|
|
_pathIconIcon: null,
|
|
_pathIconColor: null,
|
|
_isLowImportance: null,
|
|
_isOwned: null,
|
|
_isHidden: null,
|
|
_isSelected: false,
|
|
_viewMenu: null,
|
|
|
|
getEditorURI: function() {
|
|
return this._editorURI;
|
|
},
|
|
|
|
getEditorConfigureURI: function() {
|
|
return this._editorConfigureURI;
|
|
},
|
|
|
|
getShowPathURI: function() {
|
|
return this._showPathURI;
|
|
},
|
|
|
|
getShowDirectoryURI: function() {
|
|
return this._showDirectoryURI;
|
|
},
|
|
|
|
getLeftChangesetID: function() {
|
|
return this._leftID;
|
|
},
|
|
|
|
getRightChangesetID: function() {
|
|
return this._rightID;
|
|
},
|
|
|
|
setChangesetList: function(list) {
|
|
this._changesetList = list;
|
|
return this;
|
|
},
|
|
|
|
setViewMenu: function(menu) {
|
|
this._viewMenu = menu;
|
|
return this;
|
|
},
|
|
|
|
getIcon: function() {
|
|
if (!this._visible) {
|
|
return 'fa-file-o';
|
|
}
|
|
|
|
return this._icon;
|
|
},
|
|
|
|
getColor: function() {
|
|
if (!this._visible) {
|
|
return 'grey';
|
|
}
|
|
|
|
return 'blue';
|
|
},
|
|
|
|
getChangesetList: function() {
|
|
return this._changesetList;
|
|
},
|
|
|
|
/**
|
|
* Has the content of this changeset been loaded?
|
|
*
|
|
* This method returns `true` if a request has been fired, even if the
|
|
* response has not returned yet.
|
|
*
|
|
* @return bool True if the content has been loaded.
|
|
*/
|
|
isLoaded: function() {
|
|
return this._loaded;
|
|
},
|
|
|
|
|
|
/**
|
|
* Configure stabilization of the document position on content load.
|
|
*
|
|
* When we dump the changeset into the document, we can try to stabilize
|
|
* the document scroll position so that the user doesn't feel like they
|
|
* are jumping around as things load in. This is generally useful when
|
|
* populating initial changes.
|
|
*
|
|
* However, if a user explicitly requests a content load by clicking a
|
|
* "Load" link or using the dropdown menu, this stabilization generally
|
|
* feels unnatural, so we don't use it in response to explicit user action.
|
|
*
|
|
* @param bool True to stabilize the next content fill.
|
|
* @return this
|
|
*/
|
|
setStabilize: function(stabilize) {
|
|
this._stabilize = stabilize;
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* Should this changeset load immediately when the page loads?
|
|
*
|
|
* Normally, changes load immediately, but if a diff or commit is very
|
|
* large we stop doing this and have the user load files explicitly, or
|
|
* choose to load everything.
|
|
*
|
|
* @return bool True if the changeset should load automatically when the
|
|
* page loads.
|
|
*/
|
|
shouldAutoload: function() {
|
|
return this._getNodeData().autoload;
|
|
},
|
|
|
|
|
|
/**
|
|
* Load this changeset, if it isn't already loading.
|
|
*
|
|
* This fires a request to fill the content of this changeset, provided
|
|
* there isn't already a request in flight. To force a reload, use
|
|
* @{method:reload}.
|
|
*
|
|
* @return this
|
|
*/
|
|
load: function() {
|
|
if (this._loaded) {
|
|
return this;
|
|
}
|
|
|
|
return this.reload();
|
|
},
|
|
|
|
|
|
/**
|
|
* Reload the changeset content.
|
|
*
|
|
* This method always issues a request, even if the content is already
|
|
* loading. To load conditionally, use @{method:load}.
|
|
*
|
|
* @return this
|
|
*/
|
|
reload: function(state) {
|
|
this._loaded = true;
|
|
this._sequence++;
|
|
|
|
var workflow = this._newReloadWorkflow(state)
|
|
.setHandler(JX.bind(this, this._onresponse, this._sequence));
|
|
|
|
this._startContentWorkflow(workflow);
|
|
|
|
var pht = this.getChangesetList().getTranslations();
|
|
|
|
JX.DOM.setContent(
|
|
this._getContentFrame(),
|
|
JX.$N(
|
|
'div',
|
|
{className: 'differential-loading'},
|
|
pht('Loading...')));
|
|
|
|
return this;
|
|
},
|
|
|
|
_newReloadWorkflow: function(state) {
|
|
var params = this._getViewParameters(state);
|
|
return new JX.Workflow(this._renderURI, params);
|
|
},
|
|
|
|
/**
|
|
* Load missing context in a changeset.
|
|
*
|
|
* We do this when the user clicks "Show X Lines". We also expand all of
|
|
* the missing context when they "Show All Context".
|
|
*
|
|
* @param string Line range specification, like "0-40/0-20".
|
|
* @param node Row where the context should be rendered after loading.
|
|
* @param bool True if this is a bulk load of multiple context blocks.
|
|
* @return this
|
|
*/
|
|
loadContext: function(range, target, bulk) {
|
|
var params = this._getViewParameters();
|
|
params.range = range;
|
|
|
|
var pht = this.getChangesetList().getTranslations();
|
|
|
|
var container = JX.DOM.scry(target, 'td')[0];
|
|
JX.DOM.setContent(container, pht('Loading...'));
|
|
JX.DOM.alterClass(target, 'differential-show-more-loading', true);
|
|
|
|
var workflow = new JX.Workflow(this._renderURI, params)
|
|
.setHandler(JX.bind(this, this._oncontext, target));
|
|
|
|
if (bulk) {
|
|
// If we're loading a bunch of these because the viewer clicked
|
|
// "Show All Context" or similar, use lower-priority requests
|
|
// and draw a progress bar.
|
|
this._startContentWorkflow(workflow);
|
|
} else {
|
|
// If this is a single click on a context link, use a higher priority
|
|
// load without a chrome change.
|
|
workflow.start();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
loadAllContext: function() {
|
|
var nodes = JX.DOM.scry(this._node, 'tr', 'context-target');
|
|
for (var ii = 0; ii < nodes.length; ii++) {
|
|
var show = JX.DOM.scry(nodes[ii], 'a', 'show-more');
|
|
for (var jj = 0; jj < show.length; jj++) {
|
|
var data = JX.Stratcom.getData(show[jj]);
|
|
if (data.type != 'all') {
|
|
continue;
|
|
}
|
|
this.loadContext(data.range, nodes[ii], true);
|
|
}
|
|
}
|
|
},
|
|
|
|
_startContentWorkflow: function(workflow) {
|
|
var routable = workflow.getRoutable();
|
|
|
|
routable
|
|
.setPriority(500)
|
|
.setType('content')
|
|
.setKey(this._getRoutableKey());
|
|
|
|
JX.Router.getInstance().queue(routable);
|
|
},
|
|
|
|
getDisplayPath: function() {
|
|
return this._displayPath;
|
|
},
|
|
|
|
/**
|
|
* Receive a response to a context request.
|
|
*/
|
|
_oncontext: function(target, response) {
|
|
// TODO: This should be better structured.
|
|
// If the response comes back with several top-level nodes, the last one
|
|
// is the actual context; the others are headers. Add any headers first,
|
|
// then copy the new rows into the document.
|
|
var markup = JX.$H(response.changeset).getFragment();
|
|
var len = markup.childNodes.length;
|
|
var diff = JX.DOM.findAbove(target, 'table', 'differential-diff');
|
|
|
|
for (var ii = 0; ii < len - 1; ii++) {
|
|
diff.parentNode.insertBefore(markup.firstChild, diff);
|
|
}
|
|
|
|
var table = markup.firstChild;
|
|
var root = target.parentNode;
|
|
this._moveRows(table, root, target);
|
|
root.removeChild(target);
|
|
|
|
this._onchangesetresponse(response);
|
|
},
|
|
|
|
_moveRows: function(src, dst, before) {
|
|
var rows = JX.DOM.scry(src, 'tr');
|
|
for (var ii = 0; ii < rows.length; ii++) {
|
|
|
|
// Find the table this <tr /> belongs to. If it's a sub-table, like a
|
|
// table in an inline comment, don't copy it.
|
|
if (JX.DOM.findAbove(rows[ii], 'table') !== src) {
|
|
continue;
|
|
}
|
|
|
|
if (before) {
|
|
dst.insertBefore(rows[ii], before);
|
|
} else {
|
|
dst.appendChild(rows[ii]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get parameters which define the current rendering options.
|
|
*/
|
|
_getViewParameters: function(state) {
|
|
var parameters = {
|
|
ref: this._ref,
|
|
device: this._getDefaultDeviceRenderer()
|
|
};
|
|
|
|
if (state) {
|
|
JX.copy(parameters, state);
|
|
}
|
|
|
|
return parameters;
|
|
},
|
|
|
|
/**
|
|
* Get the active @{class:JX.Routable} for this changeset.
|
|
*
|
|
* After issuing a request with @{method:load} or @{method:reload}, you
|
|
* can adjust routable settings (like priority) by querying the routable
|
|
* with this method. Note that there may not be a current routable.
|
|
*
|
|
* @return JX.Routable|null Active routable, if one exists.
|
|
*/
|
|
getRoutable: function() {
|
|
return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey());
|
|
},
|
|
|
|
getRendererKey: function() {
|
|
return this._rendererKey;
|
|
},
|
|
|
|
_getDefaultDeviceRenderer: function() {
|
|
// NOTE: If you load the page at one device resolution and then resize to
|
|
// a different one we don't re-render the diffs, because it's a
|
|
// complicated mess and you could lose inline comments, cursor positions,
|
|
// etc.
|
|
return (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
|
|
},
|
|
|
|
getUndoTemplates: function() {
|
|
return this._undoTemplates;
|
|
},
|
|
|
|
getCharacterEncoding: function() {
|
|
return this._characterEncoding;
|
|
},
|
|
|
|
getHighlight: function() {
|
|
return this._highlight;
|
|
},
|
|
|
|
getRequestDocumentEngineKey: function() {
|
|
return this._requestDocumentEngineKey;
|
|
},
|
|
|
|
getResponseDocumentEngineKey: function() {
|
|
return this._responseDocumentEngineKey;
|
|
},
|
|
|
|
getAvailableDocumentEngineKeys: function() {
|
|
return this._availableDocumentEngineKeys;
|
|
},
|
|
|
|
getSelectableItems: function() {
|
|
var items = [];
|
|
|
|
items.push({
|
|
type: 'file',
|
|
changeset: this,
|
|
target: this,
|
|
nodes: {
|
|
begin: this._node,
|
|
end: null
|
|
}
|
|
});
|
|
|
|
if (!this._visible) {
|
|
return items;
|
|
}
|
|
|
|
var rows = JX.DOM.scry(this._node, 'tr');
|
|
|
|
var blocks = [];
|
|
var block;
|
|
var ii;
|
|
for (ii = 0; ii < rows.length; ii++) {
|
|
var type = this._getRowType(rows[ii]);
|
|
|
|
if (!block || (block.type !== type)) {
|
|
block = {
|
|
type: type,
|
|
items: []
|
|
};
|
|
blocks.push(block);
|
|
}
|
|
|
|
block.items.push(rows[ii]);
|
|
}
|
|
|
|
var last_inline = null;
|
|
var last_inline_item = null;
|
|
for (ii = 0; ii < blocks.length; ii++) {
|
|
block = blocks[ii];
|
|
|
|
if (block.type == 'change') {
|
|
items.push({
|
|
type: block.type,
|
|
changeset: this,
|
|
target: block.items[0],
|
|
nodes: {
|
|
begin: block.items[0],
|
|
end: block.items[block.items.length - 1]
|
|
}
|
|
});
|
|
}
|
|
|
|
if (block.type == 'comment') {
|
|
for (var jj = 0; jj < block.items.length; jj++) {
|
|
var inline = this.getInlineForRow(block.items[jj]);
|
|
|
|
// When comments are being edited, they have a hidden row with
|
|
// the actual comment and then a visible row with the editor.
|
|
|
|
// In this case, we only want to generate one item, but it should
|
|
// use the editor as a scroll target. To accomplish this, check if
|
|
// this row has the same inline as the previous row. If so, update
|
|
// the last item to use this row's nodes.
|
|
|
|
if (inline === last_inline) {
|
|
last_inline_item.nodes.begin = block.items[jj];
|
|
last_inline_item.nodes.end = block.items[jj];
|
|
continue;
|
|
} else {
|
|
last_inline = inline;
|
|
}
|
|
|
|
var is_saved = (!inline.isDraft() && !inline.isEditing());
|
|
|
|
last_inline_item = {
|
|
type: block.type,
|
|
changeset: this,
|
|
target: inline,
|
|
hidden: inline.isHidden(),
|
|
collapsed: inline.isCollapsed(),
|
|
deleted: !inline.getID() && !inline.isEditing(),
|
|
nodes: {
|
|
begin: block.items[jj],
|
|
end: block.items[jj]
|
|
},
|
|
attributes: {
|
|
unsaved: inline.isEditing(),
|
|
anyDraft: inline.isDraft() || inline.isDraftDone(),
|
|
undone: (is_saved && !inline.isDone()),
|
|
done: (is_saved && inline.isDone())
|
|
}
|
|
};
|
|
|
|
items.push(last_inline_item);
|
|
}
|
|
}
|
|
}
|
|
|
|
return items;
|
|
},
|
|
|
|
_getRowType: function(row) {
|
|
// NOTE: Don't do "className.indexOf()" elsewhere. This is evil legacy
|
|
// magic.
|
|
|
|
if (row.className.indexOf('inline') !== -1) {
|
|
return 'comment';
|
|
}
|
|
|
|
var cells = JX.DOM.scry(row, 'td');
|
|
for (var ii = 0; ii < cells.length; ii++) {
|
|
if (cells[ii].className.indexOf('old') !== -1 ||
|
|
cells[ii].className.indexOf('new') !== -1) {
|
|
return 'change';
|
|
}
|
|
}
|
|
},
|
|
|
|
_getNodeData: function() {
|
|
return JX.Stratcom.getData(this._node);
|
|
},
|
|
|
|
getVectors: function() {
|
|
return {
|
|
pos: JX.$V(this._node),
|
|
dim: JX.Vector.getDim(this._node)
|
|
};
|
|
},
|
|
|
|
_onresponse: function(sequence, response) {
|
|
if (sequence != this._sequence) {
|
|
// If this isn't the most recent request, ignore it. This normally
|
|
// means the user changed view settings between the time the page loaded
|
|
// and the content filled.
|
|
return;
|
|
}
|
|
|
|
// As we populate the changeset list, we try to hold the document scroll
|
|
// position steady, so that, e.g., users who want to leave a comment on a
|
|
// diff with a large number of changes don't constantly have the text
|
|
// area scrolled off the bottom of the screen until the entire diff loads.
|
|
//
|
|
// There are several major cases here:
|
|
//
|
|
// - If we're near the top of the document, never scroll.
|
|
// - If we're near the bottom of the document, always scroll, unless
|
|
// we have an anchor.
|
|
// - Otherwise, scroll if the changes were above (or, at least,
|
|
// almost entirely above) the viewport.
|
|
//
|
|
// We don't scroll if the changes were just near the top of the viewport
|
|
// because this makes us scroll incorrectly when an anchored change is
|
|
// visible. See T12779.
|
|
|
|
var target = this._node;
|
|
|
|
var old_pos = JX.Vector.getScroll();
|
|
var old_view = JX.Vector.getViewport();
|
|
var old_dim = JX.Vector.getDocument();
|
|
|
|
// Number of pixels away from the top or bottom of the document which
|
|
// count as "nearby".
|
|
var sticky = 480;
|
|
|
|
var near_top = (old_pos.y <= sticky);
|
|
var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
|
|
|
|
// If we have an anchor in the URL, never stick to the bottom of the
|
|
// page. See T11784 for discussion.
|
|
if (window.location.hash) {
|
|
near_bot = false;
|
|
}
|
|
|
|
var target_pos = JX.Vector.getPos(target);
|
|
var target_dim = JX.Vector.getDim(target);
|
|
var target_bot = (target_pos.y + target_dim.y);
|
|
|
|
// Detect if the changeset is entirely (or, at least, almost entirely)
|
|
// above us. The height here is roughly the height of the persistent
|
|
// banner.
|
|
var above_screen = (target_bot < old_pos.y + 64);
|
|
|
|
// If we have a URL anchor and are currently nearby, stick to it
|
|
// no matter what.
|
|
var on_target = null;
|
|
if (window.location.hash) {
|
|
try {
|
|
var anchor = JX.$(window.location.hash.replace('#', ''));
|
|
if (anchor) {
|
|
var anchor_pos = JX.$V(anchor);
|
|
if ((anchor_pos.y > old_pos.y) &&
|
|
(anchor_pos.y < old_pos.y + 96)) {
|
|
on_target = anchor;
|
|
}
|
|
}
|
|
} catch (ignored) {
|
|
// If we have a bogus anchor, just ignore it.
|
|
}
|
|
}
|
|
|
|
var frame = this._getContentFrame();
|
|
JX.DOM.setContent(frame, JX.$H(response.changeset));
|
|
|
|
if (this._stabilize) {
|
|
if (on_target) {
|
|
JX.DOM.scrollToPosition(old_pos.x, JX.$V(on_target).y - 60);
|
|
} else if (!near_top) {
|
|
if (near_bot || above_screen) {
|
|
// Figure out how much taller the document got.
|
|
var delta = (JX.Vector.getDocument().y - old_dim.y);
|
|
JX.DOM.scrollToPosition(old_pos.x, old_pos.y + delta);
|
|
}
|
|
}
|
|
this._stabilize = false;
|
|
}
|
|
|
|
this._onchangesetresponse(response);
|
|
},
|
|
|
|
_onchangesetresponse: function(response) {
|
|
// Code shared by autoload and context responses.
|
|
|
|
this._loadChangesetState(response);
|
|
|
|
JX.Stratcom.invoke('differential-inline-comment-refresh');
|
|
|
|
this._rebuildAllInlines();
|
|
|
|
JX.Stratcom.invoke('resize');
|
|
},
|
|
|
|
_loadChangesetState: function(state) {
|
|
if (state.coverage) {
|
|
for (var k in state.coverage) {
|
|
try {
|
|
JX.DOM.replace(JX.$(k), JX.$H(state.coverage[k]));
|
|
} catch (ignored) {
|
|
// Not terribly important.
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state.undoTemplates) {
|
|
this._undoTemplates = state.undoTemplates;
|
|
}
|
|
|
|
this._rendererKey = state.rendererKey;
|
|
this._highlight = state.highlight;
|
|
this._characterEncoding = state.characterEncoding;
|
|
this._requestDocumentEngineKey = state.requestDocumentEngineKey;
|
|
this._responseDocumentEngineKey = state.responseDocumentEngineKey;
|
|
this._availableDocumentEngineKeys = state.availableDocumentEngineKeys;
|
|
this._isHidden = state.isHidden;
|
|
|
|
var is_hidden = !this.isVisible();
|
|
if (this._isHidden != is_hidden) {
|
|
this.setVisible(!this._isHidden);
|
|
}
|
|
|
|
this._isLoading = false;
|
|
this.getPathView().setIsLoading(this._isLoading);
|
|
},
|
|
|
|
_getContentFrame: function() {
|
|
return JX.DOM.find(this._node, 'div', 'changeset-view-content');
|
|
},
|
|
|
|
_getRoutableKey: function() {
|
|
return 'changeset-view.' + this._ref + '.' + this._sequence;
|
|
},
|
|
|
|
getInlineForRow: function(node) {
|
|
var data = JX.Stratcom.getData(node);
|
|
|
|
if (!data.inline) {
|
|
var inline = new JX.DiffInline()
|
|
.setChangeset(this)
|
|
.bindToRow(node);
|
|
|
|
this._inlines.push(inline);
|
|
}
|
|
|
|
return data.inline;
|
|
},
|
|
|
|
newInlineForRange: function(origin, target) {
|
|
var list = this.getChangesetList();
|
|
|
|
var src = list.getLineNumberFromHeader(origin);
|
|
var dst = list.getLineNumberFromHeader(target);
|
|
|
|
var changeset_id = null;
|
|
var side = list.getDisplaySideFromHeader(origin);
|
|
if (side == 'right') {
|
|
changeset_id = this.getRightChangesetID();
|
|
} else {
|
|
changeset_id = this.getLeftChangesetID();
|
|
}
|
|
|
|
var is_new = false;
|
|
if (side == 'right') {
|
|
is_new = true;
|
|
} else if (this.getRightChangesetID() != this.getLeftChangesetID()) {
|
|
is_new = true;
|
|
}
|
|
|
|
var data = {
|
|
origin: origin,
|
|
target: target,
|
|
number: src,
|
|
length: dst - src,
|
|
changesetID: changeset_id,
|
|
displaySide: side,
|
|
isNewFile: is_new
|
|
};
|
|
|
|
var inline = new JX.DiffInline()
|
|
.setChangeset(this)
|
|
.bindToRange(data);
|
|
|
|
this._inlines.push(inline);
|
|
|
|
inline.create();
|
|
|
|
return inline;
|
|
},
|
|
|
|
newInlineReply: function(original, text) {
|
|
var inline = new JX.DiffInline()
|
|
.setChangeset(this)
|
|
.bindToReply(original);
|
|
|
|
this._inlines.push(inline);
|
|
|
|
inline.create(text);
|
|
|
|
return inline;
|
|
},
|
|
|
|
getInlineByID: function(id) {
|
|
return this._queryInline('id', id);
|
|
},
|
|
|
|
getInlineByPHID: function(phid) {
|
|
return this._queryInline('phid', phid);
|
|
},
|
|
|
|
_queryInline: function(field, value) {
|
|
// First, look for the inline in the objects we've already built.
|
|
var inline = this._findInline(field, value);
|
|
if (inline) {
|
|
return inline;
|
|
}
|
|
|
|
// If we haven't found a matching inline yet, rebuild all the inlines
|
|
// present in the document, then look again.
|
|
this._rebuildAllInlines();
|
|
return this._findInline(field, value);
|
|
},
|
|
|
|
_findInline: function(field, value) {
|
|
for (var ii = 0; ii < this._inlines.length; ii++) {
|
|
var inline = this._inlines[ii];
|
|
|
|
var target;
|
|
switch (field) {
|
|
case 'id':
|
|
target = inline.getID();
|
|
break;
|
|
case 'phid':
|
|
target = inline.getPHID();
|
|
break;
|
|
}
|
|
|
|
if (target == value) {
|
|
return inline;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
getInlines: function() {
|
|
this._rebuildAllInlines();
|
|
return this._inlines;
|
|
},
|
|
|
|
_rebuildAllInlines: function() {
|
|
var rows = JX.DOM.scry(this._node, 'tr');
|
|
var ii;
|
|
for (ii = 0; ii < rows.length; ii++) {
|
|
var row = rows[ii];
|
|
if (this._getRowType(row) != 'comment') {
|
|
continue;
|
|
}
|
|
|
|
// As a side effect, this builds any missing inline objects and adds
|
|
// them to this Changeset's list of inlines.
|
|
this.getInlineForRow(row);
|
|
}
|
|
},
|
|
|
|
redrawFileTree: function() {
|
|
var inlines = this._inlines;
|
|
var done = [];
|
|
var undone = [];
|
|
var inline;
|
|
|
|
for (var ii = 0; ii < inlines.length; ii++) {
|
|
inline = inlines[ii];
|
|
|
|
if (inline.isDeleted()) {
|
|
continue;
|
|
}
|
|
|
|
if (inline.isUndo()) {
|
|
continue;
|
|
}
|
|
|
|
if (inline.isSynthetic()) {
|
|
continue;
|
|
}
|
|
|
|
if (inline.isEditing()) {
|
|
continue;
|
|
}
|
|
|
|
if (!inline.getID()) {
|
|
// These are new comments which have been cancelled, and do not
|
|
// count as anything.
|
|
continue;
|
|
}
|
|
|
|
if (inline.isDraft()) {
|
|
continue;
|
|
}
|
|
|
|
if (!inline.isDone()) {
|
|
undone.push(inline);
|
|
} else {
|
|
done.push(inline);
|
|
}
|
|
}
|
|
|
|
var total = done.length + undone.length;
|
|
|
|
var hint;
|
|
var is_visible;
|
|
var is_completed;
|
|
if (total) {
|
|
if (done.length) {
|
|
hint = [done.length, '/', total];
|
|
} else {
|
|
hint = total;
|
|
}
|
|
is_visible = true;
|
|
is_completed = (done.length == total);
|
|
} else {
|
|
hint = '-';
|
|
is_visible = false;
|
|
is_completed = false;
|
|
}
|
|
|
|
var node = this.getPathView().getInlineNode();
|
|
|
|
JX.DOM.setContent(node, hint);
|
|
|
|
JX.DOM.alterClass(node, 'diff-tree-path-inlines-visible', is_visible);
|
|
JX.DOM.alterClass(node, 'diff-tree-path-inlines-completed', is_completed);
|
|
},
|
|
|
|
_onClickHeader: function(e) {
|
|
// If the user clicks the actual path name text, don't count this as
|
|
// a selection action: we want to let them select the path.
|
|
var path_name = e.getNode('changeset-header-path-name');
|
|
if (path_name) {
|
|
return;
|
|
}
|
|
|
|
e.prevent();
|
|
|
|
if (this._isSelected) {
|
|
this.getChangesetList().selectChangeset(null);
|
|
} else {
|
|
this.select(false);
|
|
}
|
|
},
|
|
|
|
toggleVisibility: function() {
|
|
this.setVisible(!this._visible);
|
|
|
|
var attrs = {
|
|
hidden: this.isVisible() ? 0 : 1,
|
|
discard: 1
|
|
};
|
|
|
|
var workflow = this._newReloadWorkflow(attrs)
|
|
.setHandler(JX.bag);
|
|
|
|
this._startContentWorkflow(workflow);
|
|
},
|
|
|
|
setVisible: function(visible) {
|
|
this._visible = visible;
|
|
|
|
var diff = this._getDiffNode();
|
|
var options = this._getViewButtonNode();
|
|
var show = this._getShowButtonNode();
|
|
|
|
if (this._visible) {
|
|
JX.DOM.show(diff);
|
|
JX.DOM.show(options);
|
|
JX.DOM.hide(show);
|
|
} else {
|
|
JX.DOM.hide(diff);
|
|
JX.DOM.hide(options);
|
|
JX.DOM.show(show);
|
|
|
|
if (this._viewMenu) {
|
|
this._viewMenu.close();
|
|
}
|
|
}
|
|
|
|
JX.Stratcom.invoke('resize');
|
|
|
|
var node = this._node;
|
|
JX.DOM.alterClass(node, 'changeset-content-hidden', !this._visible);
|
|
|
|
this.getPathView().setIsHidden(!this._visible);
|
|
},
|
|
|
|
setIsSelected: function(is_selected) {
|
|
this._isSelected = !!is_selected;
|
|
|
|
var node = this._node;
|
|
JX.DOM.alterClass(node, 'changeset-selected', this._isSelected);
|
|
|
|
return this;
|
|
},
|
|
|
|
_getDiffNode: function() {
|
|
if (!this._diffNode) {
|
|
this._diffNode = JX.DOM.find(this._node, 'table', 'differential-diff');
|
|
}
|
|
return this._diffNode;
|
|
},
|
|
|
|
_getViewButtonNode: function() {
|
|
if (!this._viewButtonNode) {
|
|
this._viewButtonNode = JX.DOM.find(
|
|
this._node,
|
|
'a',
|
|
'differential-view-options');
|
|
}
|
|
return this._viewButtonNode;
|
|
},
|
|
|
|
_getShowButtonNode: function() {
|
|
if (!this._showButtonNode) {
|
|
var pht = this.getChangesetList().getTranslations();
|
|
|
|
var show_button = new JX.PHUIXButtonView()
|
|
.setIcon('fa-angle-double-down')
|
|
.setText(pht('Show Changeset'))
|
|
.setColor('grey');
|
|
|
|
var button_node = show_button.getNode();
|
|
this._getViewButtonNode().parentNode.appendChild(button_node);
|
|
|
|
var onshow = JX.bind(this, this._onClickShowButton);
|
|
JX.DOM.listen(button_node, 'click', null, onshow);
|
|
|
|
this._showButtonNode = button_node;
|
|
}
|
|
return this._showButtonNode;
|
|
},
|
|
|
|
_onClickShowButton: function(e) {
|
|
e.prevent();
|
|
|
|
// We're always showing the changeset, but want to make sure the state
|
|
// change is persisted on the server.
|
|
this.toggleVisibility();
|
|
},
|
|
|
|
isVisible: function() {
|
|
return this._visible;
|
|
},
|
|
|
|
getPathView: function() {
|
|
if (!this._pathView) {
|
|
var view = new JX.DiffPathView()
|
|
.setChangeset(this)
|
|
.setPath(this._pathParts)
|
|
.setIsLowImportance(this._isLowImportance)
|
|
.setIsOwned(this._isOwned)
|
|
.setIsLoading(this._isLoading);
|
|
|
|
view.getIcon()
|
|
.setIcon(this._pathIconIcon)
|
|
.setColor(this._pathIconColor);
|
|
|
|
this._pathView = view;
|
|
}
|
|
|
|
return this._pathView;
|
|
},
|
|
|
|
select: function(scroll) {
|
|
this.getChangesetList().selectChangeset(this, scroll);
|
|
return this;
|
|
}
|
|
},
|
|
|
|
statics: {
|
|
getForNode: function(node) {
|
|
var data = JX.Stratcom.getData(node);
|
|
if (!data.changesetViewManager) {
|
|
data.changesetViewManager = new JX.DiffChangeset(node);
|
|
}
|
|
return data.changesetViewManager;
|
|
}
|
|
}
|
|
});
|