@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fold task-relationship actions into an accordion dropdown

Summary:
Ref T11179. Alternative to D16152. I think this turned out a bit better than the other one did.

Currently, we render two copies of the menu (one for mobile, one for desktop). A big chunk of this is sharing the nodes instead: when you open the mobile dropdown menu, it steals the nodes from the document. When you close it, it puts them back. Magic! Sneaky!

Test Plan:
{F1695499}

{F1695500}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11179

Differential Revision: https://secure.phabricator.com/D16157

+318 -58
+28 -21
resources/celerity/map.php
··· 7 7 */ 8 8 return array( 9 9 'names' => array( 10 - 'core.pkg.css' => 'b9e2e1e5', 11 - 'core.pkg.js' => '80f86a0a', 10 + 'core.pkg.css' => 'f577cd20', 11 + 'core.pkg.js' => 'f2139810', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => 'b3eea3f5', 14 14 'differential.pkg.js' => '4b7d8f19', ··· 124 124 'rsrc/css/phui/phui-badge.css' => '3baef8db', 125 125 'rsrc/css/phui/phui-big-info-view.css' => 'bd903741', 126 126 'rsrc/css/phui/phui-box.css' => '5c8387cf', 127 - 'rsrc/css/phui/phui-button.css' => 'a64a8de6', 127 + 'rsrc/css/phui/phui-button.css' => 'e266e0bc', 128 128 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 129 129 'rsrc/css/phui/phui-crumbs-view.css' => '6b813619', 130 130 'rsrc/css/phui/phui-curtain-view.css' => '7148ae25', ··· 516 516 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d', 517 517 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 518 518 'rsrc/js/core/phtize.js' => 'd254d646', 519 - 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475', 519 + 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '1aa4c968', 520 520 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', 521 521 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 522 522 'rsrc/js/phui/behavior-phui-profile-menu.js' => '12884df9', 523 + 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', 523 524 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 524 525 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 525 526 'rsrc/js/phuix/PHUIXAutocomplete.js' => '9196fb06', 526 - 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', 527 + 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '82e270da', 527 528 'rsrc/js/phuix/PHUIXFormControl.js' => 'e15869a8', 528 529 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', 529 530 ), ··· 669 670 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d', 670 671 'javelin-behavior-pholio-mock-edit' => 'bee502c8', 671 672 'javelin-behavior-pholio-mock-view' => 'fbe497e7', 672 - 'javelin-behavior-phui-dropdown-menu' => '54733475', 673 + 'javelin-behavior-phui-dropdown-menu' => '1aa4c968', 673 674 'javelin-behavior-phui-file-upload' => 'b003d4fb', 674 675 'javelin-behavior-phui-hovercards' => 'bcaccd64', 675 676 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 676 677 'javelin-behavior-phui-profile-menu' => '12884df9', 678 + 'javelin-behavior-phui-submenu' => 'a6f7a73b', 677 679 'javelin-behavior-policy-control' => 'd0c516d5', 678 680 'javelin-behavior-policy-rule-editor' => '5e9f347c', 679 681 'javelin-behavior-project-boards' => '14a1faae', ··· 822 824 'phui-badge-view-css' => '3baef8db', 823 825 'phui-big-info-view-css' => 'bd903741', 824 826 'phui-box-css' => '5c8387cf', 825 - 'phui-button-css' => 'a64a8de6', 827 + 'phui-button-css' => 'e266e0bc', 826 828 'phui-calendar-css' => 'ccabe893', 827 829 'phui-calendar-day-css' => 'd1cf6f93', 828 830 'phui-calendar-list-css' => '56e6381a', ··· 870 872 'phuix-action-list-view' => 'b5c256b8', 871 873 'phuix-action-view' => '8cf6d262', 872 874 'phuix-autocomplete' => '9196fb06', 873 - 'phuix-dropdown-menu' => 'bd4c8dca', 875 + 'phuix-dropdown-menu' => '82e270da', 874 876 'phuix-form-control-view' => 'e15869a8', 875 877 'phuix-icon-view' => 'bff6884b', 876 878 'policy-css' => '957ea14c', ··· 1024 1026 'javelin-workflow', 1025 1027 'javelin-workboard-controller', 1026 1028 ), 1029 + '1aa4c968' => array( 1030 + 'javelin-behavior', 1031 + 'javelin-stratcom', 1032 + 'javelin-dom', 1033 + 'phuix-dropdown-menu', 1034 + ), 1027 1035 '1ad0a787' => array( 1028 1036 'javelin-install', 1029 1037 'javelin-reactor', ··· 1275 1283 'javelin-leader', 1276 1284 'javelin-json', 1277 1285 ), 1278 - 54733475 => array( 1279 - 'javelin-behavior', 1280 - 'javelin-stratcom', 1281 - 'javelin-dom', 1282 - 'phuix-dropdown-menu', 1283 - ), 1284 1286 '54b612ba' => array( 1285 1287 'javelin-color', 1286 1288 'javelin-install', ··· 1526 1528 'javelin-magical-init', 1527 1529 'javelin-install', 1528 1530 'javelin-util', 1531 + 'javelin-vector', 1532 + 'javelin-stratcom', 1533 + ), 1534 + '82e270da' => array( 1535 + 'javelin-install', 1536 + 'javelin-util', 1537 + 'javelin-dom', 1529 1538 'javelin-vector', 1530 1539 'javelin-stratcom', 1531 1540 ), ··· 1693 1702 'javelin-uri', 1694 1703 'phabricator-notification', 1695 1704 ), 1705 + 'a6f7a73b' => array( 1706 + 'javelin-behavior', 1707 + 'javelin-stratcom', 1708 + 'javelin-dom', 1709 + ), 1696 1710 'a80d0378' => array( 1697 1711 'javelin-behavior', 1698 1712 'javelin-stratcom', ··· 1845 1859 'javelin-stratcom', 1846 1860 'javelin-vector', 1847 1861 'phui-hovercard', 1848 - ), 1849 - 'bd4c8dca' => array( 1850 - 'javelin-install', 1851 - 'javelin-util', 1852 - 'javelin-dom', 1853 - 'javelin-vector', 1854 - 'javelin-stratcom', 1855 1862 ), 1856 1863 'bdaf4d04' => array( 1857 1864 'javelin-behavior',
+4 -1
src/applications/base/controller/PhabricatorController.php
··· 474 474 public function newCurtainView($object) { 475 475 $viewer = $this->getViewer(); 476 476 477 + $action_id = celerity_generate_unique_node_id(); 478 + 477 479 $action_list = id(new PhabricatorActionListView()) 478 - ->setViewer($viewer); 480 + ->setViewer($viewer) 481 + ->setID($action_id); 479 482 480 483 // NOTE: Applications (objects of class PhabricatorApplication) can't 481 484 // currently be set here, although they don't need any of the extensions
+27 -23
src/applications/maniphest/controller/ManiphestTaskDetailController.php
··· 166 166 ->setDisabled(!$can_edit) 167 167 ->setWorkflow(!$can_edit)); 168 168 169 - $curtain->addAction( 170 - id(new PhabricatorActionView()) 171 - ->setName(pht('Merge Duplicates In')) 172 - ->setHref("/search/attach/{$phid}/TASK/merge/") 173 - ->setWorkflow(true) 174 - ->setIcon('fa-compress') 175 - ->setDisabled(!$can_edit) 176 - ->setWorkflow(true)); 177 - 178 169 $edit_config = $edit_engine->loadDefaultEditConfiguration(); 179 170 $can_create = (bool)$edit_config; 180 171 ··· 195 186 $edit_uri = $this->getApplicationURI($edit_uri); 196 187 } 197 188 198 - $curtain->addAction( 199 - id(new PhabricatorActionView()) 200 - ->setName(pht('Create Subtask')) 201 - ->setHref($edit_uri) 202 - ->setIcon('fa-level-down') 203 - ->setDisabled(!$can_create) 204 - ->setWorkflow(!$can_create)); 189 + $task_submenu = array(); 190 + 191 + $task_submenu[] = id(new PhabricatorActionView()) 192 + ->setName(pht('Create Subtask')) 193 + ->setHref($edit_uri) 194 + ->setIcon('fa-level-down') 195 + ->setDisabled(!$can_create) 196 + ->setWorkflow(!$can_create); 197 + 198 + $task_submenu[] = id(new PhabricatorActionView()) 199 + ->setName(pht('Edit Blocking Tasks')) 200 + ->setHref("/search/attach/{$phid}/TASK/blocks/") 201 + ->setWorkflow(true) 202 + ->setIcon('fa-link') 203 + ->setDisabled(!$can_edit) 204 + ->setWorkflow(true); 205 + 206 + $task_submenu[] = id(new PhabricatorActionView()) 207 + ->setName(pht('Merge Duplicates In')) 208 + ->setHref("/search/attach/{$phid}/TASK/merge/") 209 + ->setWorkflow(true) 210 + ->setIcon('fa-compress') 211 + ->setDisabled(!$can_edit) 212 + ->setWorkflow(true); 205 213 206 214 $curtain->addAction( 207 215 id(new PhabricatorActionView()) 208 - ->setName(pht('Edit Blocking Tasks')) 209 - ->setHref("/search/attach/{$phid}/TASK/blocks/") 210 - ->setWorkflow(true) 211 - ->setIcon('fa-link') 212 - ->setDisabled(!$can_edit) 213 - ->setWorkflow(true)); 214 - 216 + ->setName(pht('Edit Related Tasks...')) 217 + ->setIcon('fa-anchor') 218 + ->setSubmenu($task_submenu)); 215 219 216 220 $owner_phid = $task->getOwnerPHID(); 217 221 $author_phid = $task->getAuthorPHID();
+12 -1
src/view/layout/PhabricatorActionListView.php
··· 21 21 return $this; 22 22 } 23 23 24 + public function getID() { 25 + return $this->id; 26 + } 27 + 24 28 public function render() { 25 29 $viewer = $this->getViewer(); 26 30 ··· 44 48 45 49 require_celerity_resource('phabricator-action-list-view-css'); 46 50 51 + $items = array(); 52 + foreach ($actions as $action) { 53 + foreach ($action->getItems() as $item) { 54 + $items[] = $item; 55 + } 56 + } 57 + 47 58 return phutil_tag( 48 59 'ul', 49 60 array( 50 61 'class' => 'phabricator-action-list-view', 51 62 'id' => $this->id, 52 63 ), 53 - $actions); 64 + $items); 54 65 } 55 66 56 67 public function getDropdownMenuMetadata() {
+108 -2
src/view/layout/PhabricatorActionView.php
··· 14 14 private $metadata; 15 15 private $selected; 16 16 private $openInNewWindow; 17 + private $submenu = array(); 18 + private $hidden; 19 + private $depth; 20 + private $id; 17 21 18 22 public function setSelected($selected) { 19 23 $this->selected = $selected; ··· 95 99 return $this->openInNewWindow; 96 100 } 97 101 102 + public function getID() { 103 + if (!$this->id) { 104 + $this->id = celerity_generate_unique_node_id(); 105 + } 106 + return $this->id; 107 + } 108 + 109 + public function setSubmenu(array $submenu) { 110 + $this->submenu = $submenu; 111 + 112 + if (!$this->getHref()) { 113 + $this->setHref('#'); 114 + } 115 + 116 + return $this; 117 + } 118 + 119 + public function getItems($depth = 0) { 120 + $items = array(); 121 + 122 + $items[] = $this; 123 + foreach ($this->submenu as $action) { 124 + foreach ($action->getItems($depth + 1) as $item) { 125 + $item 126 + ->setHidden(true) 127 + ->setDepth($depth + 1); 128 + 129 + $items[] = $item; 130 + } 131 + } 132 + 133 + return $items; 134 + } 135 + 136 + public function setHidden($hidden) { 137 + $this->hidden = $hidden; 138 + return $this; 139 + } 140 + 141 + public function getHidden() { 142 + return $this->hidden; 143 + } 144 + 145 + public function setDepth($depth) { 146 + $this->depth = $depth; 147 + return $this; 148 + } 149 + 150 + public function getDepth() { 151 + return $this->depth; 152 + } 153 + 98 154 public function render() { 155 + $caret_id = celerity_generate_unique_node_id(); 99 156 100 157 $icon = null; 101 158 if ($this->icon) { ··· 153 210 $target = '_blank'; 154 211 } else { 155 212 $target = null; 213 + } 214 + 215 + if ($this->submenu) { 216 + $caret = javelin_tag( 217 + 'span', 218 + array( 219 + 'class' => 'caret-right', 220 + 'id' => $caret_id, 221 + ), 222 + ''); 223 + } else { 224 + $caret = null; 156 225 } 157 226 158 227 $item = javelin_tag( ··· 164 233 'sigil' => $sigils, 165 234 'meta' => $this->metadata, 166 235 ), 167 - array($icon, $this->name)); 236 + array($icon, $this->name, $caret)); 168 237 } 169 238 } else { 170 239 $item = phutil_tag( ··· 190 259 $classes[] = 'phabricator-action-view-selected'; 191 260 } 192 261 193 - return phutil_tag( 262 + if ($this->submenu) { 263 + $classes[] = 'phabricator-action-view-submenu'; 264 + } 265 + 266 + $style = array(); 267 + 268 + if ($this->hidden) { 269 + $style[] = 'display: none;'; 270 + } 271 + 272 + if ($this->depth) { 273 + $indent = ($this->depth * 16); 274 + $style[] = "margin-left: {$indent}px;"; 275 + } 276 + 277 + $sigil = null; 278 + $meta = null; 279 + 280 + if ($this->submenu) { 281 + Javelin::initBehavior('phui-submenu'); 282 + $sigil = 'phui-submenu'; 283 + 284 + $item_ids = array(); 285 + foreach ($this->submenu as $subitem) { 286 + $item_ids[] = $subitem->getID(); 287 + } 288 + 289 + $meta = array( 290 + 'itemIDs' => $item_ids, 291 + 'caretID' => $caret_id, 292 + ); 293 + } 294 + 295 + return javelin_tag( 194 296 'li', 195 297 array( 298 + 'id' => $this->getID(), 196 299 'class' => implode(' ', $classes), 300 + 'style' => implode(' ', $style), 301 + 'sigil' => $sigil, 302 + 'meta' => $meta, 197 303 ), 198 304 $item); 199 305 }
+12
src/view/phui/PHUIButtonView.php
··· 110 110 return $this; 111 111 } 112 112 113 + public function setDropdownMenuID($id) { 114 + Javelin::initBehavior('phui-dropdown-menu'); 115 + 116 + $this->addSigil('phui-dropdown-menu'); 117 + $this->setMetadata( 118 + array( 119 + 'menuID' => $id, 120 + )); 121 + 122 + return $this; 123 + } 124 + 113 125 protected function getTagAttributes() { 114 126 115 127 require_celerity_resource('phui-button-css');
+15 -3
src/view/phui/PHUIHeaderView.php
··· 24 24 private $badges = array(); 25 25 private $href; 26 26 private $actionList; 27 + private $actionListID; 27 28 28 29 public function setHeader($header) { 29 30 $this->header = $header; ··· 87 88 88 89 public function setActionList(PhabricatorActionListView $list) { 89 90 $this->actionList = $list; 91 + return $this; 92 + } 93 + 94 + public function setActionListID($action_list_id) { 95 + $this->actionListID = $action_list_id; 90 96 return $this; 91 97 } 92 98 ··· 189 195 190 196 protected function getTagContent() { 191 197 192 - if ($this->actionList) { 198 + if ($this->actionList || $this->actionListID) { 193 199 $action_button = id(new PHUIButtonView()) 194 200 ->setTag('a') 195 201 ->setText(pht('Actions')) 196 202 ->setHref('#') 197 203 ->setIcon('fa-bars') 198 - ->addClass('phui-mobile-menu') 199 - ->setDropdownMenu($this->actionList); 204 + ->addClass('phui-mobile-menu'); 205 + 206 + if ($this->actionList) { 207 + $action_button->setDropdownMenu($this->actionList); 208 + } else if ($this->actionListID) { 209 + $action_button->setDropdownMenuID($this->actionListID); 210 + } 211 + 200 212 $this->addActionLink($action_button); 201 213 } 202 214
+1 -3
src/view/phui/PHUITimelineEventView.php
··· 327 327 'sigil' => $sigil, 328 328 'aria-haspopup' => 'true', 329 329 'aria-expanded' => 'false', 330 - 'meta' => array( 331 - 'items' => hsprintf('%s', $action_list), 332 - ), 330 + 'meta' => $action_list->getDropdownMenuMetadata(), 333 331 ), 334 332 array( 335 333 $aural,
+1 -1
src/view/phui/PHUITwoColumnView.php
··· 114 114 $curtain = $this->getCurtain(); 115 115 if ($curtain) { 116 116 $action_list = $curtain->getActionList(); 117 - $this->header->setActionList($action_list); 117 + $this->header->setActionListID($action_list->getID()); 118 118 } 119 119 120 120 $header = phutil_tag_div(
+36
webroot/rsrc/css/phui/phui-button.css
··· 264 264 border-top-color: #000; 265 265 } 266 266 267 + .phabricator-action-view-submenu .caret-right { 268 + float: right; 269 + margin-top: 4px; 270 + margin-right: 6px; 271 + border-left-color: {$lightgreytext}; 272 + } 273 + 274 + .phabricator-action-view-submenu .caret { 275 + float: right; 276 + margin-top: 5px; 277 + margin-right: 4px; 278 + border-top: 7px solid {$lightgreytext}; 279 + } 280 + 281 + .phabricator-action-view-submenu.phui-submenu-open { 282 + background: {$greybackground}; 283 + } 284 + 285 + .phui-submenu-animate { 286 + animation: phui-submenu-summon 0.25s; 287 + } 288 + 289 + @keyframes phui-submenu-summon { 290 + 0% { 291 + color: {$lightgreytext}; 292 + margin-left: 0; 293 + transform: rotate(12deg); 294 + } 295 + 60% { 296 + margin-left: 24px; 297 + transform: rotate(-5deg); 298 + margin-top: 18px; 299 + } 300 + } 301 + 302 + 267 303 /* Icons */ 268 304 .button.has-icon { 269 305 position: relative;
+27 -2
webroot/rsrc/js/phui/behavior-phui-dropdown-menu.js
··· 16 16 17 17 e.kill(); 18 18 19 - var list = JX.$H(data.items).getFragment().firstChild; 19 + var list; 20 + var placeholder; 21 + if (data.items) { 22 + list = JX.$H(data.items).getFragment().firstChild; 23 + } else { 24 + list = JX.$(data.menuID); 25 + placeholder = JX.$N('span'); 26 + } 20 27 21 28 var icon = e.getNode('phui-dropdown-menu'); 22 29 data.menu = new JX.PHUIXDropdownMenu(icon); 23 - data.menu.setContent(list); 30 + 31 + data.menu.listen('open', function() { 32 + if (placeholder) { 33 + JX.DOM.replace(list, placeholder); 34 + } 35 + data.menu.setContent(list); 36 + }); 37 + 38 + data.menu.listen('close', function() { 39 + if (placeholder) { 40 + JX.DOM.replace(placeholder, list); 41 + } 42 + }); 43 + 24 44 data.menu.open(); 25 45 26 46 JX.DOM.listen(list, 'click', 'tag:a', function(e) { 27 47 if (!e.isNormalClick()) { 28 48 return; 29 49 } 50 + 51 + if (JX.Stratcom.pass()) { 52 + return; 53 + } 54 + 30 55 data.menu.close(); 31 56 }); 32 57 });
+44
webroot/rsrc/js/phui/behavior-phui-submenu.js
··· 1 + /** 2 + * @provides javelin-behavior-phui-submenu 3 + * @requires javelin-behavior 4 + * javelin-stratcom 5 + * javelin-dom 6 + */ 7 + 8 + JX.behavior('phui-submenu', function() { 9 + 10 + JX.Stratcom.listen('click', 'phui-submenu', function(e) { 11 + if (!e.isNormalClick()) { 12 + return; 13 + } 14 + 15 + var node = e.getNode('phui-submenu'); 16 + var data = e.getNodeData('phui-submenu'); 17 + 18 + e.kill(); 19 + 20 + data.open = !data.open; 21 + 22 + for (var ii = 0; ii < data.itemIDs.length; ii++) { 23 + var id = data.itemIDs[ii]; 24 + var item = JX.$(id); 25 + if (data.open) { 26 + JX.DOM.show(item); 27 + } else { 28 + JX.DOM.hide(item); 29 + } 30 + 31 + // Add a class so we can animate zany effects. 32 + JX.DOM.alterClass(item, 'phui-submenu-animate', data.open); 33 + } 34 + 35 + JX.DOM.alterClass(node, 'phui-submenu-open', data.open); 36 + 37 + // Toggle the caret from ">" to "V" when opening the menu, and back again 38 + // when closing it. 39 + var caret = JX.$(data.caretID); 40 + JX.DOM.alterClass(caret, 'caret', data.open); 41 + JX.DOM.alterClass(caret, 'caret-right', !data.open); 42 + }); 43 + 44 + });
+3 -1
webroot/rsrc/js/phuix/PHUIXDropdownMenu.js
··· 42 42 JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkey)); 43 43 }, 44 44 45 - events: ['open'], 45 + events: ['open', 'close'], 46 46 47 47 properties: { 48 48 width: null, ··· 82 82 } 83 83 this._open = false; 84 84 this._hide(); 85 + 86 + this.invoke('close'); 85 87 86 88 return this; 87 89 },