@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.

In ProfileMenu, put more structure between "stored/configured items" and "display items"

Summary:
Depends on D20356. Ref T13275. See also T12871 and T12949.

Currently, the whole "ProfileMenu" API operates around //stored// items. However, stored items are allowed to produce zero or more //display// items, and we sometimes want to highlight display item X but render stored item Y (as is the case with "Link" items pointing at `?filter=xyz` on Workboards).

For the most part, this either: doesn't work; or works by chance; or is kind of glued together with hope and prayer (as in D20353).

Put an actual structural layer in place between "stored/configured item" and "display item" that can link them together more clearly. Now:

- The list of `ItemConfiguration` objects (stored/configured items) is used to build an `ItemViewList`.
- This handles the selection/highlighting/default state, and knows which display items are related to which stored items.
- When we're all done figuring out what we're going to select and what we're going to highlight, it pops out an actual View which can build the HTML.

This requires API changes which are not included in this change, see next change.

This doesn't really do anything on its own, but builds toward a more satisfying fix for T12871. I'd hoped to avoid doing this for now, but wasn't able to get a patch I felt good about for T12871 built without fixing this first.

Test Plan: See next change.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13275

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

+564 -229
+4
src/__phutil_library_map__.php
··· 4056 4056 'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationTransactionQuery.php', 4057 4057 'PhabricatorProfileMenuItemIconSet' => 'applications/search/menuitem/PhabricatorProfileMenuItemIconSet.php', 4058 4058 'PhabricatorProfileMenuItemPHIDType' => 'applications/search/phidtype/PhabricatorProfileMenuItemPHIDType.php', 4059 + 'PhabricatorProfileMenuItemView' => 'applications/search/engine/PhabricatorProfileMenuItemView.php', 4060 + 'PhabricatorProfileMenuItemViewList' => 'applications/search/engine/PhabricatorProfileMenuItemViewList.php', 4059 4061 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', 4060 4062 'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', 4061 4063 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', ··· 10184 10186 'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 10185 10187 'PhabricatorProfileMenuItemIconSet' => 'PhabricatorIconSet', 10186 10188 'PhabricatorProfileMenuItemPHIDType' => 'PhabricatorPHIDType', 10189 + 'PhabricatorProfileMenuItemView' => 'Phobject', 10190 + 'PhabricatorProfileMenuItemViewList' => 'Phobject', 10187 10191 'PhabricatorProject' => array( 10188 10192 'PhabricatorProjectDAO', 10189 10193 'PhabricatorApplicationTransactionInterface',
+1 -1
src/applications/search/editor/PhabricatorProfileMenuEditEngine.php
··· 109 109 } 110 110 111 111 protected function getObjectEditTitleText($object) { 112 - $object->willBuildNavigationItems(array($object)); 112 + $object->willGetMenuItemViewList(array($object)); 113 113 return pht('Edit Menu Item: %s', $object->getDisplayName()); 114 114 } 115 115
+63 -222
src/applications/search/engine/PhabricatorProfileMenuEngine.php
··· 71 71 return $this->controller; 72 72 } 73 73 74 - private function setDefaultItem( 75 - PhabricatorProfileMenuItemConfiguration $default_item) { 76 - $this->defaultItem = $default_item; 77 - return $this; 78 - } 79 - 80 - public function getDefaultItem() { 81 - return $this->pickDefaultItem($this->getItems()); 82 - } 83 - 84 74 public function setShowNavigation($show) { 85 75 $this->showNavigation = $show; 86 76 return $this; ··· 150 140 $item_id = $request->getURIData('id'); 151 141 } 152 142 153 - $item_list = $this->getItems(); 143 + $view_list = $this->newProfileMenuItemViewList(); 154 144 155 - $selected_item = $this->pickSelectedItem( 156 - $item_list, 145 + $selected_item = $this->selectItem( 146 + $view_list, 157 147 $item_id, 158 148 $is_view); 159 149 ··· 183 173 break; 184 174 } 185 175 186 - $navigation = $this->buildNavigation($selected_item); 187 - 176 + $navigation = $view_list->newNavigationView(); 188 177 $crumbs = $controller->buildApplicationCrumbsForEditEngine(); 189 178 190 179 if (!$is_view) { ··· 288 277 if (!$this->isMenuEnginePinnable()) { 289 278 return new Aphront404Response(); 290 279 } 291 - $content = $this->buildItemDefaultContent( 292 - $selected_item, 293 - $item_list); 280 + $content = $this->buildItemDefaultContent($selected_item); 294 281 break; 295 282 case 'edit': 296 283 $content = $this->buildItemEditContent(); ··· 333 320 return $page; 334 321 } 335 322 336 - public function buildNavigation( 337 - PhabricatorProfileMenuItemConfiguration $selected_item = null) { 338 323 339 - if ($this->navigation) { 340 - return $this->navigation; 341 - } 342 - 343 - $nav = id(new AphrontSideNavFilterView()) 344 - ->setIsProfileMenu(true) 345 - ->setBaseURI(new PhutilURI($this->getItemURI(''))); 346 - 347 - $menu_items = $this->getItems(); 348 - 349 - $filtered_items = array(); 350 - foreach ($menu_items as $menu_item) { 351 - if ($menu_item->isDisabled()) { 352 - continue; 353 - } 354 - $filtered_items[] = $menu_item; 355 - } 356 - $filtered_groups = mgroup($filtered_items, 'getMenuItemKey'); 357 - foreach ($filtered_groups as $group) { 358 - $first_item = head($group); 359 - $first_item->willBuildNavigationItems($group); 360 - } 361 - 362 - $has_items = false; 363 - foreach ($menu_items as $menu_item) { 364 - if ($menu_item->isDisabled()) { 365 - continue; 366 - } 367 - 368 - $items = $menu_item->buildNavigationMenuItems(); 369 - foreach ($items as $item) { 370 - $this->validateNavigationMenuItem($item); 371 - } 372 - 373 - // If the item produced only a single item which does not otherwise 374 - // have a key, try to automatically assign it a reasonable key. This 375 - // makes selecting the correct item simpler. 376 - 377 - if (count($items) == 1) { 378 - $item = head($items); 379 - if ($item->getKey() === null) { 380 - $default_key = $menu_item->getDefaultMenuItemKey(); 381 - $item->setKey($default_key); 382 - } 383 - } 384 - 385 - foreach ($items as $item) { 386 - $nav->addMenuItem($item); 387 - $has_items = true; 388 - } 389 - } 390 - 391 - if (!$has_items) { 392 - // If the navigation menu has no items, add an empty label item to 393 - // force it to render something. 394 - $empty_item = id(new PHUIListItemView()) 395 - ->setType(PHUIListItemView::TYPE_LABEL); 396 - $nav->addMenuItem($empty_item); 397 - } 398 - 399 - $nav->selectFilter(null); 400 - 401 - $navigation_items = $nav->getMenu()->getItems(); 402 - $select_key = $this->pickHighlightedMenuItem( 403 - $navigation_items, 404 - $selected_item); 405 - $nav->selectFilter($select_key); 406 - 407 - $this->navigation = $nav; 408 - return $this->navigation; 409 - } 410 324 411 325 private function getItems() { 412 326 if ($this->items === null) { ··· 715 629 * 716 630 * @return bool True if items may be pinned as default items. 717 631 */ 718 - protected function isMenuEnginePinnable() { 632 + public function isMenuEnginePinnable() { 719 633 return !$this->isMenuEnginePersonalizable(); 720 634 } 721 635 ··· 779 693 $filtered_groups = mgroup($items, 'getMenuItemKey'); 780 694 foreach ($filtered_groups as $group) { 781 695 $first_item = head($group); 782 - $first_item->willBuildNavigationItems($group); 696 + $first_item->willGetMenuItemViewList($group); 783 697 } 784 698 785 699 // Users only need to be able to edit the object which this menu appears ··· 1153 1067 } 1154 1068 1155 1069 private function buildItemDefaultContent( 1156 - PhabricatorProfileMenuItemConfiguration $configuration, 1157 - array $items) { 1070 + PhabricatorProfileMenuItemConfiguration $configuration) { 1158 1071 1159 1072 $controller = $this->getController(); 1160 1073 $request = $controller->getRequest(); ··· 1220 1133 ->setIsTailItem(true); 1221 1134 } 1222 1135 1136 + public function getDefaultMenuItemConfiguration() { 1137 + $configs = $this->getItems(); 1138 + foreach ($configs as $config) { 1139 + if ($config->isDefault()) { 1140 + return $config; 1141 + } 1142 + } 1143 + 1144 + return null; 1145 + } 1146 + 1223 1147 public function adjustDefault($key) { 1224 1148 $controller = $this->getController(); 1225 1149 $request = $controller->getRequest(); ··· 1340 1264 pht('There are no menu items.')); 1341 1265 } 1342 1266 1343 - private function pickDefaultItem(array $items) { 1344 - // Remove all the items which can not be the default item. 1267 + 1268 + final public function newProfileMenuItemViewList() { 1269 + $items = $this->getItems(); 1270 + 1271 + // Throw away disabled items: they are not allowed to build any views for 1272 + // the menu. 1345 1273 foreach ($items as $key => $item) { 1346 - if (!$item->canMakeDefault()) { 1347 - unset($items[$key]); 1348 - continue; 1349 - } 1350 - 1351 1274 if ($item->isDisabled()) { 1352 1275 unset($items[$key]); 1353 1276 continue; 1354 1277 } 1355 1278 } 1356 1279 1357 - // If this engine supports pinning items and a valid item is pinned, 1358 - // pick that item as the default. 1359 - if ($this->isMenuEnginePinnable()) { 1360 - foreach ($items as $key => $item) { 1361 - if ($item->isDefault()) { 1362 - return $item; 1363 - } 1364 - } 1280 + // Give each item group a callback so it can load data it needs to render 1281 + // views. 1282 + $groups = mgroup($items, 'getMenuItemKey'); 1283 + foreach ($groups as $group) { 1284 + $item = head($group); 1285 + $item->willGetMenuItemViewList($group); 1365 1286 } 1366 1287 1367 - // If we have some other valid items, pick the first one as the default. 1368 - if ($items) { 1369 - return head($items); 1370 - } 1288 + $view_list = id(new PhabricatorProfileMenuItemViewList()) 1289 + ->setProfileMenuEngine($this); 1371 1290 1372 - return null; 1373 - } 1374 - 1375 - private function pickSelectedItem(array $items, $item_id, $is_view) { 1376 - if (strlen($item_id)) { 1377 - $item_id_int = (int)$item_id; 1378 - foreach ($items as $item) { 1379 - if ($item_id_int) { 1380 - if ((int)$item->getID() === $item_id_int) { 1381 - return $item; 1382 - } 1383 - } 1384 - 1385 - $builtin_key = $item->getBuiltinKey(); 1386 - if ($builtin_key === (string)$item_id) { 1387 - return $item; 1388 - } 1291 + foreach ($items as $item) { 1292 + $views = $item->getMenuItemViewList(); 1293 + foreach ($views as $view) { 1294 + $view_list->addItemView($view); 1389 1295 } 1390 - 1391 - // Nothing matches the selected item ID, so we don't have a valid 1392 - // selection. 1393 - return null; 1394 1296 } 1395 1297 1396 - if ($is_view) { 1397 - return $this->pickDefaultItem($items); 1398 - } 1399 - 1400 - return null; 1298 + return $view_list; 1401 1299 } 1402 1300 1403 - private function pickHighlightedMenuItem( 1404 - array $items, 1405 - PhabricatorProfileMenuItemConfiguration $selected_item = null) { 1406 - 1407 - assert_instances_of($items, 'PHUIListItemView'); 1408 - 1409 - $default_key = null; 1410 - if ($selected_item) { 1411 - $default_key = $selected_item->getDefaultMenuItemKey(); 1412 - } 1301 + private function selectItem( 1302 + PhabricatorProfileMenuItemViewList $view_list, 1303 + $item_id, 1304 + $want_default) { 1413 1305 1414 - $controller = $this->getController(); 1306 + // Figure out which view's content we're going to render. In most cases, 1307 + // the URI tells us. If we don't have an identifier in the URI, we'll 1308 + // render the default view instead if this is a workflow that falls back 1309 + // to default rendering. 1415 1310 1416 - // In some rare cases, when like building the "Favorites" menu on a 1417 - // 404 page, we may not have a controller. Just accept whatever default 1418 - // behavior we'd otherwise end up with. 1419 - if (!$controller) { 1420 - return $default_key; 1421 - } 1422 - 1423 - $request = $controller->getRequest(); 1424 - 1425 - // See T12949. If one of the menu items is a link to the same URI that 1426 - // the page was accessed with, we want to highlight that item. For example, 1427 - // this allows you to add links to a menu that apply filters to a 1428 - // workboard. 1429 - 1430 - $matches = array(); 1431 - foreach ($items as $item) { 1432 - $href = $item->getHref(); 1433 - if ($this->isMatchForRequestURI($request, $href)) { 1434 - $matches[] = $item; 1311 + $selected_view = null; 1312 + if (strlen($item_id)) { 1313 + $item_views = $view_list->getViewsWithItemIdentifier($item_id); 1314 + if ($item_views) { 1315 + $selected_view = head($item_views); 1435 1316 } 1436 - } 1437 - 1438 - foreach ($matches as $match) { 1439 - if ($match->getKey() === $default_key) { 1440 - return $default_key; 1317 + } else { 1318 + if ($want_default) { 1319 + $default_views = $view_list->getDefaultViews(); 1320 + if ($default_views) { 1321 + $selected_view = head($default_views); 1322 + } 1441 1323 } 1442 1324 } 1443 1325 1444 - if ($matches) { 1445 - return head($matches)->getKey(); 1326 + if ($selected_view) { 1327 + $view_list->setSelectedView($selected_view); 1328 + $selected_item = $selected_view->getMenuItemConfiguration(); 1329 + } else { 1330 + $selected_item = null; 1446 1331 } 1447 1332 1448 - return $default_key; 1333 + return $selected_item; 1449 1334 } 1450 1335 1451 - private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { 1452 - $request_uri = $request->getAbsoluteRequestURI(); 1453 - $item_uri = new PhutilURI($item_uri); 1454 - 1455 - // If the request URI and item URI don't have matching paths, they 1456 - // do not match. 1457 - if ($request_uri->getPath() !== $item_uri->getPath()) { 1458 - return false; 1459 - } 1460 - 1461 - // If the request URI and item URI don't have matching parameters, they 1462 - // also do not match. We're specifically trying to let "?filter=X" work 1463 - // on Workboards, among other use cases, so this is important. 1464 - $request_params = $request_uri->getQueryParamsAsPairList(); 1465 - $item_params = $item_uri->getQueryParamsAsPairList(); 1466 - if ($request_params !== $item_params) { 1467 - return false; 1468 - } 1469 - 1470 - // If the paths and parameters match, the item domain must be: empty; or 1471 - // match the request domain; or match the production domain. 1472 - 1473 - $request_domain = $request_uri->getDomain(); 1474 - 1475 - $production_uri = PhabricatorEnv::getProductionURI('/'); 1476 - $production_domain = id(new PhutilURI($production_uri)) 1477 - ->getDomain(); 1478 - 1479 - $allowed_domains = array( 1480 - '', 1481 - $request_domain, 1482 - $production_domain, 1483 - ); 1484 - $allowed_domains = array_fuse($allowed_domains); 1485 - 1486 - $item_domain = $item_uri->getDomain(); 1487 - $item_domain = (string)$item_domain; 1488 - 1489 - if (isset($allowed_domains[$item_domain])) { 1490 - return true; 1491 - } 1492 - 1493 - return false; 1494 - } 1495 1336 1496 1337 }
+212
src/applications/search/engine/PhabricatorProfileMenuItemView.php
··· 1 + <?php 2 + 3 + final class PhabricatorProfileMenuItemView 4 + extends Phobject { 5 + 6 + private $config; 7 + private $uri; 8 + private $name; 9 + private $icon; 10 + private $disabled; 11 + private $tooltip; 12 + private $actions = array(); 13 + private $counts = array(); 14 + private $images = array(); 15 + private $progressBars = array(); 16 + private $isExternalLink; 17 + private $specialType; 18 + 19 + public function setMenuItemConfiguration( 20 + PhabricatorProfileMenuItemConfiguration $config) { 21 + $this->config = $config; 22 + return $this; 23 + } 24 + 25 + public function getMenuItemConfiguration() { 26 + return $this->config; 27 + } 28 + 29 + public function setURI($uri) { 30 + $this->uri = $uri; 31 + return $this; 32 + } 33 + 34 + public function getURI() { 35 + return $this->uri; 36 + } 37 + 38 + public function setName($name) { 39 + $this->name = $name; 40 + return $this; 41 + } 42 + 43 + public function getName() { 44 + return $this->name; 45 + } 46 + 47 + public function setIcon($icon) { 48 + $this->icon = $icon; 49 + return $this; 50 + } 51 + 52 + public function getIcon() { 53 + return $this->icon; 54 + } 55 + 56 + public function setDisabled($disabled) { 57 + $this->disabled = $disabled; 58 + return $this; 59 + } 60 + 61 + public function getDisabled() { 62 + return $this->disabled; 63 + } 64 + 65 + public function setTooltip($tooltip) { 66 + $this->tooltip = $tooltip; 67 + return $this; 68 + } 69 + 70 + public function getTooltip() { 71 + return $this->tooltip; 72 + } 73 + 74 + public function newAction($uri) { 75 + $this->actions[] = $uri; 76 + return null; 77 + } 78 + 79 + public function newCount($count) { 80 + $this->counts[] = $count; 81 + return null; 82 + } 83 + 84 + public function newProfileImage($src) { 85 + $this->images[] = $src; 86 + return null; 87 + } 88 + 89 + public function newProgressBar($bar) { 90 + $this->progressBars[] = $bar; 91 + return null; 92 + } 93 + 94 + public function setIsExternalLink($is_external) { 95 + $this->isExternalLink = $is_external; 96 + return $this; 97 + } 98 + 99 + public function getIsExternalLink() { 100 + return $this->isExternalLink; 101 + } 102 + 103 + public function setIsLabel($is_label) { 104 + return $this->setSpecialType('label'); 105 + } 106 + 107 + public function getIsLabel() { 108 + return $this->isSpecialType('label'); 109 + } 110 + 111 + public function setIsDivider($is_divider) { 112 + return $this->setSpecialType('divider'); 113 + } 114 + 115 + public function getIsDivider() { 116 + return $this->isSpecialType('divider'); 117 + } 118 + 119 + private function setSpecialType($type) { 120 + $this->specialType = $type; 121 + return $this; 122 + } 123 + 124 + private function isSpecialType($type) { 125 + return ($this->specialType === $type); 126 + } 127 + 128 + public function newListItemView() { 129 + $view = id(new PHUIListItemView()) 130 + ->setName($this->getName()); 131 + 132 + $uri = $this->getURI(); 133 + if (strlen($uri)) { 134 + if ($this->getIsExternalLink()) { 135 + if (!PhabricatorEnv::isValidURIForLink($uri)) { 136 + $uri = '#'; 137 + } 138 + $view->setRel('noreferrer'); 139 + } 140 + 141 + $view->setHref($uri); 142 + } 143 + 144 + $icon = $this->getIcon(); 145 + if ($icon) { 146 + $view->setIcon($icon); 147 + } 148 + 149 + if ($this->getDisabled()) { 150 + $view->setDisabled(true); 151 + } 152 + 153 + if ($this->getIsLabel()) { 154 + $view->setType(PHUIListItemView::TYPE_LABEL); 155 + } 156 + 157 + if ($this->getIsDivider()) { 158 + $view 159 + ->setType(PHUIListItemView::TYPE_DIVIDER) 160 + ->addClass('phui-divider'); 161 + } 162 + 163 + if ($this->images) { 164 + require_celerity_resource('people-picture-menu-item-css'); 165 + foreach ($this->images as $image_src) { 166 + $classes = array(); 167 + $classes[] = 'people-menu-image'; 168 + 169 + if ($this->getDisabled()) { 170 + $classes[] = 'phui-image-disabled'; 171 + } 172 + 173 + $image = phutil_tag( 174 + 'img', 175 + array( 176 + 'src' => $image_src, 177 + 'class' => implode(' ', $classes), 178 + )); 179 + 180 + $image = phutil_tag( 181 + 'div', 182 + array( 183 + 'class' => 'people-menu-image-container', 184 + ), 185 + $image); 186 + 187 + $view->appendChild($image); 188 + } 189 + } 190 + 191 + foreach ($this->counts as $count) { 192 + $view->appendChild( 193 + phutil_tag( 194 + 'span', 195 + array( 196 + 'class' => 'phui-list-item-count', 197 + ), 198 + $count)); 199 + } 200 + 201 + foreach ($this->actions as $action) { 202 + $view->setActionIcon('fa-pencil', $action); 203 + } 204 + 205 + foreach ($this->progressBars as $bar) { 206 + $view->appendChild($bar); 207 + } 208 + 209 + return $view; 210 + } 211 + 212 + }
+278
src/applications/search/engine/PhabricatorProfileMenuItemViewList.php
··· 1 + <?php 2 + 3 + final class PhabricatorProfileMenuItemViewList 4 + extends Phobject { 5 + 6 + private $engine; 7 + private $views = array(); 8 + private $selectedView; 9 + 10 + public function setProfileMenuEngine(PhabricatorProfileMenuEngine $engine) { 11 + $this->engine = $engine; 12 + return $this; 13 + } 14 + 15 + public function getProfileMenuEngine() { 16 + return $this->engine; 17 + } 18 + 19 + public function addItemView(PhabricatorProfileMenuItemView $view) { 20 + $this->views[] = $view; 21 + return $this; 22 + } 23 + 24 + public function getItemViews() { 25 + return $this->views; 26 + } 27 + 28 + public function setSelectedView(PhabricatorProfileMenuItemView $view) { 29 + $found = false; 30 + foreach ($this->getItemViews() as $item_view) { 31 + if ($view === $item_view) { 32 + $found = true; 33 + break; 34 + } 35 + } 36 + 37 + if (!$found) { 38 + throw new Exception( 39 + pht( 40 + 'Provided view is not one of the views in the list: you can only '. 41 + 'select a view which appears in the list.')); 42 + } 43 + 44 + $this->selectedView = $view; 45 + 46 + return $this; 47 + } 48 + 49 + public function setSelectedViewWithItemIdentifier($identifier) { 50 + $views = $this->getViewsWithItemIdentifier($identifier); 51 + 52 + if (!$views) { 53 + throw new Exception( 54 + pht( 55 + 'No views match identifier "%s"!', 56 + $identifier)); 57 + } 58 + 59 + return $this->setSelectedView(head($views)); 60 + } 61 + 62 + public function getViewsWithItemIdentifier($identifier) { 63 + $views = $this->getItemViews(); 64 + 65 + if (!strlen($identifier)) { 66 + return array(); 67 + } 68 + 69 + if (ctype_digit($identifier)) { 70 + $identifier_int = (int)$identifier; 71 + } else { 72 + $identifier_int = null; 73 + } 74 + 75 + $identifier_str = (string)$identifier; 76 + 77 + $results = array(); 78 + foreach ($views as $view) { 79 + $config = $view->getMenuItemConfiguration(); 80 + 81 + if ($identifier_int !== null) { 82 + $config_id = (int)$config->getID(); 83 + if ($config_id === $identifier_int) { 84 + $results[] = $view; 85 + continue; 86 + } 87 + } 88 + 89 + if ($config->getBuiltinKey() === $identifier_str) { 90 + $results[] = $view; 91 + continue; 92 + } 93 + } 94 + 95 + return $results; 96 + } 97 + 98 + public function getDefaultViews() { 99 + $engine = $this->getProfileMenuEngine(); 100 + $can_pin = $engine->isMenuEnginePinnable(); 101 + 102 + $views = $this->getItemViews(); 103 + 104 + // Remove all the views which were built by an item that can not be the 105 + // default item. 106 + foreach ($views as $key => $view) { 107 + $config = $view->getMenuItemConfiguration(); 108 + 109 + if (!$config->canMakeDefault()) { 110 + unset($views[$key]); 111 + continue; 112 + } 113 + } 114 + 115 + // If this engine supports pinning items and we have candidate views from a 116 + // valid pinned item, they are the default views. 117 + if ($can_pin) { 118 + $pinned = array(); 119 + 120 + foreach ($views as $key => $view) { 121 + $config = $view->getMenuItemConfiguration(); 122 + 123 + if ($config->isDefault()) { 124 + $pinned[] = $view; 125 + continue; 126 + } 127 + } 128 + 129 + if ($pinned) { 130 + return $pinned; 131 + } 132 + } 133 + 134 + // Return whatever remains that's still valid. 135 + return $views; 136 + } 137 + 138 + public function newNavigationView() { 139 + $engine = $this->getProfileMenuEngine(); 140 + 141 + $base_uri = $engine->getItemURI(''); 142 + $base_uri = new PhutilURI($base_uri); 143 + 144 + $navigation = id(new AphrontSideNavFilterView()) 145 + ->setIsProfileMenu(true) 146 + ->setBaseURI($base_uri); 147 + 148 + $views = $this->getItemViews(); 149 + $selected_item = null; 150 + $item_key = 0; 151 + $items = array(); 152 + foreach ($views as $view) { 153 + $list_item = $view->newListItemView(); 154 + 155 + // Assign unique keys to the list items. These keys are purely internal. 156 + $list_item->setKey(sprintf('item(%d)', $item_key++)); 157 + 158 + if ($this->selectedView) { 159 + if ($this->selectedView === $view) { 160 + $selected_item = $list_item; 161 + } 162 + } 163 + 164 + $navigation->addMenuItem($list_item); 165 + $items[] = $list_item; 166 + } 167 + 168 + if (!$views) { 169 + // If the navigation menu has no items, add an empty label item to 170 + // force it to render something. 171 + $empty_item = id(new PHUIListItemView()) 172 + ->setType(PHUIListItemView::TYPE_LABEL); 173 + $navigation->addMenuItem($empty_item); 174 + } 175 + 176 + $highlight_key = $this->getHighlightedItemKey( 177 + $items, 178 + $selected_item); 179 + $navigation->selectFilter($highlight_key); 180 + 181 + return $navigation; 182 + } 183 + 184 + private function getHighlightedItemKey( 185 + array $items, 186 + PHUIListItemView $selected_item = null) { 187 + 188 + assert_instances_of($items, 'PHUIListItemView'); 189 + 190 + $default_key = null; 191 + if ($selected_item) { 192 + $default_key = $selected_item->getKey(); 193 + } 194 + 195 + $engine = $this->getProfileMenuEngine(); 196 + $controller = $engine->getController(); 197 + 198 + // In some rare cases, when like building the "Favorites" menu on a 199 + // 404 page, we may not have a controller. Just accept whatever default 200 + // behavior we'd otherwise end up with. 201 + if (!$controller) { 202 + return $default_key; 203 + } 204 + 205 + $request = $controller->getRequest(); 206 + 207 + // See T12949. If one of the menu items is a link to the same URI that 208 + // the page was accessed with, we want to highlight that item. For example, 209 + // this allows you to add links to a menu that apply filters to a 210 + // workboard. 211 + 212 + $matches = array(); 213 + foreach ($items as $item) { 214 + $href = $item->getHref(); 215 + if ($this->isMatchForRequestURI($request, $href)) { 216 + $matches[] = $item; 217 + } 218 + } 219 + 220 + foreach ($matches as $match) { 221 + if ($match->getKey() === $default_key) { 222 + return $default_key; 223 + } 224 + } 225 + 226 + if ($matches) { 227 + return head($matches)->getKey(); 228 + } 229 + 230 + return $default_key; 231 + } 232 + 233 + private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { 234 + $request_uri = $request->getAbsoluteRequestURI(); 235 + $item_uri = new PhutilURI($item_uri); 236 + 237 + // If the request URI and item URI don't have matching paths, they 238 + // do not match. 239 + if ($request_uri->getPath() !== $item_uri->getPath()) { 240 + return false; 241 + } 242 + 243 + // If the request URI and item URI don't have matching parameters, they 244 + // also do not match. We're specifically trying to let "?filter=X" work 245 + // on Workboards, among other use cases, so this is important. 246 + $request_params = $request_uri->getQueryParamsAsPairList(); 247 + $item_params = $item_uri->getQueryParamsAsPairList(); 248 + if ($request_params !== $item_params) { 249 + return false; 250 + } 251 + 252 + // If the paths and parameters match, the item domain must be: empty; or 253 + // match the request domain; or match the production domain. 254 + 255 + $request_domain = $request_uri->getDomain(); 256 + 257 + $production_uri = PhabricatorEnv::getProductionURI('/'); 258 + $production_domain = id(new PhutilURI($production_uri)) 259 + ->getDomain(); 260 + 261 + $allowed_domains = array( 262 + '', 263 + $request_domain, 264 + $production_domain, 265 + ); 266 + $allowed_domains = array_fuse($allowed_domains); 267 + 268 + $item_domain = $item_uri->getDomain(); 269 + $item_domain = (string)$item_domain; 270 + 271 + if (isset($allowed_domains[$item_domain])) { 272 + return true; 273 + } 274 + 275 + return false; 276 + } 277 + 278 + }
+6 -6
src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php
··· 100 100 return idx($this->menuItemProperties, $key, $default); 101 101 } 102 102 103 - public function buildNavigationMenuItems() { 104 - return $this->getMenuItem()->buildNavigationMenuItems($this); 105 - } 106 - 107 103 public function getMenuItemTypeName() { 108 104 return $this->getMenuItem()->getMenuItemTypeName(); 109 105 } ··· 124 120 return $this->getMenuItem()->shouldEnableForObject($object); 125 121 } 126 122 127 - public function willBuildNavigationItems(array $items) { 128 - return $this->getMenuItem()->willBuildNavigationItems($items); 123 + public function willGetMenuItemViewList(array $items) { 124 + return $this->getMenuItem()->willGetMenuItemViewList($items); 125 + } 126 + 127 + public function getMenuItemViewList() { 128 + return $this->getMenuItem()->getMenuItemViewList($this); 129 129 } 130 130 131 131 public function validateTransactions(array $map) {