diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php index a5c99a05d0..5da4760cd3 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php @@ -1,39 +1,50 @@ getProfileObject(); return $portal->getURI().$path; } protected function getBuiltinProfileItems($object) { $items = array(); $items[] = $this->newManageItem(); $items[] = $this->newItem() ->setMenuItemKey(PhabricatorDashboardPortalMenuItem::MENUITEMKEY) ->setBuiltinKey('manage') ->setIsTailItem(true); return $items; } - protected function newNoMenuItemsView() { - return $this->newEmptyView( - pht('New Portal'), - pht('Use "Edit Menu" to add menu items to this portal.')); + protected function newNoMenuItemsView(array $items) { + $object = $this->getProfileObject(); + $builtins = $this->getBuiltinProfileItems($object); + + if (count($items) <= count($builtins)) { + return $this->newEmptyView( + pht('New Portal'), + pht('Use "Edit Menu" to add menu items to this portal.')); + } else { + return $this->newEmptyValue( + pht('No Portal Content'), + pht( + 'None of the visible menu items in this portal can render any '. + 'content.')); + } } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index 5e89f22b16..fadb929594 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -1,1337 +1,1360 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setProfileObject($profile_object) { $this->profileObject = $profile_object; return $this; } public function getProfileObject() { return $this->profileObject; } public function setCustomPHID($custom_phid) { $this->customPHID = $custom_phid; return $this; } public function getCustomPHID() { return $this->customPHID; } private function getEditModeCustomPHID() { $mode = $this->getEditMode(); switch ($mode) { case self::MODE_CUSTOM: $custom_phid = $this->getCustomPHID(); break; case self::MODE_GLOBAL: $custom_phid = null; break; } return $custom_phid; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } public function setShowNavigation($show) { $this->showNavigation = $show; return $this; } public function getShowNavigation() { return $this->showNavigation; } public function addContentPageClass($class) { $this->pageClasses[] = $class; return $this; } public function setShowContentCrumbs($show_content_crumbs) { $this->showContentCrumbs = $show_content_crumbs; return $this; } public function getShowContentCrumbs() { return $this->showContentCrumbs; } abstract public function getItemURI($path); abstract protected function isMenuEngineConfigurable(); abstract protected function getBuiltinProfileItems($object); protected function getBuiltinCustomProfileItems( $object, $custom_phid) { return array(); } protected function getEditMode() { return $this->editMode; } public function buildResponse() { $controller = $this->getController(); $viewer = $controller->getViewer(); $this->setViewer($viewer); $request = $controller->getRequest(); $item_action = $request->getURIData('itemAction'); if (!$item_action) { $item_action = 'view'; } $is_view = ($item_action == 'view'); // If the engine is not configurable, don't respond to any of the editing // or configuration routes. if (!$this->isMenuEngineConfigurable()) { if (!$is_view) { return new Aphront404Response(); } } $item_id = $request->getURIData('itemID'); // If we miss on the MenuEngine route, try the EditEngine route. This will // be populated while editing items. if (!$item_id) { $item_id = $request->getURIData('id'); } $view_list = $this->newProfileMenuItemViewList(); - $selected_item = $this->selectItem( - $view_list, - $item_id, - $is_view); + if ($is_view) { + $selected_item = $this->selectViewItem($view_list, $item_id); + } else { + if (!strlen($item_id)) { + $item_id = self::ITEM_MANAGE; + } + $selected_item = $this->selectEditItem($view_list, $item_id); + } switch ($item_action) { case 'view': // If we were not able to select an item, we're still going to render // a page state. For example, this happens when you create a new // portal for the first time. break; case 'info': case 'hide': case 'default': case 'builtin': if (!$selected_item) { return new Aphront404Response(); } break; case 'edit': if (!$request->getURIData('id')) { // If we continue along the "edit" pathway without an ID, we hit an // unrelated exception because we can not build a new menu item out // of thin air. For menus, new items are created via the "new" // action. Just catch this case and 404 early since there's currently // no clean way to make EditEngine aware of this. return new Aphront404Response(); } break; } $navigation = $view_list->newNavigationView(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if (!$is_view) { - $navigation->selectFilter(self::ITEM_MANAGE); - if ($selected_item) { if ($selected_item->getCustomPHID()) { $edit_mode = 'custom'; } else { $edit_mode = 'global'; } } else { $edit_mode = $request->getURIData('itemEditMode'); } $available_modes = $this->getViewerEditModes(); if ($available_modes) { $available_modes = array_fuse($available_modes); if (isset($available_modes[$edit_mode])) { $this->editMode = $edit_mode; } else { if ($item_action != 'configure') { return new Aphront404Response(); } } } $page_title = pht('Configure Menu'); } else { if ($selected_item) { $page_title = $selected_item->getDisplayName(); } else { $page_title = pht('Empty'); } } switch ($item_action) { case 'view': if ($selected_item) { try { $content = $this->buildItemViewContent($selected_item); } catch (Exception $ex) { $content = id(new PHUIInfoView()) ->setTitle(pht('Unable to Render Dashboard')) ->setErrors(array($ex->getMessage())); } $crumbs->addTextCrumb($selected_item->getDisplayName()); } else { - $content = $this->newNoMenuItemsView(); + $content = $this->newNoContentView($this->getItems()); } if (!$content) { $content = $this->newEmptyView( pht('Empty'), pht('There is nothing here.')); } break; case 'configure': $mode = $this->getEditMode(); if (!$mode) { $crumbs->addTextCrumb(pht('Configure Menu')); $content = $this->buildMenuEditModeContent(); } else { if (count($available_modes) > 1) { $crumbs->addTextCrumb( pht('Configure Menu'), $this->getItemURI('configure/')); switch ($mode) { case self::MODE_CUSTOM: $crumbs->addTextCrumb(pht('Personal')); break; case self::MODE_GLOBAL: $crumbs->addTextCrumb(pht('Global')); break; } } else { $crumbs->addTextCrumb(pht('Configure Menu')); } $edit_list = $this->loadItems($mode); $content = $this->buildItemConfigureContent($edit_list); } break; case 'reorder': $mode = $this->getEditMode(); $edit_list = $this->loadItems($mode); $content = $this->buildItemReorderContent($edit_list); break; case 'new': $item_key = $request->getURIData('itemKey'); $mode = $this->getEditMode(); $content = $this->buildItemNewContent($item_key, $mode); break; case 'builtin': $content = $this->buildItemBuiltinContent($selected_item); break; case 'hide': $content = $this->buildItemHideContent($selected_item); break; case 'default': if (!$this->isMenuEnginePinnable()) { return new Aphront404Response(); } $content = $this->buildItemDefaultContent($selected_item); break; case 'edit': $content = $this->buildItemEditContent(); break; default: throw new Exception( pht( 'Unsupported item action "%s".', $item_action)); } if ($content instanceof AphrontResponse) { return $content; } if ($content instanceof AphrontResponseProducerInterface) { return $content; } $crumbs->setBorder(true); $page = $controller->newPage() ->setTitle($page_title) ->appendChild($content); if (!$is_view || $this->getShowContentCrumbs()) { $page->setCrumbs($crumbs); } if ($this->getShowNavigation()) { $page->setNavigation($navigation); } if ($is_view) { foreach ($this->pageClasses as $class) { $page->addClass($class); } } return $page; } - - private function getItems() { if ($this->items === null) { $this->items = $this->loadItems(self::MODE_COMBINED); } return $this->items; } private function loadItems($mode) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $items = $this->loadBuiltinProfileItems($mode); $query = id(new PhabricatorProfileMenuItemConfigurationQuery()) ->setViewer($viewer) ->withProfilePHIDs(array($object->getPHID())); switch ($mode) { case self::MODE_GLOBAL: $query->withCustomPHIDs(array(), true); break; case self::MODE_CUSTOM: $query->withCustomPHIDs(array($this->getCustomPHID()), false); break; case self::MODE_COMBINED: $query->withCustomPHIDs(array($this->getCustomPHID()), true); break; } $stored_items = $query->execute(); foreach ($stored_items as $stored_item) { $impl = $stored_item->getMenuItem(); $impl->setViewer($viewer); $impl->setEngine($this); } // Merge the stored items into the builtin items. If a builtin item has // a stored version, replace the defaults with the stored changes. foreach ($stored_items as $stored_item) { if (!$stored_item->shouldEnableForObject($object)) { continue; } $builtin_key = $stored_item->getBuiltinKey(); if ($builtin_key !== null) { // If this builtin actually exists, replace the builtin with the // stored configuration. Otherwise, we're just going to drop the // stored config: it corresponds to an out-of-date or uninstalled // item. if (isset($items[$builtin_key])) { $builtin_item = $items[$builtin_key]; // Copy runtime properties from the builtin item to the stored item. $stored_item->setIsHeadItem($builtin_item->getIsHeadItem()); $stored_item->setIsTailItem($builtin_item->getIsTailItem()); $items[$builtin_key] = $stored_item; } else { continue; } } else { $items[] = $stored_item; } } return $this->arrangeItems($items, $mode); } private function loadBuiltinProfileItems($mode) { $object = $this->getProfileObject(); switch ($mode) { case self::MODE_GLOBAL: $builtins = $this->getBuiltinProfileItems($object); break; case self::MODE_CUSTOM: $builtins = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); break; case self::MODE_COMBINED: $builtins = array(); $builtins[] = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); $builtins[] = $this->getBuiltinProfileItems($object); $builtins = array_mergev($builtins); break; } $items = PhabricatorProfileMenuItem::getAllMenuItems(); $viewer = $this->getViewer(); $order = 1; $map = array(); foreach ($builtins as $builtin) { $builtin_key = $builtin->getBuiltinKey(); if (!$builtin_key) { throw new Exception( pht( 'Object produced a builtin item with no builtin item key! '. 'Builtin items must have a unique key.')); } if (isset($map[$builtin_key])) { throw new Exception( pht( 'Object produced two items with the same builtin key ("%s"). '. 'Each item must have a unique builtin key.', $builtin_key)); } $item_key = $builtin->getMenuItemKey(); $item = idx($items, $item_key); if (!$item) { throw new Exception( pht( 'Builtin item ("%s") specifies a bad item key ("%s"); there '. 'is no corresponding item implementation available.', $builtin_key, $item_key)); } $item = clone $item; $item->setViewer($viewer); $item->setEngine($this); $builtin ->setProfilePHID($object->getPHID()) ->attachMenuItem($item) ->attachProfileObject($object) ->setMenuItemOrder($order); if (!$builtin->shouldEnableForObject($object)) { continue; } $map[$builtin_key] = $builtin; $order++; } return $map; } private function validateNavigationMenuItem($item) { if (!($item instanceof PHUIListItemView)) { throw new Exception( pht( 'Expected buildNavigationMenuItems() to return a list of '. 'PHUIListItemView objects, but got a surprise.')); } } public function getConfigureURI() { $mode = $this->getEditMode(); switch ($mode) { case self::MODE_CUSTOM: return $this->getItemURI('configure/custom/'); case self::MODE_GLOBAL: return $this->getItemURI('configure/global/'); } return $this->getItemURI('configure/'); } private function buildItemReorderContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); // If you're reordering global items, you need to be able to edit the // object the menu appears on. If you're reordering custom items, you only // need to be able to edit the custom object. Currently, the custom object // is always the viewing user's own user object. $custom_phid = $this->getEditModeCustomPHID(); if (!$custom_phid) { PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } else { $policy_object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($custom_phid)) ->executeOne(); if (!$policy_object) { throw new Exception( pht( 'Failed to load custom PHID "%s"!', $custom_phid)); } PhabricatorPolicyFilter::requireCapability( $viewer, $policy_object, PhabricatorPolicyCapability::CAN_EDIT); } $controller = $this->getController(); $request = $controller->getRequest(); $request->validateCSRF(); $order = $request->getStrList('order'); $by_builtin = array(); $by_id = array(); foreach ($items as $key => $item) { $id = $item->getID(); if ($id) { $by_id[$id] = $key; continue; } $builtin_key = $item->getBuiltinKey(); if ($builtin_key) { $by_builtin[$builtin_key] = $key; continue; } } $key_order = array(); foreach ($order as $order_item) { if (isset($by_id[$order_item])) { $key_order[] = $by_id[$order_item]; continue; } if (isset($by_builtin[$order_item])) { $key_order[] = $by_builtin[$order_item]; continue; } } $items = array_select_keys($items, $key_order) + $items; $type_order = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_ORDER; $order = 1; foreach ($items as $item) { $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($order); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); $order++; } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } protected function buildItemViewContent( PhabricatorProfileMenuItemConfiguration $item) { return $item->newPageContent(); } private function getViewerEditModes() { $modes = array(); $viewer = $this->getViewer(); if ($viewer->isLoggedIn() && $this->isMenuEnginePersonalizable()) { $modes[] = self::MODE_CUSTOM; } $object = $this->getProfileObject(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); if ($can_edit) { $modes[] = self::MODE_GLOBAL; } return $modes; } protected function isMenuEnginePersonalizable() { return true; } /** * Does this engine support pinning items? * * Personalizable menus disable pinning by default since it creates a number * of weird edge cases without providing many benefits for current menus. * * @return bool True if items may be pinned as default items. */ public function isMenuEnginePinnable() { return !$this->isMenuEnginePersonalizable(); } private function buildMenuEditModeContent() { $viewer = $this->getViewer(); $modes = $this->getViewerEditModes(); if (!$modes) { return new Aphront404Response(); } if (count($modes) == 1) { $mode = head($modes); return id(new AphrontRedirectResponse()) ->setURI($this->getItemURI("configure/{$mode}/")); } $menu = id(new PHUIObjectItemListView()) ->setUser($viewer); $modes = array_fuse($modes); if (isset($modes['custom'])) { $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Personal Menu Items')) ->setHref($this->getItemURI('configure/custom/')) ->setImageURI($viewer->getProfileImageURI()) ->addAttribute(pht('Edit the menu for your personal account.'))); } if (isset($modes['global'])) { $icon = id(new PHUIIconView()) ->setIcon('fa-globe') ->setBackground('bg-blue'); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Global Menu Items')) ->setHref($this->getItemURI('configure/global/')) ->setImageIcon($icon) ->addAttribute(pht('Edit the global default menu for all users.'))); } $box = id(new PHUIObjectBoxView()) ->setObjectList($menu); $header = id(new PHUIHeaderView()) ->setHeader(pht('Manage Menu')) ->setHeaderIcon('fa-list'); return id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); } private function buildItemConfigureContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $filtered_groups = mgroup($items, 'getMenuItemKey'); foreach ($filtered_groups as $group) { $first_item = head($group); $first_item->willGetMenuItemViewList($group); } // Users only need to be able to edit the object which this menu appears // on if they're editing global menu items. For example, users do not need // to be able to edit the Favorites application to add new items to the // Favorites menu. if (!$this->getCustomPHID()) { PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } $list_id = celerity_generate_unique_node_id(); $mode = $this->getEditMode(); Javelin::initBehavior( 'reorder-profile-menu-items', array( 'listID' => $list_id, 'orderURI' => $this->getItemURI("reorder/{$mode}/"), )); $list = id(new PHUIObjectItemListView()) ->setID($list_id) ->setNoDataString(pht('This menu currently has no items.')); $any_draggable = false; foreach ($items as $item) { $id = $item->getID(); $builtin_key = $item->getBuiltinKey(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $item, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PHUIObjectItemView()); $name = $item->getDisplayName(); $type = $item->getMenuItemTypeName(); if (!strlen(trim($name))) { $name = pht('Untitled "%s" Item', $type); } $view->setHeader($name); $view->addAttribute($type); $icon = $item->getMenuItem()->getMenuItemTypeIcon(); if ($icon !== null) { $view->setStatusIcon($icon); } if ($can_edit) { $can_move = (!$item->getIsHeadItem() && !$item->getIsTailItem()); if ($can_move) { $view ->setGrippable(true) ->addSigil('profile-menu-item') ->setMetadata( array( 'key' => nonempty($id, $builtin_key), )); $any_draggable = true; } else { $view->setGrippable(false); } if ($id) { $default_uri = $this->getItemURI("default/{$id}/"); } else { $default_uri = $this->getItemURI("default/{$builtin_key}/"); } $default_text = null; if ($this->isMenuEnginePinnable()) { if ($item->isDefault()) { $default_icon = 'fa-thumb-tack green'; $default_text = pht('Current Default'); } else if ($item->canMakeDefault()) { $default_icon = 'fa-thumb-tack'; $default_text = pht('Make Default'); } } if ($default_text !== null) { $view->addAction( id(new PHUIListItemView()) ->setHref($default_uri) ->setWorkflow(true) ->setName($default_text) ->setIcon($default_icon)); } if ($id) { $view->setHref($this->getItemURI("edit/{$id}/")); $hide_uri = $this->getItemURI("hide/{$id}/"); } else { $view->setHref($this->getItemURI("builtin/{$builtin_key}/")); $hide_uri = $this->getItemURI("hide/{$builtin_key}/"); } if ($item->isDisabled()) { $hide_icon = 'fa-plus'; $hide_text = pht('Enable'); } else if ($item->getBuiltinKey() !== null) { $hide_icon = 'fa-times'; $hide_text = pht('Disable'); } else { $hide_icon = 'fa-times'; $hide_text = pht('Delete'); } $can_disable = $item->canHideMenuItem(); $view->addAction( id(new PHUIListItemView()) ->setHref($hide_uri) ->setWorkflow(true) ->setDisabled(!$can_disable) ->setName($hide_text) ->setIcon($hide_icon)); } if ($item->isDisabled()) { $view->setDisabled(true); } $list->addItem($view); } $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $object = $this->getProfileObject(); $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); // See T12167. This makes the "Actions" dropdown button show up in the // page header. $action_list->setID(celerity_generate_unique_node_id()); $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Add New Menu Item...'))); foreach ($item_types as $item_type) { if (!$item_type->canAddToObject($object)) { continue; } $item_key = $item_type->getMenuItemKey(); $edit_mode = $this->getEditMode(); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon($item_type->getMenuItemTypeIcon()) ->setName($item_type->getMenuItemTypeName()) ->setHref($this->getItemURI("new/{$edit_mode}/{$item_key}/")) ->setWorkflow(true)); } $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Documentation'))); $doc_link = PhabricatorEnv::getDoclink('Profile Menu User Guide'); $doc_name = pht('Profile Menu User Guide'); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-book') ->setHref($doc_link) ->setName($doc_name)); $header = id(new PHUIHeaderView()) ->setHeader(pht('Menu Items')) ->setHeaderIcon('fa-list'); $list_header = id(new PHUIHeaderView()) ->setHeader(pht('Current Menu Items')); if ($any_draggable) { $list_header->setSubheader( pht('Drag items in this list to reorder them.')); } $box = id(new PHUIObjectBoxView()) ->setHeader($list_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); $curtain = id(new PHUICurtainView()) ->setViewer($viewer) ->setActionList($action_list); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $box, )); return $view; } private function buildItemNewContent($item_key, $mode) { $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $item_type = idx($item_types, $item_key); if (!$item_type) { return new Aphront404Response(); } $object = $this->getProfileObject(); if (!$item_type->canAddToObject($object)) { return new Aphront404Response(); } $custom_phid = $this->getEditModeCustomPHID(); $configuration = PhabricatorProfileMenuItemConfiguration::initializeNewItem( $object, $item_type, $custom_phid); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $controller = $this->getController(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setCustomPHID($custom_phid) ->setController($controller) ->buildResponse(); } private function buildItemEditContent() { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $controller = $this->getController(); $custom_phid = $this->getEditModeCustomPHID(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setController($controller) ->setCustomPHID($custom_phid) ->buildResponse(); } private function buildItemBuiltinContent( PhabricatorProfileMenuItemConfiguration $configuration) { // If this builtin item has already been persisted, redirect to the // edit page. $id = $configuration->getID(); if ($id) { return id(new AphrontRedirectResponse()) ->setURI($this->getItemURI("edit/{$id}/")); } // Otherwise, act like we're creating a new item, we're just starting // with the builtin template. $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $object = $this->getProfileObject(); $controller = $this->getController(); $custom_phid = $this->getEditModeCustomPHID(); return id(new PhabricatorProfileMenuEditEngine()) ->setIsBuiltin(true) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setController($controller) ->setCustomPHID($custom_phid) ->buildResponse(); } private function buildItemHideContent( PhabricatorProfileMenuItemConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); if (!$configuration->canHideMenuItem()) { return $controller->newDialog() ->setTitle(pht('Mandatory Item')) ->appendParagraph( pht('This menu item is very important, and can not be disabled.')) ->addCancelButton($this->getConfigureURI()); } if ($configuration->getBuiltinKey() === null) { $new_value = null; $title = pht('Delete Menu Item'); $body = pht('Delete this menu item?'); $button = pht('Delete Menu Item'); } else if ($configuration->isDisabled()) { $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; $title = pht('Enable Menu Item'); $body = pht( 'Enable this menu item? It will appear in the menu again.'); $button = pht('Enable Menu Item'); } else { $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DISABLED; $title = pht('Disable Menu Item'); $body = pht( 'Disable this menu item? It will no longer appear in the menu, but '. 'you can re-enable it later.'); $button = pht('Disable Menu Item'); } $v_visibility = $configuration->getVisibility(); if ($request->isFormPost()) { if ($new_value === null) { $configuration->delete(); } else { $type_visibility = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($new_value); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($configuration, $xactions); } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } return $controller->newDialog() ->setTitle($title) ->appendParagraph($body) ->addCancelButton($this->getConfigureURI()) ->addSubmitButton($button); } private function buildItemDefaultContent( PhabricatorProfileMenuItemConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $done_uri = $this->getConfigureURI(); if (!$configuration->canMakeDefault()) { return $controller->newDialog() ->setTitle(pht('Not Defaultable')) ->appendParagraph( pht( 'This item can not be set as the default item. This is usually '. 'because the item has no page of its own, or links to an '. 'external page.')) ->addCancelButton($done_uri); } if ($configuration->isDefault()) { return $controller->newDialog() ->setTitle(pht('Already Default')) ->appendParagraph( pht( 'This item is already set as the default item for this menu.')) ->addCancelButton($done_uri); } if ($request->isFormPost()) { $key = $configuration->getID(); if (!$key) { $key = $configuration->getBuiltinKey(); } $this->adjustDefault($key); return id(new AphrontRedirectResponse()) ->setURI($done_uri); } return $controller->newDialog() ->setTitle(pht('Make Default')) ->appendParagraph( pht( 'Set this item as the default for this menu? Users arriving on '. 'this page will be shown the content of this item by default.')) ->addCancelButton($done_uri) ->addSubmitButton(pht('Make Default')); } protected function newItem() { return PhabricatorProfileMenuItemConfiguration::initializeNewBuiltin(); } protected function newManageItem() { return $this->newItem() ->setBuiltinKey(self::ITEM_MANAGE) ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY) ->setIsTailItem(true); } public function getDefaultMenuItemConfiguration() { $configs = $this->getItems(); foreach ($configs as $config) { if ($config->isDefault()) { return $config; } } return null; } public function adjustDefault($key) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $items = $this->loadItems(self::MODE_COMBINED); // To adjust the default item, we first change any existing items that // are marked as defaults to "visible", then make the new default item // the default. $default = array(); $visible = array(); foreach ($items as $item) { $builtin_key = $item->getBuiltinKey(); $id = $item->getID(); $is_target = (($builtin_key !== null) && ($builtin_key === $key)) || (($id !== null) && ((int)$id === (int)$key)); if ($is_target) { if (!$item->isDefault()) { $default[] = $item; } } else { if ($item->isDefault()) { $visible[] = $item; } } } $type_visibility = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; $v_visible = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; $v_default = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT; $apply = array( array($v_visible, $visible), array($v_default, $default), ); foreach ($apply as $group) { list($value, $items) = $group; foreach ($items as $item) { $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($value); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); } } return $this; } private function arrangeItems(array $items, $mode) { // Sort the items. $items = msortv($items, 'getSortVector'); $object = $this->getProfileObject(); // If we have some global items and some custom items and are in "combined" // mode, put a hard-coded divider item between them. if ($mode == self::MODE_COMBINED) { $list = array(); $seen_custom = false; $seen_global = false; foreach ($items as $item) { if ($item->getCustomPHID()) { $seen_custom = true; } else { if ($seen_custom && !$seen_global) { $list[] = $this->newItem() ->setBuiltinKey(self::ITEM_CUSTOM_DIVIDER) ->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY) ->attachProfileObject($object) ->attachMenuItem( new PhabricatorDividerProfileMenuItem()); } $seen_global = true; } $list[] = $item; } $items = $list; } // Normalize keys since callers shouldn't rely on this array being // partially keyed. $items = array_values($items); return $items; } final protected function newEmptyView($title, $message) { return id(new PHUIInfoView()) ->setTitle($title) ->setSeverity(PHUIInfoView::SEVERITY_NODATA) ->setErrors( array( $message, )); } - protected function newNoMenuItemsView() { + protected function newNoContentView(array $items) { return $this->newEmptyView( - pht('No Menu Items'), - pht('There are no menu items.')); + pht('No Content'), + pht('No visible menu items can render content.')); } final public function newProfileMenuItemViewList() { $items = $this->getItems(); // Throw away disabled items: they are not allowed to build any views for // the menu. foreach ($items as $key => $item) { if ($item->isDisabled()) { unset($items[$key]); continue; } } // Give each item group a callback so it can load data it needs to render // views. $groups = mgroup($items, 'getMenuItemKey'); foreach ($groups as $group) { $item = head($group); $item->willGetMenuItemViewList($group); } $view_list = id(new PhabricatorProfileMenuItemViewList()) ->setProfileMenuEngine($this); foreach ($items as $item) { $views = $item->getMenuItemViewList(); foreach ($views as $view) { $view_list->addItemView($view); } } return $view_list; } - private function selectItem( + private function selectViewItem( PhabricatorProfileMenuItemViewList $view_list, - $item_id, - $want_default) { + $item_id) { // Figure out which view's content we're going to render. In most cases, // the URI tells us. If we don't have an identifier in the URI, we'll - // render the default view instead if this is a workflow that falls back - // to default rendering. + // render the default view instead. $selected_view = null; if (strlen($item_id)) { $item_views = $view_list->getViewsWithItemIdentifier($item_id); if ($item_views) { $selected_view = head($item_views); } } else { - if ($want_default) { - $default_views = $view_list->getDefaultViews(); - if ($default_views) { - $selected_view = head($default_views); - } + $default_views = $view_list->getDefaultViews(); + if ($default_views) { + $selected_view = head($default_views); } } if ($selected_view) { $view_list->setSelectedView($selected_view); $selected_item = $selected_view->getMenuItemConfiguration(); } else { $selected_item = null; } return $selected_item; } + private function selectEditItem( + PhabricatorProfileMenuItemViewList $view_list, + $item_id) { + + // First, try to select a visible item using the normal view selection + // pathway. If this works, it also highlights the menu properly. + + if ($item_id) { + $selected_item = $this->selectViewItem($view_list, $item_id); + if ($selected_item) { + return $selected_item; + } + } + + // If we didn't find an item in the view list, we may be enabling an item + // which is currently disabled or editing an item which is not generating + // any actual items in the menu. + + foreach ($this->getItems() as $item) { + if ($item->matchesIdentifier($item_id)) { + return $item; + } + } + + return null; + } + } diff --git a/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php b/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php index 4890657b4e..e5a3fd3d38 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php +++ b/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php @@ -1,278 +1,266 @@ engine = $engine; return $this; } public function getProfileMenuEngine() { return $this->engine; } public function addItemView(PhabricatorProfileMenuItemView $view) { $this->views[] = $view; return $this; } public function getItemViews() { return $this->views; } public function setSelectedView(PhabricatorProfileMenuItemView $view) { $found = false; foreach ($this->getItemViews() as $item_view) { if ($view === $item_view) { $found = true; break; } } if (!$found) { throw new Exception( pht( 'Provided view is not one of the views in the list: you can only '. 'select a view which appears in the list.')); } $this->selectedView = $view; return $this; } public function setSelectedViewWithItemIdentifier($identifier) { $views = $this->getViewsWithItemIdentifier($identifier); if (!$views) { throw new Exception( pht( 'No views match identifier "%s"!', $identifier)); } return $this->setSelectedView(head($views)); } public function getViewsWithItemIdentifier($identifier) { $views = $this->getItemViews(); - if (!strlen($identifier)) { - return array(); - } - - if (ctype_digit($identifier)) { - $identifier_int = (int)$identifier; - } else { - $identifier_int = null; - } - - $identifier_str = (string)$identifier; - $results = array(); foreach ($views as $view) { $config = $view->getMenuItemConfiguration(); - if ($identifier_int !== null) { - $config_id = (int)$config->getID(); - if ($config_id === $identifier_int) { - $results[] = $view; - continue; - } - } - - if ($config->getBuiltinKey() === $identifier_str) { - $results[] = $view; + if (!$config->matchesIdentifier($identifier)) { continue; } + + $results[] = $view; } return $results; } public function getDefaultViews() { $engine = $this->getProfileMenuEngine(); $can_pin = $engine->isMenuEnginePinnable(); $views = $this->getItemViews(); // Remove all the views which were built by an item that can not be the // default item. foreach ($views as $key => $view) { $config = $view->getMenuItemConfiguration(); if (!$config->canMakeDefault()) { unset($views[$key]); continue; } } + // Remove disabled views. + foreach ($views as $key => $view) { + if ($view->getDisabled()) { + unset($views[$key]); + } + } + // If this engine supports pinning items and we have candidate views from a // valid pinned item, they are the default views. if ($can_pin) { $pinned = array(); foreach ($views as $key => $view) { $config = $view->getMenuItemConfiguration(); if ($config->isDefault()) { $pinned[] = $view; continue; } } if ($pinned) { return $pinned; } } // Return whatever remains that's still valid. return $views; } public function newNavigationView() { $engine = $this->getProfileMenuEngine(); $base_uri = $engine->getItemURI(''); $base_uri = new PhutilURI($base_uri); $navigation = id(new AphrontSideNavFilterView()) ->setIsProfileMenu(true) ->setBaseURI($base_uri); $views = $this->getItemViews(); $selected_item = null; $item_key = 0; $items = array(); foreach ($views as $view) { $list_item = $view->newListItemView(); // Assign unique keys to the list items. These keys are purely internal. $list_item->setKey(sprintf('item(%d)', $item_key++)); if ($this->selectedView) { if ($this->selectedView === $view) { $selected_item = $list_item; } } $navigation->addMenuItem($list_item); $items[] = $list_item; } if (!$views) { // If the navigation menu has no items, add an empty label item to // force it to render something. $empty_item = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL); $navigation->addMenuItem($empty_item); } $highlight_key = $this->getHighlightedItemKey( $items, $selected_item); $navigation->selectFilter($highlight_key); return $navigation; } private function getHighlightedItemKey( array $items, PHUIListItemView $selected_item = null) { assert_instances_of($items, 'PHUIListItemView'); $default_key = null; if ($selected_item) { $default_key = $selected_item->getKey(); } $engine = $this->getProfileMenuEngine(); $controller = $engine->getController(); // In some rare cases, when like building the "Favorites" menu on a // 404 page, we may not have a controller. Just accept whatever default // behavior we'd otherwise end up with. if (!$controller) { return $default_key; } $request = $controller->getRequest(); // See T12949. If one of the menu items is a link to the same URI that // the page was accessed with, we want to highlight that item. For example, // this allows you to add links to a menu that apply filters to a // workboard. $matches = array(); foreach ($items as $item) { $href = $item->getHref(); if ($this->isMatchForRequestURI($request, $href)) { $matches[] = $item; } } foreach ($matches as $match) { if ($match->getKey() === $default_key) { return $default_key; } } if ($matches) { return head($matches)->getKey(); } return $default_key; } private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { $request_uri = $request->getAbsoluteRequestURI(); $item_uri = new PhutilURI($item_uri); // If the request URI and item URI don't have matching paths, they // do not match. if ($request_uri->getPath() !== $item_uri->getPath()) { return false; } // If the request URI and item URI don't have matching parameters, they // also do not match. We're specifically trying to let "?filter=X" work // on Workboards, among other use cases, so this is important. $request_params = $request_uri->getQueryParamsAsPairList(); $item_params = $item_uri->getQueryParamsAsPairList(); if ($request_params !== $item_params) { return false; } // If the paths and parameters match, the item domain must be: empty; or // match the request domain; or match the production domain. $request_domain = $request_uri->getDomain(); $production_uri = PhabricatorEnv::getProductionURI('/'); $production_domain = id(new PhutilURI($production_uri)) ->getDomain(); $allowed_domains = array( '', $request_domain, $production_domain, ); $allowed_domains = array_fuse($allowed_domains); $item_domain = $item_uri->getDomain(); $item_domain = (string)$item_domain; if (isset($allowed_domains[$item_domain])) { return true; } return false; } } diff --git a/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php index bf4c61f1e5..d85b02b3a2 100644 --- a/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php @@ -1,160 +1,160 @@ getOptions(); $name = idx($options, $config->getMenuItemProperty('source')); if ($name !== null) { return pht('Motivator: %s', $name); } else { return pht('Motivator'); } } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorInstructionsEditField()) ->setValue( pht( 'Motivate your team with inspirational quotes from great minds. '. 'This menu item shows a new quote every day.')), id(new PhabricatorSelectEditField()) ->setKey('source') ->setLabel(pht('Source')) ->setOptions($this->getOptions()), ); } private function getOptions() { return array( 'catfacts' => pht('Cat Facts'), ); } protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $source = $config->getMenuItemProperty('source'); switch ($source) { case 'catfacts': default: $facts = $this->getCatFacts(); $fact_name = pht('Cat Facts'); $fact_icon = 'fa-paw'; break; } $fact_text = $this->selectFact($facts); $item = $this->newItemView() ->setName($fact_name) ->setIcon($fact_icon) ->setTooltip($fact_text) - ->setHref('#'); + ->setURI('#'); return array( $item, ); } private function getCatFacts() { return array( pht('Cats purr when they are happy, upset, or asleep.'), pht('The first cats evolved on the savannah about 8,000 years ago.'), pht( 'Cats have a tail, two feet, between one and three ears, and two '. 'other feet.'), pht('Cats use their keen sense of smell to avoid feeling empathy.'), pht('The first cats evolved in swamps about 65 years ago.'), pht( 'You can tell how warm a cat is by examining the coloration: cooler '. 'areas are darker.'), pht( 'Cat tails are flexible because they contain thousands of tiny '. 'bones.'), pht( 'A cattail is a wetland plant with an appearance that resembles '. 'the tail of a cat.'), pht( 'Cats must eat a diet rich in fish to replace the tiny bones in '. 'their tails.'), pht('Cats are stealthy predators and nearly invisible to radar.'), pht( 'Cats use a special type of magnetism to help them land on their '. 'feet.'), pht( 'A cat can run seven times faster than a human, but only for a '. 'short distance.'), pht( 'The largest recorded cat was nearly 11 inches long from nose to '. 'tail.'), pht( 'Not all cats can retract their claws, but most of them can.'), pht( 'In the wild, cats and raccoons sometimes hunt together in packs.'), pht( 'The Spanish word for cat is "cato". The biggest cat is called '. '"el cato".'), pht( 'The Japanese word for cat is "kome", which is also the word for '. 'rice. Japanese cats love to eat rice, so the two are synonymous.'), pht('Cats have five pointy ends.'), pht('cat -A can find mice hiding in files.'), pht('A cat\'s visual, olfactory, and auditory senses, '. 'Contribute to their hunting skills and natural defenses.'), pht( 'Cats with high self-esteem seek out high perches '. 'to launch their attacks. Watch out!'), pht('Cats prefer vanilla ice cream.'), pht('Taco cat spelled backwards is taco cat.'), pht( 'Cats will often bring you their prey because they feel sorry '. 'for your inability to hunt.'), pht('Cats spend most of their time plotting to kill their owner.'), pht('Outside of the CAT scan, cats have made almost no contributions '. 'to modern medicine.'), pht('In ancient Egypt, the cat-god Horus watched over all cats.'), pht('The word "catastrophe" has no etymological relationship to the '. 'word "cat".'), pht('Many cats appear black in low light, suffering a -2 modifier to '. 'luck rolls.'), pht('The popular trivia game "World of Warcraft" features a race of '. 'cat people called the Khajiit.'), ); } private function selectFact(array $facts) { // This is a simple pseudorandom number generator that avoids touching // srand(), because it would seed it to a highly predictable value. It // selects a new fact every day. $seed = ((int)date('Y') * 366) + (int)date('z'); for ($ii = 0; $ii < 32; $ii++) { $seed = ((1664525 * $seed) + 1013904223) % (1 << 31); } return $facts[$seed % count($facts)]; } } diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index 84736d0990..8ca948cb07 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -1,303 +1,321 @@ setVisibility(self::VISIBILITY_VISIBLE); } public static function initializeNewItem( $profile_object, PhabricatorProfileMenuItem $item, $custom_phid) { return self::initializeNewBuiltin() ->setProfilePHID($profile_object->getPHID()) ->setMenuItemKey($item->getMenuItemKey()) ->attachMenuItem($item) ->attachProfileObject($profile_object) ->setCustomPHID($custom_phid); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'menuItemProperties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'menuItemKey' => 'text64', 'builtinKey' => 'text64?', 'menuItemOrder' => 'uint32?', 'customPHID' => 'phid?', 'visibility' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_profile' => array( 'columns' => array('profilePHID', 'menuItemOrder'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProfileMenuItemPHIDType::TYPECONST); } public function attachMenuItem(PhabricatorProfileMenuItem $item) { $this->menuItem = $item; return $this; } public function getMenuItem() { return $this->assertAttached($this->menuItem); } public function attachProfileObject($profile_object) { $this->profileObject = $profile_object; return $this; } public function getProfileObject() { return $this->assertAttached($this->profileObject); } public function setMenuItemProperty($key, $value) { $this->menuItemProperties[$key] = $value; return $this; } public function getMenuItemProperty($key, $default = null) { return idx($this->menuItemProperties, $key, $default); } public function getMenuItemTypeName() { return $this->getMenuItem()->getMenuItemTypeName(); } public function getDisplayName() { return $this->getMenuItem()->getDisplayName($this); } public function canMakeDefault() { return $this->getMenuItem()->canMakeDefault($this); } public function canHideMenuItem() { return $this->getMenuItem()->canHideMenuItem($this); } public function shouldEnableForObject($object) { return $this->getMenuItem()->shouldEnableForObject($object); } public function willGetMenuItemViewList(array $items) { return $this->getMenuItem()->willGetMenuItemViewList($items); } public function getMenuItemViewList() { return $this->getMenuItem()->getMenuItemViewList($this); } public function validateTransactions(array $map) { $item = $this->getMenuItem(); $fields = $item->buildEditEngineFields($this); $errors = array(); foreach ($fields as $field) { $field_key = $field->getKey(); $xactions = idx($map, $field_key, array()); $value = $this->getMenuItemProperty($field_key); $field_errors = $item->validateTransactions( $this, $field_key, $value, $xactions); foreach ($field_errors as $error) { $errors[] = $error; } } return $errors; } public function getSortVector() { // Sort custom items above global items. if ($this->getCustomPHID()) { $is_global = 0; } else { $is_global = 1; } // Sort "head" items above other items and "tail" items after other items. if ($this->getIsHeadItem()) { $force_position = 0; } else if ($this->getIsTailItem()) { $force_position = 2; } else { $force_position = 1; } // Sort items with an explicit order above items without an explicit order, // so any newly created builtins go to the bottom. $order = $this->getMenuItemOrder(); if ($order !== null) { $has_order = 0; } else { $has_order = 1; } return id(new PhutilSortVector()) ->addInt($is_global) ->addInt($force_position) ->addInt($has_order) ->addInt((int)$order) ->addInt((int)$this->getID()); } public function isDisabled() { if (!$this->canHideMenuItem()) { return false; } return ($this->getVisibility() === self::VISIBILITY_DISABLED); } public function isDefault() { return ($this->getVisibility() === self::VISIBILITY_DEFAULT); } public function getItemIdentifier() { $id = $this->getID(); if ($id) { return (int)$id; } return $this->getBuiltinKey(); } public function getDefaultMenuItemKey() { if ($this->getBuiltinKey()) { return $this->getBuiltinKey(); } return $this->getPHID(); } public function newPageContent() { return $this->getMenuItem()->newPageContent($this); } public function setIsHeadItem($is_head_item) { $this->isHeadItem = $is_head_item; return $this; } public function getIsHeadItem() { return $this->isHeadItem; } public function setIsTailItem($is_tail_item) { $this->isTailItem = $is_tail_item; return $this; } public function getIsTailItem() { return $this->isTailItem; } + public function matchesIdentifier($identifier) { + if (!strlen($identifier)) { + return false; + } + + if (ctype_digit($identifier)) { + if ((int)$this->getID() === (int)$identifier) { + return true; + } + } + + if ((string)$this->getBuiltinKey() === (string)$identifier) { + return true; + } + + return false; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProfileObject()->hasAutomaticCapability( $capability, $viewer); } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { // If this is an item with a custom PHID (like a personal menu item), // we only require that the user can edit the corresponding custom // object (usually their own user profile), not the object that the // menu appears on (which may be an Application like Favorites or Home). if ($capability == PhabricatorPolicyCapability::CAN_EDIT) { if ($this->getCustomPHID()) { return array( array( $this->getCustomPHID(), $capability, ), ); } } return array( array( $this->getProfileObject(), $capability, ), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProfileMenuEditor(); } public function getApplicationTransactionTemplate() { return new PhabricatorProfileMenuItemConfigurationTransaction(); } }