diff --git a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php index f88faa15c4..d37178ef7a 100644 --- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php @@ -1,42 +1,30 @@ getProfileObject(); $id = $project->getID(); return "/project/{$id}/panel/{$path}"; } protected function getBuiltinProfilePanels($object) { $panels = array(); $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_PROFILE) ->setPanelKey(PhabricatorProjectDetailsProfilePanel::PANELKEY); $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_WORKBOARD) ->setPanelKey(PhabricatorProjectWorkboardProfilePanel::PANELKEY); - // TODO: This is temporary. - $uri = urisprintf( - '/maniphest/?statuses=open()&projects=%s#R', - $object->getPHID()); - - $panels[] = $this->newPanel() - ->setBuiltinKey('tasks') - ->setPanelKey(PhabricatorLinkProfilePanel::PANELKEY) - ->setPanelProperty('icon', 'maniphest') - ->setPanelProperty('name', pht('Open Tasks')) - ->setPanelProperty('uri', $uri); - $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_MEMBERS) ->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY); return $panels; } } diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index ea4cefce56..e0f86766a1 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -1,949 +1,953 @@ 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'); if ($this->isPanelEngineConfigurable()) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); $expanded_edit_icon = id(new PHUIIconCircleView()) ->addClass('phui-list-item-icon') ->addClass('phui-profile-menu-visible-when-expanded') ->setIconFont('fa-pencil'); $collapsed_edit_icon = id(new PHUIIconCircleView()) ->addClass('phui-list-item-icon') ->addClass('phui-profile-menu-visible-when-collapsed') ->setIconFont('fa-pencil') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Edit Menu'), 'align' => 'E', )); $items[] = id(new PHUIListItemView()) ->setName('Edit Menu') ->setKey('panel.configure') ->addIcon($expanded_edit_icon) ->addIcon($collapsed_edit_icon) ->addClass('phui-profile-menu-footer') ->addClass('phui-profile-menu-footer-1') ->setHref($this->getPanelURI('configure/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); } $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-angle-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-angle-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-2') ->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('Show'); + $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'); } $item->addAction( id(new PHUIListItemView()) ->setHref($hide_uri) ->setWorkflow(true) ->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') - ->setName(pht('TODO: Write Documentation'))); + ->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->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); } $type_visibility = PhabricatorProfilePanelConfigurationTransaction::TYPE_VISIBILITY; $v_visible = PhabricatorProfilePanelConfiguration::VISIBILITY_VISIBLE; $v_default = PhabricatorProfilePanelConfiguration::VISIBILITY_DEFAULT; if ($request->isFormPost()) { // First, mark any existing default panels as merely visible. foreach ($panels as $panel) { if (!$panel->isDefault()) { continue; } $xactions = array(); $xactions[] = id(new PhabricatorProfilePanelConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($v_visible); $editor = id(new PhabricatorProfilePanelEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($panel, $xactions); } // Now, make this panel the default. $xactions = array(); $xactions[] = id(new PhabricatorProfilePanelConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($v_default); $editor = id(new PhabricatorProfilePanelEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($configuration, $xactions); 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(); } } diff --git a/src/docs/user/userguide/profile_menu.diviner b/src/docs/user/userguide/profile_menu.diviner new file mode 100644 index 0000000000..51912dd860 --- /dev/null +++ b/src/docs/user/userguide/profile_menu.diviner @@ -0,0 +1,143 @@ +@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. + +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. + +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}. diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner index 77bc3523c7..17e8473131 100644 --- a/src/docs/user/userguide/projects.diviner +++ b/src/docs/user/userguide/projects.diviner @@ -1,53 +1,95 @@ @title Projects User Guide @group userguide Organize users and objects with projects. Overview ======== NOTE: This document is only partially complete. Phabricator projects are flexible groups of users and objects. Joining Projects ================ Once you join a project, you become a member and will receive mail sent to the project, like a mailing list. For example, if a project is added as a subscriber on a task or a reviewer on a revision, you will receive mail about that task or revision. If you'd prefer not to receive mail sent to a project, you can go to {nav Members} and select {nav Disable Mail}. If you disable mail for a project, you will no longer receive mail sent to the project. Watching Projects ================= Watching a project allows you to closely follow all activity related to a project. You can **watch** a project by clicking {nav Watch Project} on the project page. To stop watching a project, click {nav Unwatch Project}. When you watch a project, you will receive a copy of mail about any objects (like tasks or revisions) that are tagged with the project, or that the project is a subscriber, reviewer, or auditor for. For moderately active projects, this may be a large volume of mail. Edit Notifications ================== Edit notifications are generated when project details (like the project description, name, or icon) are updated, or when users join or leave projects. By default, these notifications are are only sent to the acting user. These notifications are usually not very interesting, and project mail is already complicated by members and watchers. If you'd like to receive edit notifications for a project, you can write a Herald rule to keep you in the loop. + + +Customizing Menus +================= + +Projects support profile menus, which are customizable. For full details on +managing and customizing profile menus, see @{article:Profile Menu User Guide}. + +Here are some examples of common ways to customize project profile menus that +may be useful: + +**Link to Tasks or Repositories**: You can add a menu item for "Open Tasks" or +"Active Repositories" for a project by running the search in the appropriate +application, then adding a link to the search results to the menu. + +This can let you quickly jump from a project screen to related tasks, +revisions, repositories, or other objects. + +For more details on how to use search and manage queries, see +@{article:Search User Guide}. + +**New Task Button**: To let users easily make a new task that is tagged with +the current project, add a link to the "New Task" form with the project +prefilled, or to a custom form with appropriate defaults. + +For information on customizing and prefilling forms, see +@{article:User Guide: Customizing Forms}. + +**Link to Wiki Pages**: You can add links to relevant wiki pages or other +documentation to the menu to make it easy to find and access. You could also +link to a Conpherence if you have a chatroom for a project. + +**Link to External Resources**: You can link to external resources outside +of Phabricator if you have other pages which are relevant to a project. + +**Set Workboard as Default**: For projects that are mostly used to organize +tasks, change the default item to the workboard instead of the profile to get +to the workboard view more easily. + +**Hide Unused Items**: If you have a project which you don't expect to have +members or won't have a workboard, you can hide these items to streamline the +menu.