diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,11 +7,11 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'c7fc5aec', - 'core.pkg.js' => '10275c16', + 'core.pkg.css' => '15cd7345', + 'core.pkg.js' => '00dbc918', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'b3eea3f5', - 'differential.pkg.js' => '4b7d8f19', + 'differential.pkg.js' => 'd574cebf', 'diffusion.pkg.css' => '91c5d3a6', 'diffusion.pkg.js' => '3a9a8bfa', 'maniphest.pkg.css' => '4845691a', @@ -124,7 +124,7 @@ 'rsrc/css/phui/phui-badge.css' => '3baef8db', 'rsrc/css/phui/phui-big-info-view.css' => 'bd903741', 'rsrc/css/phui/phui-box.css' => '5c8387cf', - 'rsrc/css/phui/phui-button.css' => 'a64a8de6', + 'rsrc/css/phui/phui-button.css' => '287efbfa', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-crumbs-view.css' => '6b813619', 'rsrc/css/phui/phui-curtain-view.css' => '7148ae25', @@ -230,7 +230,7 @@ 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9', 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03', - 'rsrc/externals/javelin/lib/DOM.js' => '805b806a', + 'rsrc/externals/javelin/lib/DOM.js' => '5e5acdda', 'rsrc/externals/javelin/lib/History.js' => 'd4505101', 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 'rsrc/externals/javelin/lib/Leader.js' => 'fea0eb47', @@ -457,7 +457,7 @@ 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', - 'rsrc/js/core/DragAndDropFileUpload.js' => '58dea2fa', + 'rsrc/js/core/DragAndDropFileUpload.js' => 'd1d13c52', 'rsrc/js/core/DraggableList.js' => '5a13c79f', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', @@ -515,14 +515,14 @@ 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d', 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/phtize.js' => 'd254d646', - 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475', + 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '3d948804', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 'rsrc/js/phui/behavior-phui-profile-menu.js' => '12884df9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '9196fb06', - 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', + 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '1622a72d', 'rsrc/js/phuix/PHUIXFormControl.js' => 'e15869a8', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), @@ -668,7 +668,7 @@ 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d', 'javelin-behavior-pholio-mock-edit' => 'bee502c8', 'javelin-behavior-pholio-mock-view' => 'fbe497e7', - 'javelin-behavior-phui-dropdown-menu' => '54733475', + 'javelin-behavior-phui-dropdown-menu' => '3d948804', 'javelin-behavior-phui-file-upload' => 'b003d4fb', 'javelin-behavior-phui-hovercards' => 'bcaccd64', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', @@ -706,7 +706,7 @@ 'javelin-color' => '7e41274a', 'javelin-cookie' => '62dfea03', 'javelin-diffusion-locate-file-source' => 'b42eddc7', - 'javelin-dom' => '805b806a', + 'javelin-dom' => '5e5acdda', 'javelin-dynval' => 'f6555212', 'javelin-event' => '2ee659ce', 'javelin-fx' => '54b612ba', @@ -769,7 +769,7 @@ 'phabricator-core-css' => 'd0801452', 'phabricator-countdown-css' => '16c52f5c', 'phabricator-dashboard-css' => 'bc6f2127', - 'phabricator-drag-and-drop-file-upload' => '58dea2fa', + 'phabricator-drag-and-drop-file-upload' => 'd1d13c52', 'phabricator-draggable-list' => '5a13c79f', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', @@ -821,7 +821,7 @@ 'phui-badge-view-css' => '3baef8db', 'phui-big-info-view-css' => 'bd903741', 'phui-box-css' => '5c8387cf', - 'phui-button-css' => 'a64a8de6', + 'phui-button-css' => '287efbfa', 'phui-calendar-css' => 'ccabe893', 'phui-calendar-day-css' => 'd1cf6f93', 'phui-calendar-list-css' => '56e6381a', @@ -869,7 +869,7 @@ 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', - 'phuix-dropdown-menu' => 'bd4c8dca', + 'phuix-dropdown-menu' => '1622a72d', 'phuix-form-control-view' => 'e15869a8', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', @@ -1034,6 +1034,13 @@ 'javelin-workflow', 'javelin-workboard-controller', ), + '1622a72d' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-vector', + 'javelin-stratcom', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -1143,6 +1150,12 @@ 'javelin-util', 'javelin-uri', ), + '3d948804' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'phuix-dropdown-menu', + ), '3f5d6dbf' => array( 'javelin-behavior', 'javelin-dom', @@ -1274,12 +1287,6 @@ 'javelin-leader', 'javelin-json', ), - 54733475 => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'phuix-dropdown-menu', - ), '54b612ba' => array( 'javelin-color', 'javelin-install', @@ -1324,14 +1331,6 @@ 'javelin-request', 'javelin-util', ), - '58dea2fa' => array( - 'javelin-install', - 'javelin-util', - 'javelin-request', - 'javelin-dom', - 'javelin-uri', - 'phabricator-file-upload', - ), '59a7976a' => array( 'javelin-install', 'javelin-dom', @@ -1361,6 +1360,13 @@ 'javelin-stratcom', 'javelin-dom', ), + '5e5acdda' => array( + 'javelin-magical-init', + 'javelin-install', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + ), '5e9f347c' => array( 'javelin-behavior', 'multirow-row-manager', @@ -1521,13 +1527,6 @@ 'javelin-behavior', 'javelin-history', ), - '805b806a' => array( - 'javelin-magical-init', - 'javelin-install', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -1845,13 +1844,6 @@ 'javelin-vector', 'phui-hovercard', ), - 'bd4c8dca' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-vector', - 'javelin-stratcom', - ), 'bdaf4d04' => array( 'javelin-behavior', 'javelin-dom', @@ -1961,6 +1953,14 @@ 'javelin-dynval', 'javelin-reactor-dom', ), + 'd1d13c52' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-dom', + 'javelin-uri', + 'phabricator-file-upload', + ), 'd254d646' => array( 'javelin-util', ), diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -166,15 +166,6 @@ ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Merge Duplicates In')) - ->setHref("/search/attach/{$phid}/TASK/merge/") - ->setWorkflow(true) - ->setIcon('fa-compress') - ->setDisabled(!$can_edit) - ->setWorkflow(true)); - $edit_config = $edit_engine->loadDefaultEditConfiguration(); $can_create = (bool)$edit_config; @@ -195,7 +186,10 @@ $edit_uri = $this->getApplicationURI($edit_uri); } - $curtain->addAction( + $task_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $task_menu->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($edit_uri) @@ -203,7 +197,7 @@ ->setDisabled(!$can_create) ->setWorkflow(!$can_create)); - $curtain->addAction( + $task_menu->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Blocking Tasks')) ->setHref("/search/attach/{$phid}/TASK/blocks/") @@ -212,6 +206,20 @@ ->setDisabled(!$can_edit) ->setWorkflow(true)); + $task_menu->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Merge Duplicates In')) + ->setHref("/search/attach/{$phid}/TASK/merge/") + ->setWorkflow(true) + ->setIcon('fa-compress') + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Related Tasks...')) + ->setIcon('fa-anchor') + ->setDropdownMenu($task_menu)); $owner_phid = $task->getOwnerPHID(); $author_phid = $task->getAuthorPHID(); diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -14,6 +14,7 @@ private $metadata; private $selected; private $openInNewWindow; + private $isDropdown; public function setSelected($selected) { $this->selected = $selected; @@ -95,6 +96,24 @@ return $this->openInNewWindow; } + public function setDropdownMenu(PhabricatorActionListView $actions) { + Javelin::initBehavior('phui-dropdown-menu'); + + $this->addSigil('phui-dropdown-menu'); + $this->setMetadata( + array( + 'width' => 'auto', + ) + $actions->getDropdownMenuMetadata()); + + if (!$this->getHref()) { + $this->setHref('#'); + } + + $this->isDropdown = true; + + return $this; + } + public function render() { $icon = null; @@ -155,6 +174,12 @@ $target = null; } + if ($this->isDropdown) { + $caret = phutil_tag('span', array('class' => 'caret'), ''); + } else { + $caret = null; + } + $item = javelin_tag( 'a', array( @@ -164,7 +189,7 @@ 'sigil' => $sigils, 'meta' => $this->metadata, ), - array($icon, $this->name)); + array($icon, $this->name, $caret)); } } else { $item = phutil_tag( diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -327,9 +327,7 @@ 'sigil' => $sigil, 'aria-haspopup' => 'true', 'aria-expanded' => 'false', - 'meta' => array( - 'items' => hsprintf('%s', $action_list), - ), + 'meta' => $action_list->getDropdownMenuMetadata(), ), array( $aural, diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css --- a/webroot/rsrc/css/phui/phui-button.css +++ b/webroot/rsrc/css/phui/phui-button.css @@ -199,6 +199,7 @@ border: 1px solid {$blueborder}; border-radius: 3px; margin-bottom: 16px; + box-sizing: border-box; } .phuix-dropdown-menu a:focus { @@ -256,6 +257,20 @@ margin-left: 6px; } +.phabricator-action-view-item .caret { + float: right; + margin-top: 7px; + margin-right: 6px; + border-top-color: {$greytext}; +} + +a.phabricator-action-view-item.phuix-dropdown-open, +.device-desktop + .phabricator-action-view:hover + a.phabricator-action-view-item.phuix-dropdown-open { + background: {$darkgreybackground}; +} + .small.dropdown .caret { margin-top: 6px; } diff --git a/webroot/rsrc/externals/javelin/lib/DOM.js b/webroot/rsrc/externals/javelin/lib/DOM.js --- a/webroot/rsrc/externals/javelin/lib/DOM.js +++ b/webroot/rsrc/externals/javelin/lib/DOM.js @@ -1011,6 +1011,25 @@ } }, + + /** + * Test if a node is a container for a child. + * + * @param Node Container node. + * @param Node Child node. + * @return bool True if the child exists inside the container. + */ + contains: function(container, child) { + do { + if (child === container) { + return true; + } + child = child.parentNode; + } while (child); + + return false; + }, + _getAutoID : function(node) { if (!node.getAttribute('data-autoid')) { node.setAttribute('data-autoid', 'autoid_'+(++JX.DOM._autoid)); diff --git a/webroot/rsrc/js/core/DragAndDropFileUpload.js b/webroot/rsrc/js/core/DragAndDropFileUpload.js --- a/webroot/rsrc/js/core/DragAndDropFileUpload.js +++ b/webroot/rsrc/js/core/DragAndDropFileUpload.js @@ -73,19 +73,6 @@ }, start : function() { - - // TODO: move this to JX.DOM.contains()? - function contains(container, child) { - do { - if (child === container) { - return true; - } - child = child.parentNode; - } while (child); - - return false; - } - // Firefox has some issues sometimes; implement this click handler so // the user can recover. See T5188. var on_click = JX.bind(this, function (e) { @@ -115,7 +102,7 @@ } } - if (contains(this._getTarget(), e.getTarget())) { + if (JX.DOM.contains(this._getTarget(), e.getTarget())) { this._updateDepth(1); } @@ -130,7 +117,7 @@ return; } - if (contains(this._getTarget(), e.getTarget())) { + if (JX.DOM.contains(this._getTarget(), e.getTarget())) { this._updateDepth(-1); } }); diff --git a/webroot/rsrc/js/phui/behavior-phui-dropdown-menu.js b/webroot/rsrc/js/phui/behavior-phui-dropdown-menu.js --- a/webroot/rsrc/js/phui/behavior-phui-dropdown-menu.js +++ b/webroot/rsrc/js/phui/behavior-phui-dropdown-menu.js @@ -21,6 +21,7 @@ var icon = e.getNode('phui-dropdown-menu'); data.menu = new JX.PHUIXDropdownMenu(icon); data.menu.setContent(list); + data.menu.setWidth(data.width || null); data.menu.open(); JX.DOM.listen(list, 'click', 'tag:a', function(e) { diff --git a/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js --- a/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js +++ b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js @@ -51,6 +51,11 @@ offsetY: 0 }, + statics: { + _lastDim: null, + _lastPos: null + }, + members: { _node: null, _menu: null, @@ -80,6 +85,13 @@ if (!this._open) { return; } + + // Save this menu's position and dimensions as the last closed menu. + // We'll reuse them if we're opening a submenu. + var self = JX.PHUIXDropdownMenu; + self._lastPos = JX.$V(this._menu); + self._lastDim = JX.Vector.getDim(this._menu); + this._open = false; this._hide(); @@ -120,11 +132,8 @@ } var t = e.getTarget(); - while (t) { - if (t == this._menu || t == this._node) { - return; - } - t = t.parentNode; + if (JX.DOM.contains(this._menu, t) || JX.DOM.contains(this._node, t)) { + return; } this.close(); @@ -133,7 +142,14 @@ _show : function() { document.body.appendChild(this._menu); - if (this.getWidth()) { + var width = this.getWidth(); + if (width == 'auto') { + if (JX.DOM.contains(document, this._node)) { + var d = new JX.Vector.getDim(this._node); + d.y = null; + d.setDim(this._menu); + } + } else if (width) { new JX.Vector(this.getWidth(), null).setDim(this._menu); } @@ -168,6 +184,18 @@ var v = JX.$V(this._node); var d = JX.Vector.getDim(this._node); + // If the node has been removed from the DOM, assume we are opening + // a new dropdown from an existing dropdown. We're just going to replace + // the old dropdown. + if (!JX.DOM.contains(document, this._node)) { + var self = JX.PHUIXDropdownMenu; + if (self._lastPos) { + v = self._lastPos; + d = self._lastDim; + d.y = 0; + } + } + switch (this.getAlign()) { case 'right': v = v.add(d)