diff --git a/src/applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php b/src/applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php index 3c86d19a58..87b59e3e0f 100644 --- a/src/applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php +++ b/src/applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php @@ -1,48 +1,48 @@ getProfileObject(); $custom = $this->getCustomPHID(); if ($custom) { return "/favorites/personal/item/{$path}"; } else { return "/favorites/global/item/{$path}"; } } protected function getBuiltinProfileItems($object) { $items = array(); $viewer = $this->getViewer(); $engines = PhabricatorEditEngine::getAllEditEngines(); $engines = msortv($engines, 'getQuickCreateOrderVector'); foreach ($engines as $engine) { foreach ($engine->getDefaultQuickCreateFormKeys() as $form_key) { $form_hash = PhabricatorHash::digestForIndex($form_key); $builtin_key = "editengine.form({$form_hash})"; $properties = array( 'name' => null, 'formKey' => $form_key, ); $items[] = $this->newItem() ->setBuiltinKey($builtin_key) ->setMenuItemKey(PhabricatorEditEngineProfileMenuItem::MENUITEMKEY) ->setMenuItemProperties($properties); } } return $items; } } diff --git a/src/applications/home/engine/PhabricatorHomeProfileMenuEngine.php b/src/applications/home/engine/PhabricatorHomeProfileMenuEngine.php index ebcc92b189..528edba697 100644 --- a/src/applications/home/engine/PhabricatorHomeProfileMenuEngine.php +++ b/src/applications/home/engine/PhabricatorHomeProfileMenuEngine.php @@ -1,58 +1,58 @@ getProfileObject(); $custom = $this->getCustomPHID(); if ($custom) { return "/home/menu/personal/item/{$path}"; } else { return "/home/menu/global/item/{$path}"; } } protected function getBuiltinProfileItems($object) { $viewer = $this->getViewer(); $items = array(); $custom_phid = $this->getCustomPHID(); $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withUnlisted(false) ->withLaunchable(true) ->execute(); foreach ($applications as $application) { if (!$application->isPinnedByDefault($viewer)) { continue; } $properties = array( 'name' => $application->getName(), 'application' => $application->getPHID(), ); $items[] = $this->newItem() ->setBuiltinKey($application->getPHID()) ->setMenuItemKey(PhabricatorApplicationProfileMenuItem::MENUITEMKEY) ->setMenuItemProperties($properties); } // Single Manage Item, switches URI based on admin/user $items[] = $this->newItem() ->setBuiltinKey(PhabricatorHomeConstants::ITEM_MANAGE) ->setMenuItemKey( PhabricatorHomeManageProfileMenuItem::MENUITEMKEY); return $items; } } diff --git a/src/applications/people/engine/PhabricatorPeopleProfileMenuEngine.php b/src/applications/people/engine/PhabricatorPeopleProfileMenuEngine.php index 9965147a12..3d67872772 100644 --- a/src/applications/people/engine/PhabricatorPeopleProfileMenuEngine.php +++ b/src/applications/people/engine/PhabricatorPeopleProfileMenuEngine.php @@ -1,84 +1,84 @@ getProfileObject(); $username = $user->getUsername(); $username = phutil_escape_uri($username); return "/p/{$username}/item/{$path}"; } protected function getBuiltinProfileItems($object) { $viewer = $this->getViewer(); $items = array(); $items[] = $this->newItem() ->setBuiltinKey(self::ITEM_PROFILE) ->setMenuItemKey(PhabricatorPeopleDetailsProfileMenuItem::MENUITEMKEY); $have_maniphest = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorManiphestApplication', $viewer); if ($have_maniphest) { $uri = urisprintf( '/maniphest/?statuses=open()&assigned=%s#R', $object->getPHID()); $items[] = $this->newItem() ->setBuiltinKey('tasks') ->setMenuItemKey(PhabricatorLinkProfileMenuItem::MENUITEMKEY) ->setMenuItemProperty('icon', 'maniphest') ->setMenuItemProperty('name', pht('Open Tasks')) ->setMenuItemProperty('uri', $uri); } $have_differential = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDifferentialApplication', $viewer); if ($have_differential) { $uri = urisprintf( '/differential/?authors=%s#R', $object->getPHID()); $items[] = $this->newItem() ->setBuiltinKey('revisions') ->setMenuItemKey(PhabricatorLinkProfileMenuItem::MENUITEMKEY) ->setMenuItemProperty('icon', 'differential') ->setMenuItemProperty('name', pht('Revisions')) ->setMenuItemProperty('uri', $uri); } $have_diffusion = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDiffusionApplication', $viewer); if ($have_diffusion) { $uri = urisprintf( '/diffusion/commit/?authors=%s#R', $object->getPHID()); $items[] = $this->newItem() ->setBuiltinKey('commits') ->setMenuItemKey(PhabricatorLinkProfileMenuItem::MENUITEMKEY) ->setMenuItemProperty('icon', 'diffusion') ->setMenuItemProperty('name', pht('Commits')) ->setMenuItemProperty('uri', $uri); } $items[] = $this->newItem() ->setBuiltinKey(self::ITEM_MANAGE) ->setMenuItemKey(PhabricatorPeopleManageProfileMenuItem::MENUITEMKEY); return $items; } } diff --git a/src/applications/project/controller/PhabricatorProjectViewController.php b/src/applications/project/controller/PhabricatorProjectViewController.php index d2f8d4f58d..7d5dc37e0b 100644 --- a/src/applications/project/controller/PhabricatorProjectViewController.php +++ b/src/applications/project/controller/PhabricatorProjectViewController.php @@ -1,36 +1,37 @@ getRequest(); $viewer = $request->getViewer(); $response = $this->loadProject(); if ($response) { return $response; } $project = $this->getProject(); $engine = $this->getProfileMenuEngine(); $default = $engine->getDefaultItem(); switch ($default->getBuiltinKey()) { case PhabricatorProject::ITEM_WORKBOARD: $controller_object = new PhabricatorProjectBoardViewController(); break; case PhabricatorProject::ITEM_PROFILE: - default: $controller_object = new PhabricatorProjectProfileController(); break; + default: + return $engine->buildResponse(); } return $this->delegateToController($controller_object); } } diff --git a/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php b/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php index d3ea39e1e2..a39e7a8236 100644 --- a/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php @@ -1,47 +1,47 @@ getProfileObject(); $id = $project->getID(); return "/project/{$id}/item/{$path}"; } protected function getBuiltinProfileItems($object) { $items = array(); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_PROFILE) ->setMenuItemKey(PhabricatorProjectDetailsProfileMenuItem::MENUITEMKEY); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_POINTS) ->setMenuItemKey(PhabricatorProjectPointsProfileMenuItem::MENUITEMKEY); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_WORKBOARD) ->setMenuItemKey(PhabricatorProjectWorkboardProfileMenuItem::MENUITEMKEY); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_MEMBERS) ->setMenuItemKey(PhabricatorProjectMembersProfileMenuItem::MENUITEMKEY); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_SUBPROJECTS) ->setMenuItemKey( PhabricatorProjectSubprojectsProfileMenuItem::MENUITEMKEY); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_MANAGE) ->setMenuItemKey(PhabricatorProjectManageProfileMenuItem::MENUITEMKEY); return $items; } } diff --git a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php index 1bd7e796dc..ddb59ec095 100644 --- a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php @@ -1,69 +1,64 @@ getMenuItemProperty('name'); if (strlen($name)) { return $name; } return $this->getDefaultName(); } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setPlaceholder($this->getDefaultName()) ->setValue($config->getMenuItemProperty('name')), ); } protected function newNavigationMenuItems( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); $id = $project->getID(); $name = $this->getDisplayName($config); $icon = 'fa-gears'; $href = "/project/manage/{$id}/"; $item = $this->newItem() ->setHref($href) ->setName($name) ->setIcon($icon); return array( $item, ); } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index 3e11436ced..a6db1b2d2b 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -1,1067 +1,1090 @@ 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; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } private function setDefaultItem( PhabricatorProfileMenuItemConfiguration $default_item) { $this->defaultItem = $default_item; return $this; } public function getDefaultItem() { $this->loadItems(); return $this->defaultItem; } public function setMenuType($type) { $this->menuType = $type; return $this; } private function getMenuType() { return $this->menuType; } public function setShowNavigation($show) { $this->showNavigation = $show; return $this; } public function getShowNavigation() { return $this->showNavigation; } - abstract protected function getItemURI($path); + abstract public function getItemURI($path); abstract protected function isMenuEngineConfigurable(); abstract protected function getBuiltinProfileItems($object); protected function getBuiltinCustomProfileItems( $object, $custom_phid) { return array(); } 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'; + } // If the engine is not configurable, don't respond to any of the editing // or configuration routes. if (!$this->isMenuEngineConfigurable()) { switch ($item_action) { case 'view': break; default: return new Aphront404Response(); } } $item_id = $request->getURIData('itemID'); $item_list = $this->getItems(); $selected_item = null; if (strlen($item_id)) { $item_id_int = (int)$item_id; foreach ($item_list as $item) { if ($item_id_int) { if ((int)$item->getID() === $item_id_int) { $selected_item = $item; break; } } $builtin_key = $item->getBuiltinKey(); if ($builtin_key === (string)$item_id) { $selected_item = $item; break; } } } + if (!$selected_item) { + if ($item_action == 'view') { + $selected_item = $this->getDefaultItem(); + } + } + switch ($item_action) { case 'view': 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 = $this->buildNavigation(); - $navigation->selectFilter('item.configure'); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); - switch ($this->getMenuType()) { - case 'personal': - $crumbs->addTextCrumb(pht('Personal')); - break; - case 'global': - $crumbs->addTextCrumb(pht('Global')); - break; + + // TODO: This stuff might need a little tweaking at some point, since it + // causes "Global" and "Personal" to show up in contexts where they don't + // make sense, notably Projects. + if ($item_action != 'view') { + $navigation->selectFilter('item.configure'); + switch ($this->getMenuType()) { + case 'personal': + $crumbs->addTextCrumb(pht('Personal')); + break; + case 'global': + $crumbs->addTextCrumb(pht('Global')); + break; + } } switch ($item_action) { case 'view': + $navigation->selectFilter($selected_item->getItemIdentifier()); + $content = $this->buildItemViewContent($selected_item); + $crumbs->addTextCrumb($selected_item->getDisplayName()); + if (!$content) { + return new Aphront404Response(); + } break; case 'configure': $content = $this->buildItemConfigureContent($item_list); $crumbs->addTextCrumb(pht('Configure Menu')); break; case 'reorder': $content = $this->buildItemReorderContent($item_list); break; case 'new': $item_key = $request->getURIData('itemKey'); $content = $this->buildItemNewContent($item_key); break; case 'builtin': $content = $this->buildItemBuiltinContent($selected_item); break; case 'hide': $content = $this->buildItemHideContent($selected_item); break; case 'default': $content = $this->buildItemDefaultContent( $selected_item, $item_list); 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(pht('Configure Menu')) ->setCrumbs($crumbs) ->appendChild($content); if ($this->getShowNavigation()) { $page->setNavigation($navigation); } + return $page; } public function buildNavigation() { 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); } 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) { - $builtin_key = $menu_item->getBuiltinKey(); - $item_phid = $menu_item->getPHID(); - if ($builtin_key !== null) { - $item->setKey($builtin_key); - } else if ($item_phid !== null) { - $item->setKey($item_phid); - } + $item_identifier = $menu_item->getItemIdentifier(); + $item->setKey($item_identifier); } } foreach ($items as $item) { $nav->addMenuItem($item); } } $nav->selectFilter(null); $this->navigation = $nav; return $this->navigation; } private function getItems() { if ($this->items === null) { $this->items = $this->loadItems(); } return $this->items; } private function loadItems() { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $items = $this->loadBuiltinProfileItems(); $query = id(new PhabricatorProfileMenuItemConfigurationQuery()) ->setViewer($viewer) ->withProfilePHIDs(array($object->getPHID())); $menu_type = $this->getMenuType(); switch ($menu_type) { case self::MENU_GLOBAL: $query->withCustomPHIDs(array(), true); break; case self::MENU_PERSONAL: $query->withCustomPHIDs(array($this->getCustomPHID()), false); break; case self::MENU_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])) { $items[$builtin_key] = $stored_item; } else { continue; } } else { $items[] = $stored_item; } } $items = $this->arrangeItems($items); // Make sure exactly one valid item is marked as default. $default = null; $first = null; foreach ($items as $item) { if (!$item->canMakeDefault()) { continue; } if ($item->isDefault()) { $default = $item; break; } if ($first === null) { $first = $item; } } if (!$default) { $default = $first; } if ($default) { $this->setDefaultItem($default); } return $items; } private function loadBuiltinProfileItems() { $object = $this->getProfileObject(); $menu_type = $this->getMenuType(); switch ($menu_type) { case self::MENU_GLOBAL: $builtins = $this->getBuiltinProfileItems($object); break; case self::MENU_PERSONAL: $builtins = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); break; case self::MENU_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() { return $this->getItemURI('configure/'); } private function buildItemReorderContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); PhabricatorPolicyFilter::requireCapability( $viewer, $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()); } + private function buildItemViewContent( + PhabricatorProfileMenuItemConfiguration $item) { + return $item->newPageContent(); + } 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->willBuildNavigationItems($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(); Javelin::initBehavior( 'reorder-profile-menu-items', array( 'listID' => $list_id, 'orderURI' => $this->getItemURI('reorder/'), )); $list = id(new PHUIObjectItemListView()) ->setID($list_id) ->setNoDataString(pht('This menu currently has no items.')); 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); if ($can_edit) { $view ->setGrippable(true) ->addSigil('profile-menu-item') ->setMetadata( array( 'key' => nonempty($id, $builtin_key), )); if ($id) { $default_uri = $this->getItemURI("default/{$id}/"); } else { $default_uri = $this->getItemURI("default/{$builtin_key}/"); } 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'); } else { $default_text = null; } 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); } $action_view = id(new PhabricatorActionListView()) ->setUser($viewer); $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $object = $this->getProfileObject(); $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); $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(); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon($item_type->getMenuItemTypeIcon()) ->setName($item_type->getMenuItemTypeName()) ->setHref($this->getItemURI("new/{$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'); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Current Menu Items')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); $panel = id(new PHUICurtainPanelView()) ->appendChild($action_view); $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) { $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->getCustomPHID(); $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(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setController($controller) ->setCustomPHID($this->getCustomPHID()) ->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(); return id(new PhabricatorProfileMenuEditEngine()) ->setIsBuiltin(true) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setController($controller) ->setCustomPHID($this->getCustomPHID()) ->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, array $items) { $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(); } public function adjustDefault($key) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $items = $this->loadItems(); // 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) { // Sort the items. $items = msortv($items, 'getSortVector'); // If we have some global items and some custom items and are in "combined" // mode, put a hard-coded divider item between them. if ($this->getMenuType() == self::MENU_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) ->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; } } diff --git a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php index c9a13bd4ab..002e5778d5 100644 --- a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php @@ -1,160 +1,190 @@ dashboard = $dashboard; return $this; } public function getDashboard() { $dashboard = $this->dashboard; + if (!$dashboard) { return null; } else if ($dashboard->isArchived()) { return null; } + return $dashboard; } + public function newPageContent( + PhabricatorProfileMenuItemConfiguration $config) { + $viewer = $this->getViewer(); + + $dashboard_phid = $config->getMenuItemProperty('dashboardPHID'); + + // Reload the dashboard to attach panels, which we need for rendering. + $dashboard = id(new PhabricatorDashboardQuery()) + ->setViewer($viewer) + ->withPHIDs(array($dashboard_phid)) + ->needPanels(true) + ->executeOne(); + if (!$dashboard) { + return null; + } + + $engine = id(new PhabricatorDashboardRenderingEngine()) + ->setViewer($viewer) + ->setDashboard($dashboard); + + return $engine->renderDashboard(); + } + public function willBuildNavigationItems(array $items) { $viewer = $this->getViewer(); $dashboard_phids = array(); foreach ($items as $item) { $dashboard_phids[] = $item->getMenuItemProperty('dashboardPHID'); } $dashboards = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withPHIDs($dashboard_phids) ->execute(); $dashboards = mpull($dashboards, null, 'getPHID'); foreach ($items as $item) { $dashboard_phid = $item->getMenuItemProperty('dashboardPHID'); $dashboard = idx($dashboards, $dashboard_phid, null); $item->getMenuItem()->attachDashboard($dashboard); } } public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $dashboard = $this->getDashboard(); if (!$dashboard) { return pht('(Restricted/Invalid Dashboard)'); } if (strlen($this->getName($config))) { return $this->getName($config); } else { return $dashboard->getName(); } } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorDatasourceEditField()) ->setKey(self::FIELD_DASHBOARD) ->setLabel(pht('Dashboard')) ->setIsRequired(true) ->setDatasource(new PhabricatorDashboardDatasource()) ->setSingleValue($config->getMenuItemProperty('dashboardPHID')), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setValue($this->getName($config)), ); } private function getName( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('name'); } protected function newNavigationMenuItems( PhabricatorProfileMenuItemConfiguration $config) { $dashboard = $this->getDashboard(); if (!$dashboard) { return array(); } $icon = $dashboard->getIcon(); $name = $this->getDisplayName($config); - $href = $dashboard->getViewURI(); + $href = $this->getItemViewURI($config); $item = $this->newItem() ->setHref($href) ->setName($name) ->setIcon($icon); return array( $item, ); } public function validateTransactions( PhabricatorProfileMenuItemConfiguration $config, $field_key, $value, array $xactions) { $viewer = $this->getViewer(); $errors = array(); if ($field_key == self::FIELD_DASHBOARD) { if ($this->isEmptyTransaction($value, $xactions)) { $errors[] = $this->newRequiredError( pht('You must choose a dashboard.'), $field_key); } foreach ($xactions as $xaction) { $new = $xaction['new']; if (!$new) { continue; } if ($new === $value) { continue; } $dashboards = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withPHIDs(array($new)) ->execute(); if (!$dashboards) { $errors[] = $this->newInvalidError( pht( 'Dashboard "%s" is not a valid dashboard which you have '. 'permission to see.', $new), $xaction['xaction']); } } } return $errors; } } diff --git a/src/applications/search/menuitem/PhabricatorProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorProfileMenuItem.php index 6f1342d4c0..061afc7fad 100644 --- a/src/applications/search/menuitem/PhabricatorProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorProfileMenuItem.php @@ -1,110 +1,134 @@ newNavigationMenuItems($config); } abstract protected function newNavigationMenuItems( PhabricatorProfileMenuItemConfiguration $config); public function willBuildNavigationItems(array $items) {} public function getMenuItemTypeIcon() { return null; } abstract public function getMenuItemTypeName(); abstract public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config); public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array(); } public function canAddToObject($object) { return false; } public function shouldEnableForObject($object) { return true; } public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return true; } public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return false; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } + public function setEngine(PhabricatorProfileMenuEngine $engine) { + $this->engine = $engine; + return $this; + } + + public function getEngine() { + return $this->engine; + } + final public function getMenuItemKey() { return $this->getPhobjectClassConstant('MENUITEMKEY'); } final public static function getAllMenuItems() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getMenuItemKey') ->execute(); } protected function newItem() { return new PHUIListItemView(); } + public function newPageContent( + PhabricatorProfileMenuItemConfiguration $config) { + return null; + } + + public function getItemViewURI( + PhabricatorProfileMenuItemConfiguration $config) { + + $engine = $this->getEngine(); + $key = $config->getItemIdentifier(); + + return $engine->getItemURI("view/{$key}/"); + } + public function validateTransactions( PhabricatorProfileMenuItemConfiguration $config, $field_key, $value, array $xactions) { return array(); } final protected function isEmptyTransaction($value, array $xactions) { $result = $value; foreach ($xactions as $xaction) { $result = $xaction['new']; } return !strlen($result); } final protected function newError($title, $message, $xaction = null) { return new PhabricatorApplicationTransactionValidationError( PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY, $title, $message, $xaction); } final protected function newRequiredError($message, $type) { $xaction = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setMetadataValue('property.key', $type); return $this->newError(pht('Required'), $message, $xaction) ->setIsMissingFieldError(true); } final protected function newInvalidError($message, $xaction = null) { return $this->newError(pht('Invalid'), $message, $xaction); } } diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index 107fa63c72..f178cc49fe 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -1,262 +1,275 @@ 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 buildNavigationMenuItems() { return $this->getMenuItem()->buildNavigationMenuItems($this); } 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 willBuildNavigationItems(array $items) { return $this->getMenuItem()->willBuildNavigationItems($items); } 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 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($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 newPageContent() { + return $this->getMenuItem()->newPageContent($this); + } /* -( 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 getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProfileMenuItemConfigurationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } }