Changeset View
Changeset View
Standalone View
Standalone View
src/applications/search/engine/PhabricatorProfileMenuEngine.php
Show First 20 Lines • Show All 65 Lines • ▼ Show 20 Lines | public function setController(PhabricatorController $controller) { | ||||
$this->controller = $controller; | $this->controller = $controller; | ||||
return $this; | return $this; | ||||
} | } | ||||
public function getController() { | public function getController() { | ||||
return $this->controller; | return $this->controller; | ||||
} | } | ||||
private function setDefaultItem( | |||||
PhabricatorProfileMenuItemConfiguration $default_item) { | |||||
$this->defaultItem = $default_item; | |||||
return $this; | |||||
} | |||||
public function getDefaultItem() { | |||||
return $this->pickDefaultItem($this->getItems()); | |||||
} | |||||
public function setShowNavigation($show) { | public function setShowNavigation($show) { | ||||
$this->showNavigation = $show; | $this->showNavigation = $show; | ||||
return $this; | return $this; | ||||
} | } | ||||
public function getShowNavigation() { | public function getShowNavigation() { | ||||
return $this->showNavigation; | return $this->showNavigation; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 53 Lines • ▼ Show 20 Lines | public function buildResponse() { | ||||
$item_id = $request->getURIData('itemID'); | $item_id = $request->getURIData('itemID'); | ||||
// If we miss on the MenuEngine route, try the EditEngine route. This will | // If we miss on the MenuEngine route, try the EditEngine route. This will | ||||
// be populated while editing items. | // be populated while editing items. | ||||
if (!$item_id) { | if (!$item_id) { | ||||
$item_id = $request->getURIData('id'); | $item_id = $request->getURIData('id'); | ||||
} | } | ||||
$item_list = $this->getItems(); | $view_list = $this->newProfileMenuItemViewList(); | ||||
$selected_item = $this->pickSelectedItem( | $selected_item = $this->selectItem( | ||||
$item_list, | $view_list, | ||||
$item_id, | $item_id, | ||||
$is_view); | $is_view); | ||||
switch ($item_action) { | switch ($item_action) { | ||||
case 'view': | case 'view': | ||||
// If we were not able to select an item, we're still going to render | // 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 | // a page state. For example, this happens when you create a new | ||||
// portal for the first time. | // portal for the first time. | ||||
Show All 13 Lines | switch ($item_action) { | ||||
// of thin air. For menus, new items are created via the "new" | // of thin air. For menus, new items are created via the "new" | ||||
// action. Just catch this case and 404 early since there's currently | // action. Just catch this case and 404 early since there's currently | ||||
// no clean way to make EditEngine aware of this. | // no clean way to make EditEngine aware of this. | ||||
return new Aphront404Response(); | return new Aphront404Response(); | ||||
} | } | ||||
break; | break; | ||||
} | } | ||||
$navigation = $this->buildNavigation($selected_item); | $navigation = $view_list->newNavigationView(); | ||||
$crumbs = $controller->buildApplicationCrumbsForEditEngine(); | $crumbs = $controller->buildApplicationCrumbsForEditEngine(); | ||||
if (!$is_view) { | if (!$is_view) { | ||||
$navigation->selectFilter(self::ITEM_MANAGE); | $navigation->selectFilter(self::ITEM_MANAGE); | ||||
if ($selected_item) { | if ($selected_item) { | ||||
if ($selected_item->getCustomPHID()) { | if ($selected_item->getCustomPHID()) { | ||||
$edit_mode = 'custom'; | $edit_mode = 'custom'; | ||||
▲ Show 20 Lines • Show All 87 Lines • ▼ Show 20 Lines | switch ($item_action) { | ||||
break; | break; | ||||
case 'hide': | case 'hide': | ||||
$content = $this->buildItemHideContent($selected_item); | $content = $this->buildItemHideContent($selected_item); | ||||
break; | break; | ||||
case 'default': | case 'default': | ||||
if (!$this->isMenuEnginePinnable()) { | if (!$this->isMenuEnginePinnable()) { | ||||
return new Aphront404Response(); | return new Aphront404Response(); | ||||
} | } | ||||
$content = $this->buildItemDefaultContent( | $content = $this->buildItemDefaultContent($selected_item); | ||||
$selected_item, | |||||
$item_list); | |||||
break; | break; | ||||
case 'edit': | case 'edit': | ||||
$content = $this->buildItemEditContent(); | $content = $this->buildItemEditContent(); | ||||
break; | break; | ||||
default: | default: | ||||
throw new Exception( | throw new Exception( | ||||
pht( | pht( | ||||
'Unsupported item action "%s".', | 'Unsupported item action "%s".', | ||||
Show All 26 Lines | if ($is_view) { | ||||
foreach ($this->pageClasses as $class) { | foreach ($this->pageClasses as $class) { | ||||
$page->addClass($class); | $page->addClass($class); | ||||
} | } | ||||
} | } | ||||
return $page; | return $page; | ||||
} | } | ||||
public function buildNavigation( | |||||
PhabricatorProfileMenuItemConfiguration $selected_item = null) { | |||||
if ($this->navigation) { | |||||
return $this->navigation; | |||||
} | |||||
$nav = id(new AphrontSideNavFilterView()) | |||||
->setIsProfileMenu(true) | |||||
->setBaseURI(new PhutilURI($this->getItemURI(''))); | |||||
$menu_items = $this->getItems(); | |||||
$filtered_items = array(); | |||||
foreach ($menu_items as $menu_item) { | |||||
if ($menu_item->isDisabled()) { | |||||
continue; | |||||
} | |||||
$filtered_items[] = $menu_item; | |||||
} | |||||
$filtered_groups = mgroup($filtered_items, 'getMenuItemKey'); | |||||
foreach ($filtered_groups as $group) { | |||||
$first_item = head($group); | |||||
$first_item->willBuildNavigationItems($group); | |||||
} | |||||
$has_items = false; | |||||
foreach ($menu_items as $menu_item) { | |||||
if ($menu_item->isDisabled()) { | |||||
continue; | |||||
} | |||||
$items = $menu_item->buildNavigationMenuItems(); | |||||
foreach ($items as $item) { | |||||
$this->validateNavigationMenuItem($item); | |||||
} | |||||
// If the item produced only a single item which does not otherwise | |||||
// have a key, try to automatically assign it a reasonable key. This | |||||
// makes selecting the correct item simpler. | |||||
if (count($items) == 1) { | |||||
$item = head($items); | |||||
if ($item->getKey() === null) { | |||||
$default_key = $menu_item->getDefaultMenuItemKey(); | |||||
$item->setKey($default_key); | |||||
} | |||||
} | |||||
foreach ($items as $item) { | |||||
$nav->addMenuItem($item); | |||||
$has_items = true; | |||||
} | |||||
} | |||||
if (!$has_items) { | |||||
// 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); | |||||
$nav->addMenuItem($empty_item); | |||||
} | |||||
$nav->selectFilter(null); | |||||
$navigation_items = $nav->getMenu()->getItems(); | |||||
$select_key = $this->pickHighlightedMenuItem( | |||||
$navigation_items, | |||||
$selected_item); | |||||
$nav->selectFilter($select_key); | |||||
$this->navigation = $nav; | |||||
return $this->navigation; | |||||
} | |||||
private function getItems() { | private function getItems() { | ||||
if ($this->items === null) { | if ($this->items === null) { | ||||
$this->items = $this->loadItems(self::MODE_COMBINED); | $this->items = $this->loadItems(self::MODE_COMBINED); | ||||
} | } | ||||
return $this->items; | return $this->items; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 292 Lines • ▼ Show 20 Lines | abstract class PhabricatorProfileMenuEngine extends Phobject { | ||||
/** | /** | ||||
* Does this engine support pinning items? | * Does this engine support pinning items? | ||||
* | * | ||||
* Personalizable menus disable pinning by default since it creates a number | * Personalizable menus disable pinning by default since it creates a number | ||||
* of weird edge cases without providing many benefits for current menus. | * of weird edge cases without providing many benefits for current menus. | ||||
* | * | ||||
* @return bool True if items may be pinned as default items. | * @return bool True if items may be pinned as default items. | ||||
*/ | */ | ||||
protected function isMenuEnginePinnable() { | public function isMenuEnginePinnable() { | ||||
return !$this->isMenuEnginePersonalizable(); | return !$this->isMenuEnginePersonalizable(); | ||||
} | } | ||||
private function buildMenuEditModeContent() { | private function buildMenuEditModeContent() { | ||||
$viewer = $this->getViewer(); | $viewer = $this->getViewer(); | ||||
$modes = $this->getViewerEditModes(); | $modes = $this->getViewerEditModes(); | ||||
if (!$modes) { | if (!$modes) { | ||||
▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | abstract class PhabricatorProfileMenuEngine extends Phobject { | ||||
private function buildItemConfigureContent(array $items) { | private function buildItemConfigureContent(array $items) { | ||||
$viewer = $this->getViewer(); | $viewer = $this->getViewer(); | ||||
$object = $this->getProfileObject(); | $object = $this->getProfileObject(); | ||||
$filtered_groups = mgroup($items, 'getMenuItemKey'); | $filtered_groups = mgroup($items, 'getMenuItemKey'); | ||||
foreach ($filtered_groups as $group) { | foreach ($filtered_groups as $group) { | ||||
$first_item = head($group); | $first_item = head($group); | ||||
$first_item->willBuildNavigationItems($group); | $first_item->willGetMenuItemViewList($group); | ||||
} | } | ||||
// Users only need to be able to edit the object which this menu appears | // 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 | // 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 | // to be able to edit the Favorites application to add new items to the | ||||
// Favorites menu. | // Favorites menu. | ||||
if (!$this->getCustomPHID()) { | if (!$this->getCustomPHID()) { | ||||
PhabricatorPolicyFilter::requireCapability( | PhabricatorPolicyFilter::requireCapability( | ||||
▲ Show 20 Lines • Show All 357 Lines • ▼ Show 20 Lines | private function buildItemHideContent( | ||||
return $controller->newDialog() | return $controller->newDialog() | ||||
->setTitle($title) | ->setTitle($title) | ||||
->appendParagraph($body) | ->appendParagraph($body) | ||||
->addCancelButton($this->getConfigureURI()) | ->addCancelButton($this->getConfigureURI()) | ||||
->addSubmitButton($button); | ->addSubmitButton($button); | ||||
} | } | ||||
private function buildItemDefaultContent( | private function buildItemDefaultContent( | ||||
PhabricatorProfileMenuItemConfiguration $configuration, | PhabricatorProfileMenuItemConfiguration $configuration) { | ||||
array $items) { | |||||
$controller = $this->getController(); | $controller = $this->getController(); | ||||
$request = $controller->getRequest(); | $request = $controller->getRequest(); | ||||
$viewer = $this->getViewer(); | $viewer = $this->getViewer(); | ||||
PhabricatorPolicyFilter::requireCapability( | PhabricatorPolicyFilter::requireCapability( | ||||
$viewer, | $viewer, | ||||
$configuration, | $configuration, | ||||
▲ Show 20 Lines • Show All 49 Lines • ▼ Show 20 Lines | |||||
protected function newManageItem() { | protected function newManageItem() { | ||||
return $this->newItem() | return $this->newItem() | ||||
->setBuiltinKey(self::ITEM_MANAGE) | ->setBuiltinKey(self::ITEM_MANAGE) | ||||
->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY) | ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY) | ||||
->setIsTailItem(true); | ->setIsTailItem(true); | ||||
} | } | ||||
public function getDefaultMenuItemConfiguration() { | |||||
$configs = $this->getItems(); | |||||
foreach ($configs as $config) { | |||||
if ($config->isDefault()) { | |||||
return $config; | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
public function adjustDefault($key) { | public function adjustDefault($key) { | ||||
$controller = $this->getController(); | $controller = $this->getController(); | ||||
$request = $controller->getRequest(); | $request = $controller->getRequest(); | ||||
$viewer = $request->getViewer(); | $viewer = $request->getViewer(); | ||||
$items = $this->loadItems(self::MODE_COMBINED); | $items = $this->loadItems(self::MODE_COMBINED); | ||||
// To adjust the default item, we first change any existing items that | // To adjust the default item, we first change any existing items that | ||||
▲ Show 20 Lines • Show All 104 Lines • ▼ Show 20 Lines | |||||
} | } | ||||
protected function newNoMenuItemsView() { | protected function newNoMenuItemsView() { | ||||
return $this->newEmptyView( | return $this->newEmptyView( | ||||
pht('No Menu Items'), | pht('No Menu Items'), | ||||
pht('There are no menu items.')); | pht('There are no menu items.')); | ||||
} | } | ||||
private function pickDefaultItem(array $items) { | |||||
// Remove all the items which can not be the default item. | |||||
foreach ($items as $key => $item) { | |||||
if (!$item->canMakeDefault()) { | |||||
unset($items[$key]); | |||||
continue; | |||||
} | |||||
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()) { | if ($item->isDisabled()) { | ||||
unset($items[$key]); | unset($items[$key]); | ||||
continue; | continue; | ||||
} | } | ||||
} | } | ||||
// If this engine supports pinning items and a valid item is pinned, | // Give each item group a callback so it can load data it needs to render | ||||
// pick that item as the default. | // views. | ||||
if ($this->isMenuEnginePinnable()) { | $groups = mgroup($items, 'getMenuItemKey'); | ||||
foreach ($items as $key => $item) { | foreach ($groups as $group) { | ||||
if ($item->isDefault()) { | $item = head($group); | ||||
return $item; | $item->willGetMenuItemViewList($group); | ||||
} | |||||
} | |||||
} | |||||
// If we have some other valid items, pick the first one as the default. | |||||
if ($items) { | |||||
return head($items); | |||||
} | } | ||||
return null; | $view_list = id(new PhabricatorProfileMenuItemViewList()) | ||||
} | ->setProfileMenuEngine($this); | ||||
private function pickSelectedItem(array $items, $item_id, $is_view) { | |||||
if (strlen($item_id)) { | |||||
$item_id_int = (int)$item_id; | |||||
foreach ($items as $item) { | foreach ($items as $item) { | ||||
if ($item_id_int) { | $views = $item->getMenuItemViewList(); | ||||
if ((int)$item->getID() === $item_id_int) { | foreach ($views as $view) { | ||||
return $item; | $view_list->addItemView($view); | ||||
} | |||||
} | |||||
$builtin_key = $item->getBuiltinKey(); | |||||
if ($builtin_key === (string)$item_id) { | |||||
return $item; | |||||
} | |||||
} | } | ||||
// Nothing matches the selected item ID, so we don't have a valid | |||||
// selection. | |||||
return null; | |||||
} | |||||
if ($is_view) { | |||||
return $this->pickDefaultItem($items); | |||||
} | |||||
return null; | |||||
} | |||||
private function pickHighlightedMenuItem( | |||||
array $items, | |||||
PhabricatorProfileMenuItemConfiguration $selected_item = null) { | |||||
assert_instances_of($items, 'PHUIListItemView'); | |||||
$default_key = null; | |||||
if ($selected_item) { | |||||
$default_key = $selected_item->getDefaultMenuItemKey(); | |||||
} | } | ||||
$controller = $this->getController(); | return $view_list; | ||||
// 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(); | private function selectItem( | ||||
PhabricatorProfileMenuItemViewList $view_list, | |||||
$item_id, | |||||
$want_default) { | |||||
// See T12949. If one of the menu items is a link to the same URI that | // Figure out which view's content we're going to render. In most cases, | ||||
// the page was accessed with, we want to highlight that item. For example, | // the URI tells us. If we don't have an identifier in the URI, we'll | ||||
// this allows you to add links to a menu that apply filters to a | // render the default view instead if this is a workflow that falls back | ||||
// workboard. | // to default rendering. | ||||
$matches = array(); | $selected_view = null; | ||||
foreach ($items as $item) { | if (strlen($item_id)) { | ||||
$href = $item->getHref(); | $item_views = $view_list->getViewsWithItemIdentifier($item_id); | ||||
if ($this->isMatchForRequestURI($request, $href)) { | if ($item_views) { | ||||
$matches[] = $item; | $selected_view = head($item_views); | ||||
} | |||||
} | |||||
foreach ($matches as $match) { | |||||
if ($match->getKey() === $default_key) { | |||||
return $default_key; | |||||
} | } | ||||
} else { | |||||
if ($want_default) { | |||||
$default_views = $view_list->getDefaultViews(); | |||||
if ($default_views) { | |||||
$selected_view = head($default_views); | |||||
} | } | ||||
if ($matches) { | |||||
return head($matches)->getKey(); | |||||
} | } | ||||
return $default_key; | |||||
} | } | ||||
private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { | if ($selected_view) { | ||||
$request_uri = $request->getAbsoluteRequestURI(); | $view_list->setSelectedView($selected_view); | ||||
$item_uri = new PhutilURI($item_uri); | $selected_item = $selected_view->getMenuItemConfiguration(); | ||||
} else { | |||||
// If the request URI and item URI don't have matching paths, they | $selected_item = null; | ||||
// 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 | return $selected_item; | ||||
// 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; | |||||
} | |||||
} | } |