diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -12,7 +12,7 @@ 'core.pkg.css' => 'a4a2417c', 'core.pkg.js' => '4355a8d3', 'differential.pkg.css' => '607c84be', - 'differential.pkg.js' => 'e40c5192', + 'differential.pkg.js' => '688775a9', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => 'a98c0bf7', 'maniphest.pkg.css' => '35995d6d', @@ -378,9 +378,11 @@ 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'a2ab19be', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', - 'rsrc/js/application/diff/DiffChangeset.js' => '5a4e4a3b', - 'rsrc/js/application/diff/DiffChangesetList.js' => '3ac694dd', + 'rsrc/js/application/diff/DiffChangeset.js' => 'd1eda7b4', + 'rsrc/js/application/diff/DiffChangesetList.js' => '469beba0', 'rsrc/js/application/diff/DiffInline.js' => '16e97ebc', + 'rsrc/js/application/diff/DiffPathView.js' => 'e5166692', + 'rsrc/js/application/diff/DiffTreeView.js' => '4055adeb', 'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17', 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', 'rsrc/js/application/differential/behavior-populate.js' => 'b86ef6c2', @@ -778,9 +780,11 @@ 'phabricator-darklog' => '3b869402', 'phabricator-darkmessage' => '26cd4b73', 'phabricator-dashboard-css' => '5a205b9d', - 'phabricator-diff-changeset' => '5a4e4a3b', - 'phabricator-diff-changeset-list' => '3ac694dd', + 'phabricator-diff-changeset' => 'd1eda7b4', + 'phabricator-diff-changeset-list' => '469beba0', 'phabricator-diff-inline' => '16e97ebc', + 'phabricator-diff-path-view' => 'e5166692', + 'phabricator-diff-tree-view' => '4055adeb', 'phabricator-drag-and-drop-file-upload' => '4370900d', 'phabricator-draggable-list' => '0169e425', 'phabricator-fatal-config-template-css' => '20babf50', @@ -1240,10 +1244,6 @@ 'trigger-rule', 'trigger-rule-type', ), - '3ac694dd' => array( - 'javelin-install', - 'phuix-button-view', - ), '3ae89b20' => array( 'phui-workcard-view-css', ), @@ -1267,6 +1267,9 @@ 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), + '4055adeb' => array( + 'javelin-dom', + ), '407ee861' => array( 'javelin-behavior', 'javelin-uri', @@ -1316,6 +1319,11 @@ 'javelin-util', 'phabricator-busy', ), + '469beba0' => array( + 'javelin-install', + 'phuix-button-view', + 'phabricator-diff-tree-view', + ), '47a0728b' => array( 'javelin-behavior', 'javelin-dom', @@ -1466,17 +1474,6 @@ 'javelin-dom', 'javelin-history', ), - '5a4e4a3b' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - 'javelin-workflow', - 'javelin-router', - 'javelin-behavior-device', - 'javelin-vector', - 'phabricator-diff-inline', - ), '5a6f6a06' => array( 'javelin-behavior', 'javelin-quicksand', @@ -2099,6 +2096,18 @@ 'javelin-workflow', 'javelin-util', ), + 'd1eda7b4' => array( + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-install', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', + 'javelin-vector', + 'phabricator-diff-inline', + 'phabricator-diff-path-view', + ), 'd3799cb4' => array( 'javelin-install', ), @@ -2125,6 +2134,9 @@ 'javelin-dom', 'phuix-dropdown-menu', ), + 'e5166692' => array( + 'javelin-dom', + ), 'e5bdb730' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php --- a/src/applications/differential/view/DifferentialChangesetDetailView.php +++ b/src/applications/differential/view/DifferentialChangesetDetailView.php @@ -178,6 +178,9 @@ $changeset_state = null; } + $path_parts = trim($display_filename, '/'); + $path_parts = explode('/', $path_parts); + return javelin_tag( 'div', array( @@ -189,9 +192,9 @@ 'ref' => $this->getRenderingRef(), 'autoload' => $this->getAutoload(), 'displayPath' => hsprintf('%s', $display_parts), - 'path' => $display_filename, 'icon' => $display_icon, 'treeNodeID' => 'tree-node-'.$changeset->getAnchorName(), + 'pathParts' => $path_parts, 'editorURI' => $this->getEditorURI(), 'editorConfigureURI' => $this->getEditorConfigureURI(), diff --git a/webroot/rsrc/js/application/diff/DiffChangeset.js b/webroot/rsrc/js/application/diff/DiffChangeset.js --- a/webroot/rsrc/js/application/diff/DiffChangeset.js +++ b/webroot/rsrc/js/application/diff/DiffChangeset.js @@ -9,10 +9,10 @@ * javelin-behavior-device * javelin-vector * phabricator-diff-inline + * phabricator-diff-path-view * @javelin */ - JX.install('DiffChangeset', { construct : function(node) { @@ -29,6 +29,7 @@ this._rightID = data.right; this._displayPath = JX.$H(data.displayPath); + this._pathParts = data.pathParts; this._icon = data.icon; this._editorURI = data.editorURI; @@ -70,6 +71,7 @@ _editorURI: null, _editorConfigureURI: null, + _pathView: null, getEditorURI: function() { return this._editorURI; @@ -891,6 +893,21 @@ _onundo: function(e) { e.kill(); this.toggleVisibility(); + }, + + getPathView: function() { + if (!this._pathView) { + this._pathView = new JX.DiffPathView() + .setChangeset(this) + .setPath(this._pathParts); + } + + return this._pathView; + }, + + select: function(scroll) { + this.getChangesetList().selectChangeset(this, scroll); + return this; } }, diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js --- a/webroot/rsrc/js/application/diff/DiffChangesetList.js +++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js @@ -2,6 +2,7 @@ * @provides phabricator-diff-changeset-list * @requires javelin-install * phuix-button-view + * phabricator-diff-tree-view * @javelin */ @@ -640,6 +641,25 @@ }; }, + selectChangeset: function(changeset, scroll) { + var items = this._getSelectableItems(); + + var cursor = null; + for (var ii = 0; ii < items.length; ii++) { + var item = items[ii]; + if (changeset === item.target) { + cursor = ii; + break; + } + } + + if (cursor !== null) { + this._setSelectionState(items[cursor], true); + } + + return this; + }, + _setSelectionState: function(item, scroll) { this._cursorItem = item; this._redrawSelection(scroll); @@ -1467,10 +1487,18 @@ var node = this._getBannerNode(); var changeset = this._getVisibleChangeset(); + var tree = this._getTreeView(); + var formation = this.getFormationView(); if (!changeset) { this._bannerChangeset = null; JX.DOM.remove(node); + tree.setSelectedPath(null); + + if (formation) { + formation.repaint(); + } + return; } @@ -1481,6 +1509,14 @@ } this._bannerChangeset = changeset; + var paths = tree.getPaths(); + for (var ii = 0; ii < paths.length; ii++) { + var path = paths[ii]; + if (path.getChangeset() === changeset) { + tree.setSelectedPath(path); + } + } + var inlines = this._getInlinesByType(); var unsaved = inlines.unsaved; @@ -1586,6 +1622,10 @@ JX.DOM.setContent(node, [buttons_view, path_view]); document.body.appendChild(node); + + if (formation) { + formation.repaint(); + } }, _getInlinesByType: function() { @@ -1958,6 +1998,20 @@ return null; }, + _getTreeView: function() { + if (!this._treeView) { + var tree = new JX.DiffTreeView(); + + for (var ii = 0; ii < this._changesets.length; ii++) { + var changeset = this._changesets[ii]; + tree.addPath(changeset.getPathView()); + } + + this._treeView = tree; + } + return this._treeView; + }, + _redrawFiletree : function() { var formation = this.getFormationView(); @@ -1970,15 +2024,8 @@ var flank_body = flank.getBodyNode(); - var items = []; - for (var ii = 0; ii < this._changesets.length; ii++) { - var changeset = this._changesets[ii]; - - var node = JX.$N('div', {}, changeset.getDisplayPath()); - items.push(node); - } - - JX.DOM.setContent(flank_body, items); + var tree = this._getTreeView(); + JX.DOM.setContent(flank_body, tree.getNode()); } } diff --git a/webroot/rsrc/js/application/diff/DiffPathView.js b/webroot/rsrc/js/application/diff/DiffPathView.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/diff/DiffPathView.js @@ -0,0 +1,83 @@ +/** + * @provides phabricator-diff-path-view + * @requires javelin-dom + * @javelin + */ + +JX.install('DiffPathView', { + + construct: function() { + }, + + properties: { + changeset: null + }, + + members: { + _node: null, + _path: null, + _depth: 0, + _selected: false, + + getNode: function() { + if (!this._node) { + this._node = JX.$N('li'); + + var onclick = JX.bind(this, this._onclick); + JX.DOM.listen(this._node, 'click', null, onclick); + } + return this._node; + }, + + setPath: function(path) { + this._path = path; + this._redraw(); + return this; + }, + + getPath: function() { + return this._path; + }, + + setDepth: function(depth) { + this._depth = depth; + this._redraw(); + return this; + }, + + setIsSelected: function(selected) { + this._selected = selected; + this._redraw(); + return this; + }, + + _onclick: function(e) { + if (!e.isNormalClick()) { + return; + } + + var changeset = this.getChangeset(); + if (changeset) { + changeset.select(true); + } + + e.kill(); + }, + + _redraw: function() { + var node = this.getNode(); + + node.style.paddingLeft = (8 * this._depth) + 'px'; + + var display = this._path[this._path.length - 1]; + + if (this._selected) { + display = ['*', display]; + } + + JX.DOM.setContent(node, display); + } + + } + +}); diff --git a/webroot/rsrc/js/application/diff/DiffTreeView.js b/webroot/rsrc/js/application/diff/DiffTreeView.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/diff/DiffTreeView.js @@ -0,0 +1,159 @@ +/** + * @provides phabricator-diff-tree-view + * @requires javelin-dom + * @javelin + */ + +JX.install('DiffTreeView', { + + construct: function() { + this._keys = []; + this._tree = this._newTreeNode(null, [], 0); + this._nodes = {}; + this._paths = []; + }, + + members: { + _node: null, + _keys: null, + _tree: null, + _nodes: null, + _dirty: false, + _paths: null, + _selectedPath: null, + + getNode: function() { + if (!this._node) { + this._node = JX.$N('ul'); + } + + if (this._dirty) { + this.redraw(); + } + + return this._node; + }, + + addPath: function(path) { + this._paths.push(path); + + var tree = this._getTree(this._tree, path.getPath(), 0); + tree.pathObject = path; + + this._dirty = true; + + return this; + }, + + getPaths: function() { + return this._paths; + }, + + setSelectedPath: function(path) { + if (this._selectedPath) { + this._selectedPath.setIsSelected(false); + this._selectedPath = null; + } + + if (path) { + path.setIsSelected(true); + } + + this._selectedPath = path; + + return this; + }, + + redraw: function() { + if (!this._dirty) { + return; + } + this._dirty = false; + + var ii; + + // For nodes which don't have a path object yet, build one. + var tree; + var trees = []; + for (ii = 0; ii < this._keys.length; ii++) { + var key = this._keys[ii]; + tree = this._nodes[key]; + var path = tree.pathObject; + + if (!path) { + path = new JX.DiffPathView() + .setPath(tree.parts); + tree.pathObject = path; + } + + trees.push(tree); + } + + for (ii = 0; ii < trees.length; ii++) { + tree = trees[ii]; + + if (!tree.parent) { + tree.depth = 0; + } else { + tree.depth = tree.parent.depth + 1; + } + + tree.pathObject.setDepth((tree.depth - 1)); + } + + var nodes = []; + for (ii = 0; ii < trees.length; ii++) { + tree = trees[ii]; + nodes.push(tree.pathObject.getNode()); + } + + JX.DOM.setContent(this.getNode(), nodes); + }, + + _getTree: function(root, path, ii) { + if (ii >= path.length) { + return root; + } + + var part = path[ii]; + + if (!root.children.hasOwnProperty(part)) { + root.children[part] = this._newTreeNode(root, path, ii); + root.childCount++; + } + + return this._getTree(root.children[part], path, ii + 1); + }, + + _newTreeNode: function(parent, path, ii) { + var key; + var parts; + if (path.length) { + parts = path.slice(0, ii + 1); + key = parts.join('/'); + this._keys.push(key); + } else { + parts = []; + key = null; + } + + var node = { + parent: parent, + nodeKey: key, + parts: parts, + children: {}, + pathObject: null, + childCount: 0, + depth: 0 + }; + + if (key !== null) { + this._nodes[key] = node; + } + + return node; + } + + } + +});