@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
at upstream/main 1371 lines 39 kB view raw
1<?php 2 3abstract class PhabricatorProfileMenuEngine extends Phobject { 4 5 private $viewer; 6 private $profileObject; 7 private $customPHID; 8 private $items; 9 private $controller; 10 private $navigation; 11 private $editMode; 12 private $pageClasses = array(); 13 private $showContentCrumbs = true; 14 15 const ITEM_CUSTOM_DIVIDER = 'engine.divider'; 16 const ITEM_MANAGE = 'item.configure'; 17 18 const MODE_COMBINED = 'combined'; 19 const MODE_GLOBAL = 'global'; 20 const MODE_CUSTOM = 'custom'; 21 22 public function setViewer(PhabricatorUser $viewer) { 23 $this->viewer = $viewer; 24 return $this; 25 } 26 27 public function getViewer() { 28 return $this->viewer; 29 } 30 31 /** 32 * @param object $profile_object A PhabricatorApplication subclass 33 */ 34 public function setProfileObject($profile_object) { 35 $this->profileObject = $profile_object; 36 return $this; 37 } 38 39 /** 40 * @return object A PhabricatorApplication subclass 41 */ 42 public function getProfileObject() { 43 return $this->profileObject; 44 } 45 46 /** 47 * @param $custom_phid A User PHID 48 */ 49 public function setCustomPHID($custom_phid) { 50 $this->customPHID = $custom_phid; 51 return $this; 52 } 53 54 /** 55 * @return string|null A User PHID, or null 56 */ 57 public function getCustomPHID() { 58 return $this->customPHID; 59 } 60 61 /** 62 * @return string|null A User PHID, or null 63 */ 64 private function getEditModeCustomPHID() { 65 $mode = $this->getEditMode(); 66 67 switch ($mode) { 68 case self::MODE_CUSTOM: 69 $custom_phid = $this->getCustomPHID(); 70 break; 71 case self::MODE_GLOBAL: 72 default: 73 $custom_phid = null; 74 break; 75 } 76 77 return $custom_phid; 78 } 79 80 public function setController(PhabricatorController $controller) { 81 $this->controller = $controller; 82 return $this; 83 } 84 85 public function getController() { 86 return $this->controller; 87 } 88 89 public function addContentPageClass($class) { 90 $this->pageClasses[] = $class; 91 return $this; 92 } 93 94 public function setShowContentCrumbs($show_content_crumbs) { 95 $this->showContentCrumbs = $show_content_crumbs; 96 return $this; 97 } 98 99 public function getShowContentCrumbs() { 100 return $this->showContentCrumbs; 101 } 102 103 abstract public function getItemURI($path); 104 abstract protected function isMenuEngineConfigurable(); 105 106 /** 107 * @return array of PhabricatorProfileMenuItemConfiguration objects 108 */ 109 abstract protected function getBuiltinProfileItems($object); 110 111 protected function getBuiltinCustomProfileItems( 112 $object, 113 $custom_phid) { 114 return array(); 115 } 116 117 protected function getEditMode() { 118 return $this->editMode; 119 } 120 121 public function buildResponse() { 122 $controller = $this->getController(); 123 124 $viewer = $controller->getViewer(); 125 $this->setViewer($viewer); 126 127 $request = $controller->getRequest(); 128 129 $item_action = $request->getURIData('itemAction'); 130 if (!$item_action) { 131 $item_action = 'view'; 132 } 133 134 $is_view = ($item_action == 'view'); 135 136 // If the engine is not configurable, don't respond to any of the editing 137 // or configuration routes. 138 if (!$this->isMenuEngineConfigurable()) { 139 if (!$is_view) { 140 return new Aphront404Response(); 141 } 142 } 143 144 $item_id = $request->getURIData('itemID'); 145 146 // If we miss on the MenuEngine route, try the EditEngine route. This will 147 // be populated while editing items. 148 if (!$item_id) { 149 $item_id = $request->getURIData('id'); 150 } 151 152 $view_list = $this->newProfileMenuItemViewList(); 153 154 if ($is_view) { 155 $selected_item = $this->selectViewItem($view_list, $item_id); 156 } else { 157 if (!phutil_nonempty_scalar($item_id)) { 158 $item_id = self::ITEM_MANAGE; 159 } 160 $selected_item = $this->selectEditItem($view_list, $item_id); 161 } 162 163 switch ($item_action) { 164 case 'view': 165 // If we were not able to select an item, we're still going to render 166 // a page state. For example, this happens when you create a new 167 // portal for the first time. 168 break; 169 case 'info': 170 case 'hide': 171 case 'default': 172 case 'builtin': 173 if (!$selected_item) { 174 return new Aphront404Response(); 175 } 176 break; 177 case 'edit': 178 if (!$request->getURIData('id')) { 179 // If we continue along the "edit" pathway without an ID, we hit an 180 // unrelated exception because we can not build a new menu item out 181 // of thin air. For menus, new items are created via the "new" 182 // action. Just catch this case and 404 early since there's currently 183 // no clean way to make EditEngine aware of this. 184 return new Aphront404Response(); 185 } 186 break; 187 } 188 189 $navigation = $view_list->newNavigationView(); 190 $crumbs = $controller->buildApplicationCrumbsForEditEngine(); 191 192 if (!$is_view) { 193 $edit_mode = null; 194 195 if ($selected_item) { 196 if ($selected_item->getBuiltinKey() !== self::ITEM_MANAGE) { 197 if ($selected_item->getCustomPHID()) { 198 $edit_mode = 'custom'; 199 } else { 200 $edit_mode = 'global'; 201 } 202 } 203 } 204 205 if ($edit_mode === null) { 206 $edit_mode = $request->getURIData('itemEditMode'); 207 } 208 209 $available_modes = $this->getViewerEditModes(); 210 if ($available_modes) { 211 $available_modes = array_fuse($available_modes); 212 if ($edit_mode !== null && isset($available_modes[$edit_mode])) { 213 $this->editMode = $edit_mode; 214 } else { 215 if ($item_action != 'configure') { 216 return new Aphront404Response(); 217 } 218 } 219 } 220 $page_title = pht('Configure Menu'); 221 } else { 222 if ($selected_item) { 223 $page_title = $selected_item->getDisplayName(); 224 } else { 225 $page_title = pht('Empty'); 226 } 227 } 228 229 switch ($item_action) { 230 case 'view': 231 if ($selected_item) { 232 try { 233 $content = $this->buildItemViewContent($selected_item); 234 } catch (Exception $ex) { 235 $content = id(new PHUIInfoView()) 236 ->setTitle(pht('Unable to Render Dashboard')) 237 ->setErrors(array($ex->getMessage())); 238 } 239 240 $crumbs->addTextCrumb($selected_item->getDisplayName()); 241 } else { 242 $content = $this->newNoContentView($this->getItems()); 243 } 244 245 if (!$content) { 246 $content = $this->newEmptyView( 247 pht('Empty'), 248 pht('There is nothing here.')); 249 } 250 break; 251 case 'configure': 252 $mode = $this->getEditMode(); 253 if (!$mode) { 254 $crumbs->addTextCrumb(pht('Configure Menu')); 255 $content = $this->buildMenuEditModeContent(); 256 } else { 257 if (count($available_modes) > 1) { 258 $crumbs->addTextCrumb( 259 pht('Configure Menu'), 260 $this->getItemURI('configure/')); 261 262 switch ($mode) { 263 case self::MODE_CUSTOM: 264 $crumbs->addTextCrumb(pht('Personal')); 265 break; 266 case self::MODE_GLOBAL: 267 $crumbs->addTextCrumb(pht('Global')); 268 break; 269 } 270 } else { 271 $crumbs->addTextCrumb(pht('Configure Menu')); 272 } 273 $edit_list = $this->loadItems($mode); 274 $content = $this->buildItemConfigureContent($edit_list); 275 } 276 break; 277 case 'reorder': 278 $mode = $this->getEditMode(); 279 $edit_list = $this->loadItems($mode); 280 $content = $this->buildItemReorderContent($edit_list); 281 break; 282 case 'new': 283 $item_key = $request->getURIData('itemKey'); 284 $mode = $this->getEditMode(); 285 $content = $this->buildItemNewContent($item_key, $mode); 286 break; 287 case 'builtin': 288 $content = $this->buildItemBuiltinContent($selected_item); 289 break; 290 case 'hide': 291 $content = $this->buildItemHideContent($selected_item); 292 break; 293 case 'default': 294 if (!$this->isMenuEnginePinnable()) { 295 return new Aphront404Response(); 296 } 297 $content = $this->buildItemDefaultContent($selected_item); 298 break; 299 case 'edit': 300 $content = $this->buildItemEditContent(); 301 break; 302 default: 303 throw new Exception( 304 pht( 305 'Unsupported item action "%s".', 306 $item_action)); 307 } 308 309 if ($content instanceof AphrontResponse) { 310 return $content; 311 } 312 313 if ($content instanceof AphrontResponseProducerInterface) { 314 return $content; 315 } 316 317 $crumbs->setBorder(true); 318 319 $page = $controller->newPage() 320 ->setTitle($page_title) 321 ->appendChild($content); 322 323 if (!$is_view || $this->getShowContentCrumbs()) { 324 $page->setCrumbs($crumbs); 325 } 326 327 $page->setNavigation($navigation); 328 329 if ($is_view) { 330 foreach ($this->pageClasses as $class) { 331 $page->addClass($class); 332 } 333 } 334 335 return $page; 336 } 337 338 private function getItems() { 339 if ($this->items === null) { 340 $this->items = $this->loadItems(self::MODE_COMBINED); 341 } 342 343 return $this->items; 344 } 345 346 private function loadItems($mode) { 347 $viewer = $this->getViewer(); 348 $object = $this->getProfileObject(); 349 350 $items = $this->loadBuiltinProfileItems($mode); 351 352 $query = id(new PhabricatorProfileMenuItemConfigurationQuery()) 353 ->setViewer($viewer) 354 ->withProfilePHIDs(array($object->getPHID())); 355 356 switch ($mode) { 357 case self::MODE_GLOBAL: 358 $query->withCustomPHIDs(array(), true); 359 break; 360 case self::MODE_CUSTOM: 361 $query->withCustomPHIDs(array($this->getCustomPHID()), false); 362 break; 363 case self::MODE_COMBINED: 364 $query->withCustomPHIDs(array($this->getCustomPHID()), true); 365 break; 366 } 367 368 $stored_items = $query->execute(); 369 370 foreach ($stored_items as $stored_item) { 371 $impl = $stored_item->getMenuItem(); 372 $impl->setViewer($viewer); 373 $impl->setEngine($this); 374 } 375 376 // Merge the stored items into the builtin items. If a builtin item has 377 // a stored version, replace the defaults with the stored changes. 378 foreach ($stored_items as $stored_item) { 379 if (!$stored_item->shouldEnableForObject($object)) { 380 continue; 381 } 382 383 $builtin_key = $stored_item->getBuiltinKey(); 384 if ($builtin_key !== null) { 385 // If this builtin actually exists, replace the builtin with the 386 // stored configuration. Otherwise, we're just going to drop the 387 // stored config: it corresponds to an out-of-date or uninstalled 388 // item. 389 if (isset($items[$builtin_key])) { 390 $builtin_item = $items[$builtin_key]; 391 392 // Copy runtime properties from the builtin item to the stored item. 393 $stored_item->setIsHeadItem($builtin_item->getIsHeadItem()); 394 $stored_item->setIsTailItem($builtin_item->getIsTailItem()); 395 396 $items[$builtin_key] = $stored_item; 397 } else { 398 continue; 399 } 400 } else { 401 $items[] = $stored_item; 402 } 403 } 404 405 return $this->arrangeItems($items, $mode); 406 } 407 408 private function loadBuiltinProfileItems($mode) { 409 $object = $this->getProfileObject(); 410 $builtins = array(); 411 412 switch ($mode) { 413 case self::MODE_GLOBAL: 414 $builtins = $this->getBuiltinProfileItems($object); 415 break; 416 case self::MODE_CUSTOM: 417 $builtins = $this->getBuiltinCustomProfileItems( 418 $object, 419 $this->getCustomPHID()); 420 break; 421 case self::MODE_COMBINED: 422 $builtins[] = $this->getBuiltinCustomProfileItems( 423 $object, 424 $this->getCustomPHID()); 425 $builtins[] = $this->getBuiltinProfileItems($object); 426 $builtins = array_mergev($builtins); 427 break; 428 } 429 430 $items = PhabricatorProfileMenuItem::getAllMenuItems(); 431 $viewer = $this->getViewer(); 432 433 $order = 1; 434 $map = array(); 435 foreach ($builtins as $builtin) { 436 $builtin_key = $builtin->getBuiltinKey(); 437 438 if (!$builtin_key) { 439 throw new Exception( 440 pht( 441 'Object produced a builtin item with no builtin item key! '. 442 'Builtin items must have a unique key.')); 443 } 444 445 if (isset($map[$builtin_key])) { 446 throw new Exception( 447 pht( 448 'Object produced two items with the same builtin key ("%s"). '. 449 'Each item must have a unique builtin key.', 450 $builtin_key)); 451 } 452 453 $item_key = $builtin->getMenuItemKey(); 454 455 $item = idx($items, $item_key); 456 if (!$item) { 457 throw new Exception( 458 pht( 459 'Builtin item ("%s") specifies a bad item key ("%s"); there '. 460 'is no corresponding item implementation available.', 461 $builtin_key, 462 $item_key)); 463 } 464 465 $item = clone $item; 466 $item->setViewer($viewer); 467 $item->setEngine($this); 468 469 $builtin 470 ->setProfilePHID($object->getPHID()) 471 ->attachMenuItem($item) 472 ->attachProfileObject($object) 473 ->setMenuItemOrder($order); 474 475 if (!$builtin->shouldEnableForObject($object)) { 476 continue; 477 } 478 479 $map[$builtin_key] = $builtin; 480 481 $order++; 482 } 483 484 return $map; 485 } 486 487 public function getConfigureURI() { 488 $mode = $this->getEditMode(); 489 490 switch ($mode) { 491 case self::MODE_CUSTOM: 492 return $this->getItemURI('configure/custom/'); 493 case self::MODE_GLOBAL: 494 return $this->getItemURI('configure/global/'); 495 } 496 497 return $this->getItemURI('configure/'); 498 } 499 500 private function buildItemReorderContent(array $items) { 501 $viewer = $this->getViewer(); 502 $object = $this->getProfileObject(); 503 504 // If you're reordering global items, you need to be able to edit the 505 // object the menu appears on. If you're reordering custom items, you only 506 // need to be able to edit the custom object. Currently, the custom object 507 // is always the viewing user's own user object. 508 $custom_phid = $this->getEditModeCustomPHID(); 509 510 if (!$custom_phid) { 511 PhabricatorPolicyFilter::requireCapability( 512 $viewer, 513 $object, 514 PhabricatorPolicyCapability::CAN_EDIT); 515 } else { 516 $policy_object = id(new PhabricatorObjectQuery()) 517 ->setViewer($viewer) 518 ->withPHIDs(array($custom_phid)) 519 ->executeOne(); 520 521 if (!$policy_object) { 522 throw new Exception( 523 pht( 524 'Failed to load custom PHID "%s"!', 525 $custom_phid)); 526 } 527 528 PhabricatorPolicyFilter::requireCapability( 529 $viewer, 530 $policy_object, 531 PhabricatorPolicyCapability::CAN_EDIT); 532 } 533 534 $controller = $this->getController(); 535 $request = $controller->getRequest(); 536 537 $request->validateCSRF(); 538 539 $order = $request->getStrList('order'); 540 541 $by_builtin = array(); 542 $by_id = array(); 543 544 foreach ($items as $key => $item) { 545 $id = $item->getID(); 546 if ($id) { 547 $by_id[$id] = $key; 548 continue; 549 } 550 551 $builtin_key = $item->getBuiltinKey(); 552 if ($builtin_key) { 553 $by_builtin[$builtin_key] = $key; 554 continue; 555 } 556 } 557 558 $key_order = array(); 559 foreach ($order as $order_item) { 560 if (isset($by_id[$order_item])) { 561 $key_order[] = $by_id[$order_item]; 562 continue; 563 } 564 if (isset($by_builtin[$order_item])) { 565 $key_order[] = $by_builtin[$order_item]; 566 continue; 567 } 568 } 569 570 $items = array_select_keys($items, $key_order) + $items; 571 572 $type_order = 573 PhabricatorProfileMenuItemConfigurationTransaction::TYPE_ORDER; 574 575 $order = 1; 576 foreach ($items as $item) { 577 $xactions = array(); 578 579 $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) 580 ->setTransactionType($type_order) 581 ->setNewValue($order); 582 583 $editor = id(new PhabricatorProfileMenuEditor()) 584 ->setContentSourceFromRequest($request) 585 ->setActor($viewer) 586 ->setContinueOnMissingFields(true) 587 ->setContinueOnNoEffect(true) 588 ->applyTransactions($item, $xactions); 589 590 $order++; 591 } 592 593 return id(new AphrontRedirectResponse()) 594 ->setURI($this->getConfigureURI()); 595 } 596 597 protected function buildItemViewContent( 598 PhabricatorProfileMenuItemConfiguration $item) { 599 return $item->newPageContent(); 600 } 601 602 private function getViewerEditModes() { 603 $modes = array(); 604 605 $viewer = $this->getViewer(); 606 607 if ($viewer->isLoggedIn() && $this->isMenuEnginePersonalizable()) { 608 $modes[] = self::MODE_CUSTOM; 609 } 610 611 $object = $this->getProfileObject(); 612 $can_edit = PhabricatorPolicyFilter::hasCapability( 613 $viewer, 614 $object, 615 PhabricatorPolicyCapability::CAN_EDIT); 616 617 if ($can_edit) { 618 $modes[] = self::MODE_GLOBAL; 619 } 620 621 return $modes; 622 } 623 624 protected function isMenuEnginePersonalizable() { 625 return true; 626 } 627 628 /** 629 * Does this engine support pinning items? 630 * 631 * Personalizable menus disable pinning by default since it creates a number 632 * of weird edge cases without providing many benefits for current menus. 633 * 634 * @return bool True if items may be pinned as default items. 635 */ 636 public function isMenuEnginePinnable() { 637 return !$this->isMenuEnginePersonalizable(); 638 } 639 640 private function buildMenuEditModeContent() { 641 $viewer = $this->getViewer(); 642 643 $modes = $this->getViewerEditModes(); 644 if (!$modes) { 645 return new Aphront404Response(); 646 } 647 648 if (count($modes) == 1) { 649 $mode = head($modes); 650 return id(new AphrontRedirectResponse()) 651 ->setURI($this->getItemURI("configure/{$mode}/")); 652 } 653 654 $menu = id(new PHUIObjectItemListView()) 655 ->setUser($viewer); 656 657 $modes = array_fuse($modes); 658 659 if (isset($modes['custom'])) { 660 $menu->addItem( 661 id(new PHUIObjectItemView()) 662 ->setHeader(pht('Personal Menu Items')) 663 ->setHref($this->getItemURI('configure/custom/')) 664 ->setImageURI($viewer->getProfileImageURI()) 665 ->addAttribute(pht('Edit the menu for your personal account.'))); 666 } 667 668 if (isset($modes['global'])) { 669 $icon = id(new PHUIIconView()) 670 ->setIcon('fa-globe') 671 ->setBackground('bg-blue'); 672 673 $menu->addItem( 674 id(new PHUIObjectItemView()) 675 ->setHeader(pht('Global Menu Items')) 676 ->setHref($this->getItemURI('configure/global/')) 677 ->setImageIcon($icon) 678 ->addAttribute(pht('Edit the global default menu for all users.'))); 679 } 680 681 $box = id(new PHUIObjectBoxView()) 682 ->setObjectList($menu); 683 684 $header = id(new PHUIHeaderView()) 685 ->setHeader(pht('Manage Menu')) 686 ->setHeaderIcon('fa-list'); 687 688 return id(new PHUITwoColumnView()) 689 ->setHeader($header) 690 ->setFooter($box); 691 } 692 693 private function buildItemConfigureContent(array $items) { 694 $viewer = $this->getViewer(); 695 $object = $this->getProfileObject(); 696 697 $filtered_groups = mgroup($items, 'getMenuItemKey'); 698 foreach ($filtered_groups as $group) { 699 $first_item = head($group); 700 $first_item->willGetMenuItemViewList($group); 701 } 702 703 // Users only need to be able to edit the object which this menu appears 704 // on if they're editing global menu items. For example, users do not need 705 // to be able to edit the Favorites application to add new items to the 706 // Favorites menu. 707 if (!$this->getCustomPHID()) { 708 PhabricatorPolicyFilter::requireCapability( 709 $viewer, 710 $object, 711 PhabricatorPolicyCapability::CAN_EDIT); 712 } 713 714 $list_id = celerity_generate_unique_node_id(); 715 716 $mode = $this->getEditMode(); 717 718 Javelin::initBehavior( 719 'reorder-profile-menu-items', 720 array( 721 'listID' => $list_id, 722 'orderURI' => $this->getItemURI("reorder/{$mode}/"), 723 )); 724 725 $list = id(new PHUIObjectItemListView()) 726 ->setID($list_id) 727 ->setNoDataString(pht('This menu currently has no items.')); 728 729 $any_draggable = false; 730 foreach ($items as $item) { 731 $id = $item->getID(); 732 $builtin_key = $item->getBuiltinKey(); 733 734 $can_edit = PhabricatorPolicyFilter::hasCapability( 735 $viewer, 736 $item, 737 PhabricatorPolicyCapability::CAN_EDIT); 738 739 $view = new PHUIObjectItemView(); 740 741 $name = $item->getDisplayName(); 742 $type = $item->getMenuItemTypeName(); 743 if (!strlen(trim($name))) { 744 $name = pht('Untitled "%s" Item', $type); 745 } 746 747 $view->setHeader($name); 748 $view->addAttribute($type); 749 750 $icon = $item->getMenuItem()->getMenuItemTypeIcon(); 751 if ($icon !== null) { 752 $view->setStatusIcon($icon); 753 } 754 755 if ($can_edit) { 756 $can_move = (!$item->getIsHeadItem() && !$item->getIsTailItem()); 757 if ($can_move) { 758 $view 759 ->setGrippable(true) 760 ->addSigil('profile-menu-item') 761 ->setMetadata( 762 array( 763 'key' => nonempty($id, $builtin_key), 764 )); 765 $any_draggable = true; 766 } else { 767 $view->setGrippable(false); 768 } 769 770 if ($id) { 771 $default_uri = $this->getItemURI("default/{$id}/"); 772 } else { 773 $default_uri = $this->getItemURI("default/{$builtin_key}/"); 774 } 775 776 $default_text = null; 777 778 if ($this->isMenuEnginePinnable()) { 779 if ($item->isDefault()) { 780 $default_icon = 'fa-thumb-tack green'; 781 $default_text = pht('Current Default'); 782 } else if ($item->canMakeDefault()) { 783 $default_icon = 'fa-thumb-tack'; 784 $default_text = pht('Make Default'); 785 } 786 } 787 788 if ($default_text !== null) { 789 $view->addAction( 790 id(new PHUIListItemView()) 791 ->setHref($default_uri) 792 ->setWorkflow(true) 793 ->setName($default_text) 794 ->setIcon($default_icon)); 795 } 796 797 if ($id) { 798 $view->setHref($this->getItemURI("edit/{$id}/")); 799 $hide_uri = $this->getItemURI("hide/{$id}/"); 800 } else { 801 $view->setHref($this->getItemURI("builtin/{$builtin_key}/")); 802 $hide_uri = $this->getItemURI("hide/{$builtin_key}/"); 803 } 804 805 if ($item->isDisabled()) { 806 $hide_icon = 'fa-plus'; 807 $hide_text = pht('Enable'); 808 } else if ($item->getBuiltinKey() !== null) { 809 $hide_icon = 'fa-times'; 810 $hide_text = pht('Disable'); 811 } else { 812 $hide_icon = 'fa-times'; 813 $hide_text = pht('Delete'); 814 } 815 816 $can_disable = $item->canHideMenuItem(); 817 818 $view->addAction( 819 id(new PHUIListItemView()) 820 ->setHref($hide_uri) 821 ->setWorkflow(true) 822 ->setDisabled(!$can_disable) 823 ->setName($hide_text) 824 ->setIcon($hide_icon)); 825 } 826 827 if ($item->isDisabled()) { 828 $view->setDisabled(true); 829 } 830 831 $list->addItem($view); 832 } 833 834 $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); 835 $object = $this->getProfileObject(); 836 837 $action_list = id(new PhabricatorActionListView()) 838 ->setViewer($viewer); 839 840 // See T12167. This makes the "Actions" dropdown button show up in the 841 // page header. 842 $action_list->setID(celerity_generate_unique_node_id()); 843 844 $action_list->addAction( 845 id(new PhabricatorActionView()) 846 ->setLabel(true) 847 ->setName(pht('Add New Menu Item...'))); 848 849 foreach ($item_types as $item_type) { 850 if (!$item_type->canAddToObject($object)) { 851 continue; 852 } 853 854 $item_key = $item_type->getMenuItemKey(); 855 $edit_mode = $this->getEditMode(); 856 857 $action_list->addAction( 858 id(new PhabricatorActionView()) 859 ->setIcon($item_type->getMenuItemTypeIcon()) 860 ->setName($item_type->getMenuItemTypeName()) 861 ->setHref($this->getItemURI("new/{$edit_mode}/{$item_key}/")) 862 ->setWorkflow(true)); 863 } 864 865 $action_list->addAction( 866 id(new PhabricatorActionView()) 867 ->setLabel(true) 868 ->setName(pht('Documentation'))); 869 870 $doc_link = PhabricatorEnv::getDoclink('Profile Menu User Guide'); 871 $doc_name = pht('Profile Menu User Guide'); 872 873 $action_list->addAction( 874 id(new PhabricatorActionView()) 875 ->setIcon('fa-book') 876 ->setHref($doc_link) 877 ->setName($doc_name)); 878 879 $header = id(new PHUIHeaderView()) 880 ->setHeader(pht('Menu Items')) 881 ->setHeaderIcon('fa-list'); 882 883 $list_header = id(new PHUIHeaderView()) 884 ->setHeader(pht('Current Menu Items')); 885 886 if ($any_draggable) { 887 $list_header->setSubheader( 888 pht('Drag items in this list to reorder them.')); 889 } 890 891 $box = id(new PHUIObjectBoxView()) 892 ->setHeader($list_header) 893 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 894 ->setObjectList($list); 895 896 $curtain = id(new PHUICurtainView()) 897 ->setViewer($viewer) 898 ->setActionList($action_list); 899 900 $view = id(new PHUITwoColumnView()) 901 ->setHeader($header) 902 ->setCurtain($curtain) 903 ->setMainColumn( 904 array( 905 $box, 906 )); 907 908 return $view; 909 } 910 911 private function buildItemNewContent($item_key, $mode) { 912 $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); 913 $item_type = idx($item_types, $item_key); 914 if (!$item_type) { 915 return new Aphront404Response(); 916 } 917 918 $object = $this->getProfileObject(); 919 if (!$item_type->canAddToObject($object)) { 920 return new Aphront404Response(); 921 } 922 923 $custom_phid = $this->getEditModeCustomPHID(); 924 925 $configuration = PhabricatorProfileMenuItemConfiguration::initializeNewItem( 926 $object, 927 $item_type, 928 $custom_phid); 929 930 $viewer = $this->getViewer(); 931 932 PhabricatorPolicyFilter::requireCapability( 933 $viewer, 934 $configuration, 935 PhabricatorPolicyCapability::CAN_EDIT); 936 937 $controller = $this->getController(); 938 939 return id(new PhabricatorProfileMenuEditEngine()) 940 ->setMenuEngine($this) 941 ->setProfileObject($object) 942 ->setNewMenuItemConfiguration($configuration) 943 ->setCustomPHID($custom_phid) 944 ->setController($controller) 945 ->buildResponse(); 946 } 947 948 private function buildItemEditContent() { 949 $viewer = $this->getViewer(); 950 $object = $this->getProfileObject(); 951 $controller = $this->getController(); 952 $custom_phid = $this->getEditModeCustomPHID(); 953 954 return id(new PhabricatorProfileMenuEditEngine()) 955 ->setMenuEngine($this) 956 ->setProfileObject($object) 957 ->setController($controller) 958 ->setCustomPHID($custom_phid) 959 ->buildResponse(); 960 } 961 962 private function buildItemBuiltinContent( 963 PhabricatorProfileMenuItemConfiguration $configuration) { 964 965 // If this builtin item has already been persisted, redirect to the 966 // edit page. 967 $id = $configuration->getID(); 968 if ($id) { 969 return id(new AphrontRedirectResponse()) 970 ->setURI($this->getItemURI("edit/{$id}/")); 971 } 972 973 // Otherwise, act like we're creating a new item, we're just starting 974 // with the builtin template. 975 $viewer = $this->getViewer(); 976 977 PhabricatorPolicyFilter::requireCapability( 978 $viewer, 979 $configuration, 980 PhabricatorPolicyCapability::CAN_EDIT); 981 982 $object = $this->getProfileObject(); 983 $controller = $this->getController(); 984 $custom_phid = $this->getEditModeCustomPHID(); 985 986 return id(new PhabricatorProfileMenuEditEngine()) 987 ->setIsBuiltin(true) 988 ->setMenuEngine($this) 989 ->setProfileObject($object) 990 ->setNewMenuItemConfiguration($configuration) 991 ->setController($controller) 992 ->setCustomPHID($custom_phid) 993 ->buildResponse(); 994 } 995 996 private function buildItemHideContent( 997 PhabricatorProfileMenuItemConfiguration $configuration) { 998 999 $controller = $this->getController(); 1000 $request = $controller->getRequest(); 1001 $viewer = $this->getViewer(); 1002 1003 PhabricatorPolicyFilter::requireCapability( 1004 $viewer, 1005 $configuration, 1006 PhabricatorPolicyCapability::CAN_EDIT); 1007 1008 if (!$configuration->canHideMenuItem()) { 1009 return $controller->newDialog() 1010 ->setTitle(pht('Mandatory Item')) 1011 ->appendParagraph( 1012 pht('This menu item is very important, and can not be disabled.')) 1013 ->addCancelButton($this->getConfigureURI()); 1014 } 1015 1016 if ($configuration->getBuiltinKey() === null) { 1017 $new_value = null; 1018 1019 $title = pht('Delete Menu Item'); 1020 $body = pht('Delete this menu item?'); 1021 $button = pht('Delete Menu Item'); 1022 } else if ($configuration->isDisabled()) { 1023 $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; 1024 1025 $title = pht('Enable Menu Item'); 1026 $body = pht( 1027 'Enable this menu item? It will appear in the menu again.'); 1028 $button = pht('Enable Menu Item'); 1029 } else { 1030 $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DISABLED; 1031 1032 $title = pht('Disable Menu Item'); 1033 $body = pht( 1034 'Disable this menu item? It will no longer appear in the menu, but '. 1035 'you can re-enable it later.'); 1036 $button = pht('Disable Menu Item'); 1037 } 1038 1039 $v_visibility = $configuration->getVisibility(); 1040 if ($request->isFormPost()) { 1041 if ($new_value === null) { 1042 $configuration->delete(); 1043 } else { 1044 $type_visibility = 1045 PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; 1046 1047 $xactions = array(); 1048 1049 $xactions[] = 1050 id(new PhabricatorProfileMenuItemConfigurationTransaction()) 1051 ->setTransactionType($type_visibility) 1052 ->setNewValue($new_value); 1053 1054 $editor = id(new PhabricatorProfileMenuEditor()) 1055 ->setContentSourceFromRequest($request) 1056 ->setActor($viewer) 1057 ->setContinueOnMissingFields(true) 1058 ->setContinueOnNoEffect(true) 1059 ->applyTransactions($configuration, $xactions); 1060 } 1061 1062 return id(new AphrontRedirectResponse()) 1063 ->setURI($this->getConfigureURI()); 1064 } 1065 1066 return $controller->newDialog() 1067 ->setTitle($title) 1068 ->appendParagraph($body) 1069 ->addCancelButton($this->getConfigureURI()) 1070 ->addSubmitButton($button); 1071 } 1072 1073 private function buildItemDefaultContent( 1074 PhabricatorProfileMenuItemConfiguration $configuration) { 1075 1076 $controller = $this->getController(); 1077 $request = $controller->getRequest(); 1078 $viewer = $this->getViewer(); 1079 1080 PhabricatorPolicyFilter::requireCapability( 1081 $viewer, 1082 $configuration, 1083 PhabricatorPolicyCapability::CAN_EDIT); 1084 1085 $done_uri = $this->getConfigureURI(); 1086 1087 if (!$configuration->canMakeDefault()) { 1088 return $controller->newDialog() 1089 ->setTitle(pht('Not Defaultable')) 1090 ->appendParagraph( 1091 pht( 1092 'This item can not be set as the default item. This is usually '. 1093 'because the item has no page of its own, or links to an '. 1094 'external page.')) 1095 ->addCancelButton($done_uri); 1096 } 1097 1098 if ($configuration->isDefault()) { 1099 return $controller->newDialog() 1100 ->setTitle(pht('Already Default')) 1101 ->appendParagraph( 1102 pht( 1103 'This item is already set as the default item for this menu.')) 1104 ->addCancelButton($done_uri); 1105 } 1106 1107 if ($request->isFormPost()) { 1108 $key = $configuration->getID(); 1109 if (!$key) { 1110 $key = $configuration->getBuiltinKey(); 1111 } 1112 1113 $this->adjustDefault($key); 1114 1115 return id(new AphrontRedirectResponse()) 1116 ->setURI($done_uri); 1117 } 1118 1119 return $controller->newDialog() 1120 ->setTitle(pht('Make Default')) 1121 ->appendParagraph( 1122 pht( 1123 'Set this item as the default for this menu? Users arriving on '. 1124 'this page will be shown the content of this item by default.')) 1125 ->addCancelButton($done_uri) 1126 ->addSubmitButton(pht('Make Default')); 1127 } 1128 1129 protected function newItem() { 1130 return PhabricatorProfileMenuItemConfiguration::initializeNewBuiltin(); 1131 } 1132 1133 protected function newManageItem() { 1134 return $this->newItem() 1135 ->setBuiltinKey(self::ITEM_MANAGE) 1136 ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY) 1137 ->setIsTailItem(true); 1138 } 1139 1140 protected function newDividerItem($key) { 1141 return $this->newItem() 1142 ->setBuiltinKey($key) 1143 ->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY) 1144 ->setIsTailItem(true); 1145 } 1146 1147 public function getDefaultMenuItemConfiguration() { 1148 $configs = $this->getItems(); 1149 foreach ($configs as $config) { 1150 if ($config->isDefault()) { 1151 return $config; 1152 } 1153 } 1154 1155 return null; 1156 } 1157 1158 public function adjustDefault($key) { 1159 $controller = $this->getController(); 1160 $request = $controller->getRequest(); 1161 $viewer = $request->getViewer(); 1162 1163 $items = $this->loadItems(self::MODE_COMBINED); 1164 1165 // To adjust the default item, we first change any existing items that 1166 // are marked as defaults to "visible", then make the new default item 1167 // the default. 1168 1169 $default = array(); 1170 $visible = array(); 1171 1172 foreach ($items as $item) { 1173 $builtin_key = $item->getBuiltinKey(); 1174 $id = $item->getID(); 1175 1176 $is_target = 1177 (($builtin_key !== null) && ($builtin_key === $key)) || 1178 (($id !== null) && ((int)$id === (int)$key)); 1179 1180 if ($is_target) { 1181 if (!$item->isDefault()) { 1182 $default[] = $item; 1183 } 1184 } else { 1185 if ($item->isDefault()) { 1186 $visible[] = $item; 1187 } 1188 } 1189 } 1190 1191 $type_visibility = 1192 PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; 1193 1194 $v_visible = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; 1195 $v_default = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT; 1196 1197 $apply = array( 1198 array($v_visible, $visible), 1199 array($v_default, $default), 1200 ); 1201 1202 foreach ($apply as $group) { 1203 list($value, $items) = $group; 1204 foreach ($items as $item) { 1205 $xactions = array(); 1206 1207 $xactions[] = 1208 id(new PhabricatorProfileMenuItemConfigurationTransaction()) 1209 ->setTransactionType($type_visibility) 1210 ->setNewValue($value); 1211 1212 $editor = id(new PhabricatorProfileMenuEditor()) 1213 ->setContentSourceFromRequest($request) 1214 ->setActor($viewer) 1215 ->setContinueOnMissingFields(true) 1216 ->setContinueOnNoEffect(true) 1217 ->applyTransactions($item, $xactions); 1218 } 1219 } 1220 1221 return $this; 1222 } 1223 1224 private function arrangeItems(array $items, $mode) { 1225 // Sort the items. 1226 $items = msortv($items, 'getSortVector'); 1227 1228 $object = $this->getProfileObject(); 1229 1230 // If we have some global items and some custom items and are in "combined" 1231 // mode, put a hard-coded divider item between them. 1232 if ($mode == self::MODE_COMBINED) { 1233 $list = array(); 1234 $seen_custom = false; 1235 $seen_global = false; 1236 foreach ($items as $item) { 1237 if ($item->getCustomPHID()) { 1238 $seen_custom = true; 1239 } else { 1240 if ($seen_custom && !$seen_global) { 1241 $list[] = $this->newItem() 1242 ->setBuiltinKey(self::ITEM_CUSTOM_DIVIDER) 1243 ->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY) 1244 ->attachProfileObject($object) 1245 ->attachMenuItem( 1246 new PhabricatorDividerProfileMenuItem()); 1247 } 1248 $seen_global = true; 1249 } 1250 $list[] = $item; 1251 } 1252 $items = $list; 1253 } 1254 1255 // Normalize keys since callers shouldn't rely on this array being 1256 // partially keyed. 1257 $items = array_values($items); 1258 1259 return $items; 1260 } 1261 1262 final protected function newEmptyView($title, $message) { 1263 return id(new PHUIInfoView()) 1264 ->setTitle($title) 1265 ->setSeverity(PHUIInfoView::SEVERITY_NODATA) 1266 ->setErrors( 1267 array( 1268 $message, 1269 )); 1270 } 1271 1272 protected function newNoContentView(array $items) { 1273 return $this->newEmptyView( 1274 pht('No Content'), 1275 pht('No visible menu items can render content.')); 1276 } 1277 1278 1279 final public function newProfileMenuItemViewList() { 1280 $items = $this->getItems(); 1281 1282 // Throw away disabled items: they are not allowed to build any views for 1283 // the menu. 1284 foreach ($items as $key => $item) { 1285 if ($item->isDisabled()) { 1286 unset($items[$key]); 1287 continue; 1288 } 1289 } 1290 1291 // Give each item group a callback so it can load data it needs to render 1292 // views. 1293 $groups = mgroup($items, 'getMenuItemKey'); 1294 foreach ($groups as $group) { 1295 $item = head($group); 1296 $item->willGetMenuItemViewList($group); 1297 } 1298 1299 $view_list = id(new PhabricatorProfileMenuItemViewList()) 1300 ->setProfileMenuEngine($this); 1301 1302 foreach ($items as $item) { 1303 $views = $item->getMenuItemViewList(); 1304 foreach ($views as $view) { 1305 $view_list->addItemView($view); 1306 } 1307 } 1308 1309 return $view_list; 1310 } 1311 1312 private function selectViewItem( 1313 PhabricatorProfileMenuItemViewList $view_list, 1314 $item_id) { 1315 1316 // Figure out which view's content we're going to render. In most cases, 1317 // the URI tells us. If we don't have an identifier in the URI, we'll 1318 // render the default view instead. 1319 1320 $selected_view = null; 1321 if (phutil_nonempty_string($item_id)) { 1322 $item_views = $view_list->getViewsWithItemIdentifier($item_id); 1323 if ($item_views) { 1324 $selected_view = head($item_views); 1325 } 1326 } else { 1327 $default_views = $view_list->getDefaultViews(); 1328 if ($default_views) { 1329 $selected_view = head($default_views); 1330 } 1331 } 1332 1333 if ($selected_view) { 1334 $view_list->setSelectedView($selected_view); 1335 $selected_item = $selected_view->getMenuItemConfiguration(); 1336 } else { 1337 $selected_item = null; 1338 } 1339 1340 return $selected_item; 1341 } 1342 1343 private function selectEditItem( 1344 PhabricatorProfileMenuItemViewList $view_list, 1345 $item_id) { 1346 1347 // First, try to select a visible item using the normal view selection 1348 // pathway. If this works, it also highlights the menu properly. 1349 1350 if ($item_id) { 1351 $selected_item = $this->selectViewItem($view_list, $item_id); 1352 if ($selected_item) { 1353 return $selected_item; 1354 } 1355 } 1356 1357 // If we didn't find an item in the view list, we may be enabling an item 1358 // which is currently disabled or editing an item which is not generating 1359 // any actual items in the menu. 1360 1361 foreach ($this->getItems() as $item) { 1362 if ($item->matchesIdentifier($item_id)) { 1363 return $item; 1364 } 1365 } 1366 1367 return null; 1368 } 1369 1370 1371}