diff --git a/src/applications/home/application/PhabricatorApplicationHome.php b/src/applications/home/application/PhabricatorApplicationHome.php index 9c487bd2a5..d9edbb094a 100644 --- a/src/applications/home/application/PhabricatorApplicationHome.php +++ b/src/applications/home/application/PhabricatorApplicationHome.php @@ -1,110 +1,111 @@ <?php final class PhabricatorApplicationHome extends PhabricatorApplication { public function getBaseURI() { - return '/'; + return '/home/'; } public function getShortDescription() { - return pht('Where the <3 is'); + return pht('Command Center'); } public function getIconName() { return 'home'; } public function getRoutes() { return array( - '/(?:(?P<filter>(?:jump))/)?' => 'PhabricatorHomeMainController', + '/' => 'PhabricatorHomeMainController', + '/(?P<only>home)/' => 'PhabricatorHomeMainController', '/home/' => array( 'create/' => 'PhabricatorHomeQuickCreateController', ), ); } public function isLaunchable() { return false; } public function canUninstall() { return false; } public function getApplicationOrder() { return 9; } public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { $items = array(); if ($user->isLoggedIn() && $user->isUserActivated()) { $create_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $create_id, 'dropdownID' => 'phabricator-quick-create-menu', 'local' => true, 'desktop' => true, 'right' => true, )); $item = id(new PHUIListItemView()) ->setName(pht('Create New...')) ->setIcon('new-sm') ->addClass('core-menu-item') ->setHref('/home/create/') ->addSigil('quick-create-menu') ->setID($create_id) ->setAural(pht('Quick Create')) ->setOrder(300); $items[] = $item; } return $items; } public function loadAllQuickCreateItems(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->execute(); $items = array(); foreach ($applications as $application) { $app_items = $application->getQuickCreateItems($viewer); foreach ($app_items as $app_item) { $items[] = $app_item; } } return $items; } public function buildMainMenuExtraNodes( PhabricatorUser $viewer, PhabricatorController $controller = null) { $items = $this->loadAllQuickCreateItems($viewer); $view = new PHUIListView(); $view->newLabel(pht('Create New...')); foreach ($items as $item) { $view->addMenuItem($item); } return phutil_tag( 'div', array( 'id' => 'phabricator-quick-create-menu', 'class' => 'phabricator-main-menu-dropdown phui-list-sidenav', 'style' => 'display: none', ), $view); } } diff --git a/src/applications/home/controller/PhabricatorHomeController.php b/src/applications/home/controller/PhabricatorHomeController.php index 53ac7f8713..17f543e4b9 100644 --- a/src/applications/home/controller/PhabricatorHomeController.php +++ b/src/applications/home/controller/PhabricatorHomeController.php @@ -1,77 +1,86 @@ <?php abstract class PhabricatorHomeController extends PhabricatorController { public function buildStandardPageResponse($view, array $data) { $page = $this->buildStandardPageView(); $page->setBaseURI('/'); $page->setTitle(idx($data, 'title')); $page->setGlyph("\xE2\x9A\x92"); $page->appendChild($view); $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } public function buildNav() { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/')); $applications = id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withInstalled(true) ->withUnlisted(false) ->withLaunchable(true) ->execute(); $pinned = $user->loadPreferences()->getPinnedApplications( $applications, $user); // Force "Applications" to appear at the bottom. $meta_app = 'PhabricatorApplicationApplications'; $pinned = array_fuse($pinned); unset($pinned[$meta_app]); $pinned[$meta_app] = $meta_app; $applications[$meta_app] = PhabricatorApplication::getByClass($meta_app); $tiles = array(); + + $home_app = new PhabricatorApplicationHome(); + + $tiles[] = id(new PhabricatorApplicationLaunchView()) + ->setApplication($home_app) + ->setApplicationStatus($home_app->loadStatus($user)) + ->addClass('phabricator-application-launch-phone-only') + ->setUser($user); + foreach ($pinned as $pinned_application) { if (empty($applications[$pinned_application])) { continue; } $application = $applications[$pinned_application]; $tile = id(new PhabricatorApplicationLaunchView()) ->setApplication($application) ->setApplicationStatus($application->loadStatus($user)) ->setUser($user); $tiles[] = $tile; } $nav->addCustomBlock( phutil_tag( 'div', array( 'class' => 'application-tile-group', ), $tiles)); $nav->addFilter( '', pht('Customize Applications...'), '/settings/panel/home/'); $nav->addClass('phabricator-side-menu-home'); $nav->selectFilter(null); return $nav; } } diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php index 693695e300..30a63fbd09 100644 --- a/src/applications/home/controller/PhabricatorHomeMainController.php +++ b/src/applications/home/controller/PhabricatorHomeMainController.php @@ -1,410 +1,410 @@ <?php final class PhabricatorHomeMainController extends PhabricatorHomeController { - private $filter; + private $only; private $minipanels = array(); public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { - $this->filter = idx($data, 'filter'); + $this->only = idx($data, 'only'); } public function processRequest() { $user = $this->getRequest()->getUser(); - $nav = $this->buildNav(); $dashboard = PhabricatorDashboardInstall::getDashboard( $user, $user->getPHID(), get_class($this->getCurrentApplication())); if ($dashboard) { - $rendered_dashboard = id(new PhabricatorDashboardRenderingEngine()) + $content = id(new PhabricatorDashboardRenderingEngine()) ->setViewer($user) ->setDashboard($dashboard) ->renderDashboard(); - $nav->appendChild($rendered_dashboard); } else { $project_query = new PhabricatorProjectQuery(); $project_query->setViewer($user); $project_query->withMemberPHIDs(array($user->getPHID())); $projects = $project_query->execute(); - $nav = $this->buildMainResponse($nav, $projects); + $content = $this->buildMainResponse($projects); } - $nav->appendChild(id(new PhabricatorGlobalUploadTargetView()) - ->setUser($user)); + if (!$this->only) { + $nav = $this->buildNav(); + $nav->appendChild( + array( + $content, + id(new PhabricatorGlobalUploadTargetView())->setUser($user), + )); + $content = $nav; + } return $this->buildApplicationPage( - $nav, + $content, array( 'title' => 'Phabricator', 'device' => true, )); } - private function buildMainResponse($nav, array $projects) { + private function buildMainResponse(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->getRequest()->getUser(); $has_maniphest = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorApplicationManiphest', $viewer); $has_audit = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorApplicationAudit', $viewer); $has_differential = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorApplicationDifferential', $viewer); if ($has_maniphest) { $unbreak_panel = $this->buildUnbreakNowPanel(); $triage_panel = $this->buildNeedsTriagePanel($projects); $tasks_panel = $this->buildTasksPanel(); } else { $unbreak_panel = null; $triage_panel = null; $tasks_panel = null; } if ($has_audit) { $audit_panel = $this->buildAuditPanel(); $commit_panel = $this->buildCommitPanel(); } else { $audit_panel = null; $commit_panel = null; } if (PhabricatorEnv::getEnvConfig('welcome.html') !== null) { $welcome_panel = $this->buildWelcomePanel(); } else { $welcome_panel = null; } if ($has_differential) { $revision_panel = $this->buildRevisionPanel(); } else { $revision_panel = null; } - $content = array( + return array( $welcome_panel, $unbreak_panel, $triage_panel, $revision_panel, $tasks_panel, $audit_panel, $commit_panel, $this->minipanels, ); - - $nav->appendChild($content); - - return $nav; - } private function buildUnbreakNowPanel() { $unbreak_now = PhabricatorEnv::getEnvConfig( 'maniphest.priorities.unbreak-now'); if (!$unbreak_now) { return null; } $user = $this->getRequest()->getUser(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($unbreak_now)) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No "Unbreak Now!" Tasks', 'Nothing appears to be critically broken right now.'); } $href = urisprintf( '/maniphest/?statuses=%s&priorities=%s#R', implode(',', ManiphestTaskStatus::getOpenStatusConstants()), $unbreak_now); $title = pht('Unbreak Now!'); $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); return $panel; } private function buildNeedsTriagePanel(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $needs_triage = PhabricatorEnv::getEnvConfig( 'maniphest.priorities.needs-triage'); if (!$needs_triage) { return null; } $user = $this->getRequest()->getUser(); if (!$user->isLoggedIn()) { return null; } if ($projects) { $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($needs_triage)) ->withAnyProjects(mpull($projects, 'getPHID')) ->setLimit(10); $tasks = $task_query->execute(); } else { $tasks = array(); } if (!$tasks) { return $this->renderMiniPanel( 'No "Needs Triage" Tasks', hsprintf( 'No tasks in <a href="/project/">projects you are a member of</a> '. 'need triage.')); } $title = pht('Needs Triage'); $href = urisprintf( '/maniphest/?statuses=%s&priorities=%s&userProjects=%s#R', implode(',', ManiphestTaskStatus::getOpenStatusConstants()), $needs_triage, $user->getPHID()); $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); return $panel; } private function buildRevisionPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $revision_query = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withResponsibleUsers(array($user_phid)) ->needRelationships(true) ->needFlags(true) ->needDrafts(true); $revisions = $revision_query->execute(); list($blocking, $active, ) = DifferentialRevisionQuery::splitResponsible( $revisions, array($user_phid)); if (!$blocking && !$active) { return $this->renderMiniPanel( 'No Waiting Revisions', 'No revisions are waiting on you.'); } $title = pht('Revisions Waiting on You'); $href = '/differential'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $revision_view = id(new DifferentialRevisionListView()) ->setHighlightAge(true) ->setRevisions(array_merge($blocking, $active)) ->setUser($user); $phids = array_merge( array($user_phid), $revision_view->getRequiredHandlePHIDs()); $handles = $this->loadViewerHandles($phids); $revision_view->setHandles($handles); $list_view = $revision_view->render(); $list_view->setFlush(true); $panel->appendChild($list_view); $panel->setNoBackground(); return $panel; } private function buildWelcomePanel() { $panel = new AphrontPanelView(); $panel->appendChild( phutil_safe_html( PhabricatorEnv::getEnvConfig('welcome.html'))); $panel->setNoBackground(); return $panel; } private function buildTasksPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY) ->withOwners(array($user_phid)) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No Assigned Tasks', 'You have no assigned tasks.'); } $title = pht('Assigned Tasks'); $href = '/maniphest'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); return $panel; } private function buildTaskListView(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $user = $this->getRequest()->getUser(); $phids = array_merge( array_filter(mpull($tasks, 'getOwnerPHID')), array_mergev(mpull($tasks, 'getProjectPHIDs'))); $handles = $this->loadViewerHandles($phids); $view = new ManiphestTaskListView(); $view->setTasks($tasks); $view->setUser($user); $view->setHandles($handles); return $view; } private function renderSectionHeader($title, $href) { $header = phutil_tag( 'a', array( 'href' => $href, ), $title); return $header; } private function renderMiniPanel($title, $body) { $panel = new AphrontMiniPanelView(); $panel->appendChild( phutil_tag( 'p', array( ), array( phutil_tag('strong', array(), $title.': '), $body ))); $this->minipanels[] = $panel; } public function buildAuditPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $query = id(new DiffusionCommitQuery()) ->setViewer($user) ->withAuditorPHIDs($phids) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_OPEN) ->withAuditAwaitingUser($user) ->needAuditRequests(true) ->needCommitData(true) ->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( 'No Audits', 'No commits are waiting for you to audit them.'); } $view = id(new PhabricatorAuditListView()) ->setCommits($commits) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Audits'); $href = '/audit/'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); $panel->setNoBackground(); return $panel; } public function buildCommitPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = array($user->getPHID()); $query = id(new DiffusionCommitQuery()) ->setViewer($user) ->withAuthorPHIDs($phids) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_CONCERN) ->needCommitData(true) ->needAuditRequests(true) ->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( 'No Problem Commits', 'No one has raised concerns with your commits.'); } $view = id(new PhabricatorAuditListView()) ->setCommits($commits) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Problem Commits'); $href = '/audit/'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); $panel->setNoBackground(); return $panel; } } diff --git a/src/applications/meta/view/PhabricatorApplicationLaunchView.php b/src/applications/meta/view/PhabricatorApplicationLaunchView.php index e58c3f02b8..512221eb00 100644 --- a/src/applications/meta/view/PhabricatorApplicationLaunchView.php +++ b/src/applications/meta/view/PhabricatorApplicationLaunchView.php @@ -1,143 +1,145 @@ <?php -final class PhabricatorApplicationLaunchView extends AphrontView { +final class PhabricatorApplicationLaunchView extends AphrontTagView { private $application; private $status; public function setApplication(PhabricatorApplication $application) { $this->application = $application; return $this; } public function setApplicationStatus(array $status) { $this->status = $status; return $this; } - public function render() { + protected function getTagName() { + return $this->application ? 'a' : 'div'; + } + + protected function getTagAttributes() { + $application = $this->application; + return array( + 'class' => array('phabricator-application-launch-container'), + 'href' => $application ? $application->getBaseURI() : null, + ); + } + + protected function getTagContent() { $application = $this->application; require_celerity_resource('phabricator-application-launch-view-css'); require_celerity_resource('sprite-apps-large-css'); $content = array(); $icon = null; if ($application) { $content[] = phutil_tag( 'span', array( 'class' => 'phabricator-application-launch-name', ), $application->getName()); if ($application->isBeta()) { $content[] = javelin_tag( 'span', array( 'aural' => false, 'class' => 'phabricator-application-beta', ), "\xCE\xB2"); } $content[] = phutil_tag( 'span', array( 'class' => 'phabricator-application-launch-description', ), $application->getShortDescription()); $counts = array(); $text = array(); if ($this->status) { foreach ($this->status as $status) { $type = $status->getType(); $counts[$type] = idx($counts, $type, 0) + $status->getCount(); if ($status->getCount()) { $text[] = $status->getText(); } } } $attention = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; $warning = PhabricatorApplicationStatusView::TYPE_WARNING; if (!empty($counts[$attention]) || !empty($counts[$warning])) { $count = idx($counts, $attention, 0); $count1 = $count2 = ''; if ($count > 0) { $count1 = phutil_tag( 'span', array( 'class' => 'phabricator-application-attention-count', ), $count); } if (!empty($counts[$warning])) { $count2 = phutil_tag( 'span', array( 'class' => 'phabricator-application-warning-count', ), $counts[$warning]); } if (nonempty($count1) && nonempty($count2)) { $numbers = array($count1, ' / ', $count2); } else { $numbers = array($count1, $count2); } Javelin::initBehavior('phabricator-tooltips'); $content[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => implode("\n", $text), 'size' => 240, ), 'class' => 'phabricator-application-launch-attention', ), $numbers); } $classes = array(); $classes[] = 'phabricator-application-launch-icon'; $styles = array(); if ($application->getIconURI()) { $styles[] = 'background-image: url('.$application->getIconURI().')'; } else { $icon = $application->getIconName(); $classes[] = 'sprite-apps-large'; $classes[] = 'apps-'.$icon.'-dark-large'; } $icon = phutil_tag( 'span', array( 'class' => implode(' ', $classes), 'style' => nonempty(implode('; ', $styles), null), ), ''); } - $classes = array(); - $classes[] = 'phabricator-application-launch-container'; - - $app_button = phutil_tag( - $application ? 'a' : 'div', - array( - 'class' => implode(' ', $classes), - 'href' => $application ? $application->getBaseURI() : null, - ), - array( - $icon, - $content, - )); - - return $app_button; + return array( + $icon, + $content, + ); } + } diff --git a/webroot/rsrc/css/application/base/phabricator-application-launch-view.css b/webroot/rsrc/css/application/base/phabricator-application-launch-view.css index 674337ca43..c412384df0 100644 --- a/webroot/rsrc/css/application/base/phabricator-application-launch-view.css +++ b/webroot/rsrc/css/application/base/phabricator-application-launch-view.css @@ -1,95 +1,103 @@ /** * @provides phabricator-application-launch-view-css */ /* - Application List ---------------------------------------------------------- Spacing container for the list of large application buttons. */ .application-tile-group { overflow: hidden; } /* - Application Launch Button ------------------------------------------------- Spacing container for the list of large application buttons. */ a.phabricator-application-launch-container, div.phabricator-application-launch-container { display: block; float: left; overflow: hidden; position: relative; text-decoration: none; width: 100%; height: 44px; border-bottom: 1px solid {$hovergrey}; } .device-phone div.phabricator-application-launch-container { display: none; } .phabricator-application-launch-icon { display: block; position: absolute; width: 28px; height: 28px; top: 8px; left: 8px; } .device-desktop a.phabricator-application-launch-container:hover { background-color: {$hovergrey}; text-decoration: none; } .phabricator-application-launch-name { display: block; font-weight: bold; color: {$darkbluetext}; font-size: 13px; margin: 6px 0 2px 44px; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.9); } .phabricator-application-launch-description { color: {$bluetext}; font-size: 11px; margin-left: 44px; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.9); text-overflow: ellipsis; width: 150px; overflow: hidden; white-space: nowrap; display: inline-block; } .phabricator-application-beta { position: absolute; top: 4px; left: 4px; color: {$bluetext}; font-size: 11px; } .phabricator-application-launch-attention { position: absolute; top: 8px; right: 8px; color: {$darkbluetext}; font-weight: bold; font-size: 12px; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.9); } .phabricator-application-attention-count { color: {$sky}; } + +a.phabricator-application-launch-phone-only { + display: none; +} + +.device-phone a.phabricator-application-launch-phone-only { + display: block; +}