diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'c61091b0', + 'core.pkg.css' => '7fce81fc', 'core.pkg.js' => '573e6664', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -143,7 +143,7 @@ 'rsrc/css/phui/phui-object-item-list-view.css' => '26c30d3f', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => 'a26fa598', + 'rsrc/css/phui/phui-profile-menu.css' => '72d69773', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-spacing.css' => '042804d6', @@ -500,6 +500,7 @@ 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475', 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', + 'rsrc/js/phui/behavior-phui-profile-menu.js' => 'bf2c93d6', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '21dc9144', @@ -648,6 +649,7 @@ 'javelin-behavior-pholio-mock-view' => 'fbe497e7', 'javelin-behavior-phui-dropdown-menu' => '54733475', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', + 'javelin-behavior-phui-profile-menu' => 'bf2c93d6', 'javelin-behavior-policy-control' => 'ae45872f', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-project-boards' => 'ba4fa35c', @@ -817,7 +819,7 @@ 'phui-object-item-list-view-css' => '26c30d3f', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => 'a26fa598', + 'phui-profile-menu-css' => '72d69773', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-spacing-css' => '042804d6', @@ -1772,6 +1774,11 @@ 'javelin-util', 'javelin-request', ), + 'bf2c93d6' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), 'bff6884b' => array( 'javelin-install', 'javelin-dom', diff --git a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php --- a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php +++ b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php @@ -191,14 +191,29 @@ // Background color for "dark" themes. 'page.background.dark' => '#ebecee', + // NOTE: We can't just do these with an alpha channel because the + // fixed items in the footer may render on top of other items, so the + // backgrounds must be opaque. + + // This is the base background color. 'menu.profile.background' => '#525868', + + // This is premultiplied 7.5% alpha. + 'menu.profile.background.hover' => '#4c5160', + + // This is premultiplied 15% alpha. + 'menu.profile.background.selected' => '#464b59', + 'menu.profile.text' => '#c6c7cb', 'menu.profile.text.selected' => '#ffffff', - 'menu.profile.icon' => '#ffffff', 'menu.profile.icon.disabled' => '#b9bcc2', 'menu.main.height' => '44px', + 'menu.profile.width' => '240px', + 'menu.profile.width.collapsed' => '80px', + 'menu.profile.item.height' => '46px', + ); } diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -4,6 +4,7 @@ private $project; private $profileMenu; + private $profilePanelEngine; protected function setProject(PhabricatorProject $project) { $this->project = $project; @@ -98,14 +99,8 @@ protected function getProfileMenu() { if (!$this->profileMenu) { - $project = $this->getProject(); - if ($project) { - $viewer = $this->getViewer(); - - $engine = id(new PhabricatorProjectProfilePanelEngine()) - ->setViewer($viewer) - ->setProfileObject($project); - + $engine = $this->getProfilePanelEngine(); + if ($engine) { $this->profileMenu = $engine->buildNavigation(); } } @@ -131,4 +126,24 @@ return $crumbs; } + protected function getProfilePanelEngine() { + if (!$this->profilePanelEngine) { + $viewer = $this->getViewer(); + $project = $this->getProject(); + if ($project) { + $engine = id(new PhabricatorProjectProfilePanelEngine()) + ->setViewer($viewer) + ->setProfileObject($project); + $this->profilePanelEngine = $engine; + } + } + return $this->profilePanelEngine; + } + + protected function setProfilePanelEngine( + PhabricatorProjectProfilePanelEngine $engine) { + $this->profilePanelEngine = $engine; + return $this; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectPanelController.php b/src/applications/project/controller/PhabricatorProjectPanelController.php --- a/src/applications/project/controller/PhabricatorProjectPanelController.php +++ b/src/applications/project/controller/PhabricatorProjectPanelController.php @@ -12,10 +12,13 @@ $viewer = $this->getViewer(); $project = $this->getProject(); - return id(new PhabricatorProjectProfilePanelEngine()) + $engine = id(new PhabricatorProjectProfilePanelEngine()) ->setProfileObject($project) - ->setController($this) - ->buildResponse(); + ->setController($this); + + $this->setProfilePanelEngine($engine); + + return $engine->buildResponse(); } } diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -6,6 +6,7 @@ private $profileObject; private $panels; private $controller; + private $navigation; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -147,6 +148,10 @@ } public function buildNavigation() { + if ($this->navigation) { + return $this->navigation; + } + $nav = id(new AphrontSideNavFilterView()) ->setIsProfileMenu(true) ->setBaseURI(new PhutilURI($this->getPanelURI(''))); @@ -185,14 +190,15 @@ } } - $configure_item = $this->newConfigureMenuItem(); - if ($configure_item) { - $nav->addMenuItem($configure_item); + $more_items = $this->newAutomaticMenuItems($nav); + foreach ($more_items as $item) { + $nav->addMenuItem($item); } $nav->selectFilter(null); - return $nav; + $this->navigation = $nav; + return $this->navigation; } private function getPanels() { @@ -301,26 +307,112 @@ } } - private function newConfigureMenuItem() { - if (!$this->isPanelEngineConfigurable()) { - return null; + 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 PHUIIconView()) + ->addClass('phui-list-item-icon') + ->addClass('phui-profile-menu-visible-when-expanded') + ->setIconFont('fa-pencil'); + + $collapsed_edit_icon = id(new PHUIIconView()) + ->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(); - $object = $this->getProfileObject(); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $object, - PhabricatorPolicyCapability::CAN_EDIT); + $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' => $is_collapsed, + 'settingsURI' => $settings_uri, + )); - return id(new PHUIListItemView()) - ->setName('Configure Menu') - ->setKey('panel.configure') - ->setIcon('fa-gear') - ->setHref($this->getPanelURI('configure/')) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit); + $collapse_icon = id(new PHUIIconView()) + ->addClass('phui-list-item-icon') + ->addClass('phui-profile-menu-visible-when-expanded') + ->setIconFont('fa-angle-left'); + + $expand_icon = id(new PHUIIconView()) + ->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() { diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -41,6 +41,8 @@ const PREFERENCE_RESOURCE_POSTPROCESSOR = 'resource-postprocessor'; const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; + const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; + // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; const MAILTAG_PREFERENCE_EMAIL = 1; diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -27,6 +27,7 @@ private $crumbs; private $classes = array(); private $menuID; + private $mainID; private $isProfileMenu; private $footer = array(); @@ -168,6 +169,13 @@ return $this; } + public function getMainID() { + if (!$this->mainID) { + $this->mainID = celerity_generate_unique_node_id(); + } + return $this->mainID; + } + public function render() { if ($this->menu->getItems()) { if (!$this->baseURI) { @@ -212,7 +220,7 @@ $local_id = null; $background_id = null; $local_menu = null; - $main_id = celerity_generate_unique_node_id(); + $main_id = $this->getMainID(); if ($this->flexible) { $drag_id = celerity_generate_unique_node_id(); diff --git a/src/view/layout/PHUIApplicationMenuView.php b/src/view/layout/PHUIApplicationMenuView.php --- a/src/view/layout/PHUIApplicationMenuView.php +++ b/src/view/layout/PHUIApplicationMenuView.php @@ -75,8 +75,11 @@ $profile_menu = $this->getProfileMenu(); if ($profile_menu) { foreach ($profile_menu->getMenu()->getItems() as $item) { + if ($item->getHideInApplicationMenu()) { + continue; + } + $item = clone $item; - $item->setRenderNameAsTooltip(false); $view->addMenuItem($item); } } diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php --- a/src/view/phui/PHUIListItemView.php +++ b/src/view/phui/PHUIListItemView.php @@ -28,6 +28,17 @@ private $aural; private $profileImage; private $indented; + private $hideInApplicationMenu; + private $icons = array(); + + public function setHideInApplicationMenu($hide) { + $this->hideInApplicationMenu = $hide; + return $this; + } + + public function getHideInApplicationMenu() { + return $this->hideInApplicationMenu; + } public function setDropdownMenu(PhabricatorActionListView $actions) { Javelin::initBehavior('phui-dropdown-menu'); @@ -150,6 +161,15 @@ return $this; } + public function addIcon(PHUIIconView $icon) { + $this->icons[] = $icon; + return $this; + } + + public function getIcons() { + return $this->icons; + } + protected function getTagName() { return 'li'; } @@ -274,6 +294,8 @@ $classes[] = 'phui-list-item-indented'; } + $icons = $this->getIcons(); + return javelin_tag( $this->href ? 'a' : 'div', array( @@ -285,6 +307,7 @@ array( $aural, $icon, + $icons, $this->renderChildren(), $name, )); diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -16,12 +16,17 @@ display: table-cell; position: relative; vertical-align: top; - width: 240px; - max-width: 240px; + width: {$menu.profile.width}; + max-width: {$menu.profile.width}; margin-top: 0; overflow: hidden; } +.device-desktop .phui-profile-menu-collapsed .phabricator-nav-local { + width: {$menu.profile.width.collapsed}; + max-width: {$menu.profile.width.collapsed}; +} + .device-desktop .phui-profile-menu .phabricator-nav-content { display: table-cell; margin-left: 0; @@ -47,23 +52,47 @@ line-height: 22px; overflow: hidden; text-overflow: ellipsis; + + /* NOTE: We must have an opaque background on these items so the footer + items appear opaque when the render over normal items. */ + background: {$menu.profile.background}; } .phui-profile-menu .phabricator-side-menu .phui-list-item-icon, .phui-profile-menu .phabricator-side-menu .phui-list-item-href .phui-icon-view { position: absolute; - left: 13px; top: 12px; + left: 13px; font-size: 20px; width: 22px; height: 22px; line-height: 22px; text-align: center; - color: {$menu.profile.icon}; + color: {$menu.profile.text}; background-size: 100%; } +.phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-href { + text-align: center; + padding: 42px 8px 12px; + font-size: 11px; + line-height: 13px; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-icon, +.phui-profile-menu .phui-profile-menu-collapsed + .phui-list-item-href .phui-icon-view { + top: 10px; + left: 29px; +} + .phui-profile-menu .phabricator-side-menu .phui-list-item-disabled .phui-list-item-icon { @@ -76,7 +105,16 @@ .device-desktop .phui-profile-menu .phabricator-side-menu .phui-list-item-href:hover { - background-color: rgba(0, 0, 0, 0.075); + background-color: {$menu.profile.background.hover}; + color: {$menu.profile.text.selected}; +} + +.phui-profile-menu .phabricator-side-menu + .phui-list-item-selected + .phui-list-item-icon, +.device-desktop .phui-profile-menu .phabricator-side-menu + .phui-list-item-href:hover + .phui-list-item-icon { color: {$menu.profile.text.selected}; } @@ -85,7 +123,7 @@ .device-desktop .phui-profile-menu .phabricator-side-menu .phui-list-item-selected .phui-list-item-href:hover { - background-color: rgba(0, 0, 0, 0.150); + background-color: {$menu.profile.background.selected}; color: {$menu.profile.text.selected}; } @@ -107,3 +145,56 @@ font-size: 12px; color: {$menu.profile.text}; } + +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-spacer { + box-sizing: border-box; + height: {$menu.profile.item.height}; +} + +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-footer { + position: fixed; + box-sizing: border-box; + width: {$menu.profile.width}; + bottom: 0px; +} + +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-footer-1 { + left: 0; +} + +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-footer-2 { + left: 120px; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer { + width: 40px; + height: {$menu.profile.item.height}; + bottom: 0px; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer-1 { + left: 0; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer-2 { + left: 40px; +} + + +.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer + .phui-list-item-name { + display: none; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer + .phui-list-item-icon { + top: 10px; + left: 10px; +} + +.phui-profile-menu .phui-profile-menu-expanded + .phui-profile-menu-visible-when-collapsed, +.phui-profile-menu .phui-profile-menu-collapsed + .phui-profile-menu-visible-when-expanded { + display: none; +} diff --git a/webroot/rsrc/js/phui/behavior-phui-profile-menu.js b/webroot/rsrc/js/phui/behavior-phui-profile-menu.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/phui/behavior-phui-profile-menu.js @@ -0,0 +1,28 @@ +/** + * @provides javelin-behavior-phui-profile-menu + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + */ + +JX.behavior('phui-profile-menu', function(config) { + var menu_node = JX.$(config.menuID); + var collapse_node = JX.$(config.collapseID); + + var is_collapsed = config.isCollapsed; + + JX.DOM.listen(collapse_node, 'click', null, function(e) { + is_collapsed = !is_collapsed; + JX.DOM.alterClass(menu_node, 'phui-profile-menu-collapsed', is_collapsed); + JX.DOM.alterClass(menu_node, 'phui-profile-menu-expanded', !is_collapsed); + + if (config.settingsURI) { + new JX.Request(config.settingsURI) + .setData({value: (is_collapsed ? 1 : 0)}) + .send(); + } + + e.kill(); + }); + +});