diff --git a/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php b/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php index ce0bd98d8b..f010016d35 100644 --- a/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php +++ b/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php @@ -1,54 +1,59 @@ getPanelProperty('name'); if (strlen($name)) { return $name; } return $this->getDefaultName(); } public function buildEditEngineFields( PhabricatorProfilePanelConfiguration $config) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setPlaceholder($this->getDefaultName()) ->setValue($config->getPanelProperty('name')), ); } protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { $user = $config->getProfileObject(); $id = $user->getID(); $item = $this->newItem() ->setHref("/people/manage/{$id}/") ->setName($this->getDisplayName($config)) ->setIcon('fa-gears'); return array( $item, ); } } diff --git a/src/applications/project/profilepanel/PhabricatorProjectManageProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectManageProfilePanel.php index 16985890d4..ae66c4cc3e 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectManageProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectManageProfilePanel.php @@ -1,64 +1,69 @@ getPanelProperty('name'); if (strlen($name)) { return $name; } return $this->getDefaultName(); } public function buildEditEngineFields( PhabricatorProfilePanelConfiguration $config) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setPlaceholder($this->getDefaultName()) ->setValue($config->getPanelProperty('name')), ); } protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $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/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index 658cf5c266..6500676bd1 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -1,946 +1,957 @@ 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 setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } private function setDefaultPanel( PhabricatorProfilePanelConfiguration $default_panel) { $this->defaultPanel = $default_panel; return $this; } public function getDefaultPanel() { $this->loadPanels(); return $this->defaultPanel; } abstract protected function getPanelURI($path); protected function isPanelEngineConfigurable() { return PhabricatorEnv::getEnvConfig('phabricator.show-prototypes'); } public function buildResponse() { $controller = $this->getController(); $viewer = $controller->getViewer(); $this->setViewer($viewer); $request = $controller->getRequest(); $panel_action = $request->getURIData('panelAction'); // If the engine is not configurable, don't respond to any of the editing // or configuration routes. if (!$this->isPanelEngineConfigurable()) { switch ($panel_action) { case 'view': break; default: return new Aphront404Response(); } } $panel_id = $request->getURIData('panelID'); $panel_list = $this->loadPanels(); $selected_panel = null; if (strlen($panel_id)) { $panel_id_int = (int)$panel_id; foreach ($panel_list as $panel) { if ($panel_id_int) { if ((int)$panel->getID() === $panel_id_int) { $selected_panel = $panel; break; } } $builtin_key = $panel->getBuiltinKey(); if ($builtin_key === (string)$panel_id) { $selected_panel = $panel; break; } } } switch ($panel_action) { case 'view': case 'info': case 'hide': case 'default': case 'builtin': if (!$selected_panel) { return new Aphront404Response(); } break; } $navigation = $this->buildNavigation(); $navigation->selectFilter('panel.configure'); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); switch ($panel_action) { case 'view': $content = $this->buildPanelViewContent($selected_panel); break; case 'configure': $content = $this->buildPanelConfigureContent($panel_list); $crumbs->addTextCrumb(pht('Configure Menu')); break; case 'reorder': $content = $this->buildPanelReorderContent($panel_list); break; case 'new': $panel_key = $request->getURIData('panelKey'); $content = $this->buildPanelNewContent($panel_key); break; case 'builtin': $content = $this->buildPanelBuiltinContent($selected_panel); break; case 'hide': $content = $this->buildPanelHideContent($selected_panel); break; case 'default': $content = $this->buildPanelDefaultContent( $selected_panel, $panel_list); break; case 'edit': $content = $this->buildPanelEditContent(); break; default: throw new Exception( pht( 'Unsupported panel action "%s".', $panel_action)); } if ($content instanceof AphrontResponse) { return $content; } if ($content instanceof AphrontResponseProducerInterface) { return $content; } return $controller->newPage() ->setTitle(pht('Profile Stuff')) ->setNavigation($navigation) ->setCrumbs($crumbs) ->appendChild($content); } public function buildNavigation() { if ($this->navigation) { return $this->navigation; } $nav = id(new AphrontSideNavFilterView()) ->setIsProfileMenu(true) ->setBaseURI(new PhutilURI($this->getPanelURI(''))); $panels = $this->getPanels(); foreach ($panels as $panel) { if ($panel->isDisabled()) { continue; } $items = $panel->buildNavigationMenuItems(); foreach ($items as $item) { $this->validateNavigationMenuItem($item); } // If the panel 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 = $panel->getBuiltinKey(); $panel_phid = $panel->getPHID(); if ($builtin_key !== null) { $item->setKey($builtin_key); } else if ($panel_phid !== null) { $item->setKey($panel_phid); } } } foreach ($items as $item) { $nav->addMenuItem($item); } } $more_items = $this->newAutomaticMenuItems($nav); foreach ($more_items as $item) { $nav->addMenuItem($item); } $nav->selectFilter(null); $this->navigation = $nav; return $this->navigation; } private function getPanels() { if ($this->panels === null) { $this->panels = $this->loadPanels(); } return $this->panels; } private function loadPanels() { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $panels = $this->loadBuiltinProfilePanels(); $stored_panels = id(new PhabricatorProfilePanelConfigurationQuery()) ->setViewer($viewer) ->withProfilePHIDs(array($object->getPHID())) ->execute(); // Merge the stored panels into the builtin panels. If a builtin panel has // a stored version, replace the defaults with the stored changes. foreach ($stored_panels as $stored_panel) { $builtin_key = $stored_panel->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 // panel. if (isset($panels[$builtin_key])) { $panels[$builtin_key] = $stored_panel; } else { continue; } } else { $panels[] = $stored_panel; } } foreach ($panels as $panel) { $impl = $panel->getPanel(); $impl->setViewer($viewer); } $panels = msort($panels, 'getSortKey'); // Normalize keys since callers shouldn't rely on this array being // partially keyed. $panels = array_values($panels); // Make sure exactly one valid panel is marked as default. $default = null; $first = null; foreach ($panels as $panel) { if (!$panel->canMakeDefault()) { continue; } if ($panel->isDefault()) { $default = $panel; break; } if ($first === null) { $first = $panel; } } if (!$default) { $default = $first; } if ($default) { $this->setDefaultPanel($default); } return $panels; } private function loadBuiltinProfilePanels() { $object = $this->getProfileObject(); $builtins = $this->getBuiltinProfilePanels($object); $panels = PhabricatorProfilePanel::getAllPanels(); $order = 1; $map = array(); foreach ($builtins as $builtin) { $builtin_key = $builtin->getBuiltinKey(); if (!$builtin_key) { throw new Exception( pht( 'Object produced a builtin panel with no builtin panel key! '. 'Builtin panels must have a unique key.')); } if (isset($map[$builtin_key])) { throw new Exception( pht( 'Object produced two panels with the same builtin key ("%s"). '. 'Each panel must have a unique builtin key.', $builtin_key)); } $panel_key = $builtin->getPanelKey(); $panel = idx($panels, $panel_key); if (!$panel) { throw new Exception( pht( 'Builtin panel ("%s") specifies a bad panel key ("%s"); there '. 'is no corresponding panel implementation available.', $builtin_key, $panel_key)); } $builtin ->setProfilePHID($object->getPHID()) ->attachPanel($panel) ->attachProfileObject($object) ->setPanelOrder($order); $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.')); } } private function newAutomaticMenuItems(AphrontSideNavFilterView $nav) { $items = array(); // NOTE: We're adding a spacer item for the fixed footer, so that if the // menu taller than the page content you can still scroll down the page far // enough to access the last item without the content being obscured by the // fixed items. $items[] = id(new PHUIListItemView()) ->setHideInApplicationMenu(true) ->addClass('phui-profile-menu-spacer'); $collapse_id = celerity_generate_unique_node_id(); $viewer = $this->getViewer(); $collapse_key = PhabricatorUserPreferences::PREFERENCE_PROFILE_MENU_COLLAPSED; $preferences = $viewer->loadPreferences(); $is_collapsed = $preferences->getPreference($collapse_key, false); if ($is_collapsed) { $nav->addClass('phui-profile-menu-collapsed'); } else { $nav->addClass('phui-profile-menu-expanded'); } if ($viewer->isLoggedIn()) { $settings_uri = '/settings/adjust/?key='.$collapse_key; } else { $settings_uri = null; } Javelin::initBehavior( 'phui-profile-menu', array( 'menuID' => $nav->getMainID(), 'collapseID' => $collapse_id, 'isCollapsed' => (bool)$is_collapsed, 'settingsURI' => $settings_uri, )); $collapse_icon = id(new PHUIIconCircleView()) ->addClass('phui-list-item-icon') ->addClass('phui-profile-menu-visible-when-expanded') ->setIconFont('fa-chevron-left'); $expand_icon = id(new PHUIIconCircleView()) ->addClass('phui-list-item-icon') ->addClass('phui-profile-menu-visible-when-collapsed') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Expand'), 'align' => 'E', )) ->setIconFont('fa-chevron-right'); $items[] = id(new PHUIListItemView()) ->setName('Collapse') ->addIcon($collapse_icon) ->addIcon($expand_icon) ->setID($collapse_id) ->addClass('phui-profile-menu-footer') ->addClass('phui-profile-menu-footer-1') ->setHideInApplicationMenu(true) ->setHref('#'); return $items; } public function getConfigureURI() { return $this->getPanelURI('configure/'); } private function buildPanelReorderContent(array $panels) { $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 ($panels as $key => $panel) { $id = $panel->getID(); if ($id) { $by_id[$id] = $key; continue; } $builtin_key = $panel->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; } } $panels = array_select_keys($panels, $key_order) + $panels; $type_order = PhabricatorProfilePanelConfigurationTransaction::TYPE_ORDER; $order = 1; foreach ($panels as $panel) { $xactions = array(); $xactions[] = id(new PhabricatorProfilePanelConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($order); $editor = id(new PhabricatorProfilePanelEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($panel, $xactions); $order++; } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } private function buildPanelConfigureContent(array $panels) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); 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->getPanelURI('reorder/'), )); $list = id(new PHUIObjectItemListView()) ->setID($list_id); foreach ($panels as $panel) { $id = $panel->getID(); $builtin_key = $panel->getBuiltinKey(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $panel, PhabricatorPolicyCapability::CAN_EDIT); $item = id(new PHUIObjectItemView()); $name = $panel->getDisplayName(); $type = $panel->getPanelTypeName(); if (!strlen(trim($name))) { $name = pht('Untitled "%s" Item', $type); } $item->setHeader($name); $item->addAttribute($type); if ($can_edit) { $item ->setGrippable(true) ->addSigil('profile-menu-item') ->setMetadata( array( 'key' => nonempty($id, $builtin_key), )); if ($id) { $default_uri = $this->getPanelURI("default/{$id}/"); } else { $default_uri = $this->getPanelURI("default/{$builtin_key}/"); } if ($panel->isDefault()) { $default_icon = 'fa-thumb-tack green'; $default_text = pht('Current Default'); } else if ($panel->canMakeDefault()) { $default_icon = 'fa-thumb-tack'; $default_text = pht('Make Default'); } else { $default_text = null; } if ($default_text !== null) { $item->addAction( id(new PHUIListItemView()) ->setHref($default_uri) ->setWorkflow(true) ->setName($default_text) ->setIcon($default_icon)); } if ($id) { $item->setHref($this->getPanelURI("edit/{$id}/")); $hide_uri = $this->getPanelURI("hide/{$id}/"); } else { $item->setHref($this->getPanelURI("builtin/{$builtin_key}/")); $hide_uri = $this->getPanelURI("hide/{$builtin_key}/"); } if ($panel->isDisabled()) { $hide_icon = 'fa-plus'; $hide_text = pht('Enable'); } else if ($panel->getBuiltinKey() !== null) { $hide_icon = 'fa-times'; $hide_text = pht('Disable'); } else { $hide_icon = 'fa-times'; $hide_text = pht('Delete'); } + $can_disable = $panel->canHidePanel(); + $item->addAction( id(new PHUIListItemView()) ->setHref($hide_uri) ->setWorkflow(true) + ->setDisabled(!$can_disable) ->setName($hide_text) ->setIcon($hide_icon)); } if ($panel->isDisabled()) { $item->setDisabled(true); } $list->addItem($item); } $action_view = id(new PhabricatorActionListView()) ->setUser($viewer); $panel_types = PhabricatorProfilePanel::getAllPanels(); $action_view->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Add New Menu Item...'))); foreach ($panel_types as $panel_type) { if (!$panel_type->canAddToObject($object)) { continue; } $panel_key = $panel_type->getPanelKey(); $action_view->addAction( id(new PhabricatorActionView()) ->setIcon($panel_type->getPanelTypeIcon()) ->setName($panel_type->getPanelTypeName()) ->setHref($this->getPanelURI("new/{$panel_key}/"))); } $action_view->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_view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-book') ->setHref($doc_link) ->setName($doc_name)); $action_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Configure Menu')) ->setHref('#') ->setIconFont('fa-gear') ->setDropdownMenu($action_view); $header = id(new PHUIHeaderView()) ->setHeader(pht('Profile Menu Items')) ->addActionLink($action_button); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setObjectList($list); return $box; } private function buildPanelNewContent($panel_key) { $panel_types = PhabricatorProfilePanel::getAllPanels(); $panel_type = idx($panel_types, $panel_key); if (!$panel_type) { return new Aphront404Response(); } $object = $this->getProfileObject(); if (!$panel_type->canAddToObject($object)) { return new Aphront404Response(); } $configuration = PhabricatorProfilePanelConfiguration::initializeNewPanelConfiguration( $object, $panel_type); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $controller = $this->getController(); return id(new PhabricatorProfilePanelEditEngine()) ->setPanelEngine($this) ->setProfileObject($object) ->setNewPanelConfiguration($configuration) ->setController($controller) ->buildResponse(); } private function buildPanelEditContent() { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $controller = $this->getController(); return id(new PhabricatorProfilePanelEditEngine()) ->setPanelEngine($this) ->setProfileObject($object) ->setController($controller) ->buildResponse(); } private function buildPanelBuiltinContent( PhabricatorProfilePanelConfiguration $configuration) { // If this builtin panel has already been persisted, redirect to the // edit page. $id = $configuration->getID(); if ($id) { return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI("edit/{$id}/")); } // Otherwise, act like we're creating a new panel, 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 PhabricatorProfilePanelEditEngine()) ->setIsBuiltin(true) ->setPanelEngine($this) ->setProfileObject($object) ->setNewPanelConfiguration($configuration) ->setController($controller) ->buildResponse(); } private function buildPanelHideContent( PhabricatorProfilePanelConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); + if (!$configuration->canHidePanel()) { + return $controller->newDialog() + ->setTitle(pht('Mandatory Panel')) + ->appendParagraph( + pht('This panel 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 = PhabricatorProfilePanelConfiguration::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 = PhabricatorProfilePanelConfiguration::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 = PhabricatorProfilePanelConfigurationTransaction::TYPE_VISIBILITY; $xactions = array(); $xactions[] = id(new PhabricatorProfilePanelConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($new_value); $editor = id(new PhabricatorProfilePanelEditor()) ->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 buildPanelDefaultContent( PhabricatorProfilePanelConfiguration $configuration, array $panels) { $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 newPanel() { return PhabricatorProfilePanelConfiguration::initializeNewBuiltin(); } public function adjustDefault($key) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $panels = $this->loadPanels(); // To adjust the default panel, we first change any existing panels that // are marked as defaults to "visible", then make the new default panel // the default. $default = array(); $visible = array(); foreach ($panels as $panel) { $builtin_key = $panel->getBuiltinKey(); $id = $panel->getID(); $is_target = (($builtin_key !== null) && ($builtin_key === $key)) || (($id !== null) && ($id === (int)$key)); if ($is_target) { if (!$panel->isDefault()) { $default[] = $panel; } } else { if ($panel->isDefault()) { $visible[] = $panel; } } } $type_visibility = PhabricatorProfilePanelConfigurationTransaction::TYPE_VISIBILITY; $v_visible = PhabricatorProfilePanelConfiguration::VISIBILITY_VISIBLE; $v_default = PhabricatorProfilePanelConfiguration::VISIBILITY_DEFAULT; $apply = array( array($v_visible, $visible), array($v_default, $default), ); foreach ($apply as $group) { list($value, $panels) = $group; foreach ($panels as $panel) { $xactions = array(); $xactions[] = id(new PhabricatorProfilePanelConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($value); $editor = id(new PhabricatorProfilePanelEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($panel, $xactions); } } return $this; } } diff --git a/src/applications/search/profilepanel/PhabricatorProfilePanel.php b/src/applications/search/profilepanel/PhabricatorProfilePanel.php index 49159dbf8d..8316d13467 100644 --- a/src/applications/search/profilepanel/PhabricatorProfilePanel.php +++ b/src/applications/search/profilepanel/PhabricatorProfilePanel.php @@ -1,62 +1,67 @@ newNavigationMenuItems($config); } abstract protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config); public function getPanelTypeIcon() { return null; } abstract public function getPanelTypeName(); abstract public function getDisplayName( PhabricatorProfilePanelConfiguration $config); public function buildEditEngineFields( PhabricatorProfilePanelConfiguration $config) { return array(); } public function canAddToObject($object) { return false; } + public function canHidePanel( + PhabricatorProfilePanelConfiguration $config) { + return true; + } + public function canMakeDefault( PhabricatorProfilePanelConfiguration $config) { return false; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } final public function getPanelKey() { return $this->getPhobjectClassConstant('PANELKEY'); } final public static function getAllPanels() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getPanelKey') ->execute(); } protected function newItem() { return new PHUIListItemView(); } } diff --git a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php index 127ce6b688..faeaeb5207 100644 --- a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php @@ -1,194 +1,201 @@ setVisibility(self::VISIBILITY_VISIBLE); } public static function initializeNewPanelConfiguration( $profile_object, PhabricatorProfilePanel $panel) { return self::initializeNewBuiltin() ->setProfilePHID($profile_object->getPHID()) ->setPanelKey($panel->getPanelKey()) ->attachPanel($panel) ->attachProfileObject($profile_object); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'panelProperties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'panelKey' => 'text64', 'builtinKey' => 'text64?', 'panelOrder' => 'uint32?', 'visibility' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_profile' => array( 'columns' => array('profilePHID', 'panelOrder'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProfilePanelPHIDType::TYPECONST); } public function attachPanel(PhabricatorProfilePanel $panel) { $this->panel = $panel; return $this; } public function getPanel() { return $this->assertAttached($this->panel); } public function attachProfileObject($profile_object) { $this->profileObject = $profile_object; return $this; } public function getProfileObject() { return $this->assertAttached($this->profileObject); } public function setPanelProperty($key, $value) { $this->panelProperties[$key] = $value; return $this; } public function getPanelProperty($key, $default = null) { return idx($this->panelProperties, $key, $default); } public function buildNavigationMenuItems() { return $this->getPanel()->buildNavigationMenuItems($this); } public function getPanelTypeName() { return $this->getPanel()->getPanelTypeName(); } public function getDisplayName() { return $this->getPanel()->getDisplayName($this); } public function canMakeDefault() { return $this->getPanel()->canMakeDefault($this); } + public function canHidePanel() { + return $this->getPanel()->canHidePanel($this); + } + public function getSortKey() { $order = $this->getPanelOrder(); if ($order === null) { $order = 'Z'; } else { $order = sprintf('%020d', $order); } return sprintf( '~%s%020d', $order, $this->getID()); } public function isDisabled() { + if (!$this->canHidePanel()) { + return false; + } return ($this->getVisibility() === self::VISIBILITY_DISABLED); } public function isDefault() { return ($this->getVisibility() === self::VISIBILITY_DEFAULT); } /* -( 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); } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { return array( array( $this->getProfileObject(), $capability, ), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProfilePanelEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProfilePanelConfigurationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/docs/user/userguide/profile_menu.diviner b/src/docs/user/userguide/profile_menu.diviner index 51912dd860..bd278b2702 100644 --- a/src/docs/user/userguide/profile_menu.diviner +++ b/src/docs/user/userguide/profile_menu.diviner @@ -1,143 +1,149 @@ @title Profile Menu User Guide @group userguide Master profile menus for projects and other objects. Overview ======== Some objects, like projects, have customizable menus called "profile menus". This guide discusses how to add, remove, reorder, configure and extend these menus. Supported Applications ====================== These applications currently support profile menus: | Application | Support | |-----|-----| | Projects | Full | | People | //Read-Only// | Collapsing and Expanding ======================== To collapse a full-width profile menu, click {nav icon="angle-left", name="Collapse"}. To expand a narrow menu, click {nav icon="angle-right", name="Expand"}. If you're logged in, this setting is sticky, and all menus will respect your preference. Editing Menus ============= You can only edit an object's menu if you can edit the object. For example, you must have permission to edit a project in order to reconfigure the menu for the project. -To edit a menu, click {nav icon="pencil", name="Edit Menu"}. This brings you to -the menu configuration interface which allows you to add and remove items, -reorder the menu, edit existing items, and choose a default item. +To edit a menu, click {nav icon="cogs", name="Manage"} in the menu, then click +{nav icon="th-list", name="Edit Menu"}. This brings you to the menu +configuration interface which allows you to add and remove items, reorder the +menu, edit existing items, and choose a default item. Menus are comprised of a list of items. Some of the items are builtin (for example, projects have builtin "Profile", "Workboard" and "Members" items). You can also add custom items. Builtin and custom items mostly behave in similar ways, but there are a few exceptions (for example, you can not completely delete builtin items). Adding Items ============ To add new items to a menu, use {nav icon="cog", name="Configure Menu"} and choose a type of item to add. See below for more details on available items. You can also find a link to this documentation in the same menu, if you want to reference it later. Reordering Items ================ To reorder items, drag and drop them to the desired position. Your changes will be reflected in the item ordering in the menu. Setting a Default ================= The default item controls what content is shown when a user browses to the object's main page. For example, the default item for a project controls where the user ends up when they click a link to the project from another application. To choose a default item, click {nav icon="thumb-tack", name="Make Default"}. Not all kinds of items can be set as the default item. For example, you can not set a separator line as a default because the item can't be selected and has no content. If no default is explicitly selected, or a default is deleted or disabled, the first item which is eligible to be a default is used as the default item. Removing Items ============== To remove items, click the {nav icon="times", name="Delete"} action. Builtin items can not be deleted and have a {nav icon="times", name="Disable"} action instead, which will hide them but not delete them. You an re-enable a disabled item with the {nav icon="plus', name="Enable"} action. +A few items can not be hidden or deleted. For example, the +{nav icon="cogs", name="Manage"} item must always be available in the menu +because if you hid it by accident there would no longer be a way to access +the configuration interface and fix the mistake. + Removing or hiding an item does not disable the underlying functionality. For example, if you hide the "Members" item for a project, that just removes it from the menu. The project still has members, and users can still navigate to the members page by following a link to it from elsewhere in the application or entering the URI manually. Editing Items ============= To edit an item, click the name of the item. This will show you available configuration for the item and allow you to edit it. Which properties are editable depends on what sort of item you are editing. Most items can be renamed, and some items have more settings available. For example, when editing a link, you can choose the link target and select an icon for the item. A few items have no configuration. For example, visual separator lines are purely cosmetic and have no available settings. Available Items =============== When you add items, you can choose between different types of items to add. Which item types are available depends on what sort of object you are editing the menu for, but most objects support these items: - {icon link} **Link**: Allows you to create an item which links to somewhere else in Phabricator, or to an external site. - {icon minus} **Divider**: Adds a visual separator to the menu. This is purely cosmetic. - {icon coffee} **Motivator**: Motivate your employees with inspirational quotes. A new quote every day! To learn more about how an item works, try adding it. You can always delete it later if it doesn't do what you wanted. Writing New Item Types ====================== IMPORTANT: This feature is not stable, and the API is subject to change. To add new types of items, subclass @{class:PhabricatorProfilePanel}.