diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3fa504018264236b73f5979d12ae81ae29779ff6..de567d04d9487aa3784ce3f88c9d6facb272d44c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -65,8 +65,11 @@ phutil_register_library_map(array( 'SprintPoints' => 'util/SprintPoints.php', 'SprintProjectController' => 'controller/SprintProjectController.php', 'SprintProjectCustomField' => 'customfield/SprintProjectCustomField.php', + 'SprintProjectDetailsProfilePanel' => 'engine/SprintProjectDetailsProfilePanel.php', 'SprintProjectProfileController' => 'controller/SprintProjectProfileController.php', + 'SprintProjectProfilePanelEngine' => 'engine/SprintProjectProfilePanelEngine.php', 'SprintProjectViewController' => 'controller/SprintProjectViewController.php', + 'SprintProjectWorkboardProfilePanel' => 'profilepanel/SprintProjectWorkboardProfilePanel.php', 'SprintQuery' => 'query/SprintQuery.php', 'SprintQueryTest' => 'tests/SprintQueryTest.php', 'SprintReportBurnUpView' => 'view/reports/SprintReportBurnUpView.php', @@ -105,7 +108,7 @@ phutil_register_library_map(array( 'SprintBoardColumnDetailController' => 'SprintBoardController', 'SprintBoardColumnEditController' => 'SprintBoardController', 'SprintBoardColumnHideController' => 'SprintBoardController', - 'SprintBoardController' => 'SprintController', + 'SprintBoardController' => 'SprintProjectController', 'SprintBoardImportController' => 'SprintBoardController', 'SprintBoardMoveController' => 'SprintBoardController', 'SprintBoardReorderController' => 'SprintBoardController', @@ -140,8 +143,11 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomField', 'PhabricatorStandardCustomFieldInterface', ), + 'SprintProjectDetailsProfilePanel' => 'PhabricatorProfilePanel', 'SprintProjectProfileController' => 'SprintProjectController', + 'SprintProjectProfilePanelEngine' => 'PhabricatorProfilePanelEngine', 'SprintProjectViewController' => 'SprintController', + 'SprintProjectWorkboardProfilePanel' => 'PhabricatorProfilePanel', 'SprintQuery' => 'SprintDAO', 'SprintQueryTest' => 'SprintTestCase', 'SprintReportBurnUpView' => 'SprintView', diff --git a/src/controller/SprintController.php b/src/controller/SprintController.php index 02947e69dfbe75421ad953eaa69863b827dd9c51..a05d62535599f5bce1e2f29876be3ffd0ddb64d0 100644 --- a/src/controller/SprintController.php +++ b/src/controller/SprintController.php @@ -118,62 +118,6 @@ abstract class SprintController extends PhabricatorController { return $id; } - public function buildIconNavView(PhabricatorProject $project) { - $nav = $this->buildSprintIconNavView($project); - $nav->selectFilter(null); - return $nav; - } - - public function buildSprintIconNavView(PhabricatorProject $project) { - $viewer = $this->getViewer(); - $id = $project->getID(); - $picture = $project->getProfileImageURI(); - $name = $project->getName(); - $enable_phragile = PhabricatorEnv::getEnvConfig('sprint.enable-phragile'); - $phragile_base_uri = PhabricatorEnv::getEnvConfig('sprint.phragile-uri'); - $phragile_uri = new PhutilURI($phragile_base_uri.$id); - $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($viewer) - ->withProjectPHIDs(array($project->getPHID())) - ->execute(); - if ($columns) { - $board_icon = 'fa-columns'; - } else { - $board_icon = 'fa-columns grey'; - } - - $nav = new AphrontSideNavFilterView(); - $nav->setIconNav(true); - if ($this->isSprint($project) !== false) { - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - $nav->addIcon("profile/{$id}/", $name, null, $picture, null); - $nav->addIcon("burn/{$id}/", pht('Burndown'), 'fa-fire', null, null); - if ($enable_phragile) { - $nav->addIcon("sprints/{$id}/", pht('Phragile'), 'fa-pie-chart', null, $phragile_uri); - } - $nav->addIcon("board/{$id}/", pht('Sprint Board'), $board_icon, null, null); - $nav->addIcon('.', pht('Sprint List'), 'fa-bar-chart', null, null); - } else { - $nav->setBaseURI(new PhutilURI($this->getProjectsURI())); - $nav->addIcon("profile/{$id}/", $name, null, $picture); - $nav->addIcon("board/{$id}/", pht('Workboard'), $board_icon); - } - $class = 'PhabricatorManiphestApplication'; - if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - $phid = $project->getPHID(); - $query_uri = urisprintf( - '/maniphest/?statuses=open()&projects=%s#R', - $phid); - $nav->addIcon(null, pht('Open Tasks'), 'fa-anchor', null, $query_uri); - } - - $nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o', null, null); - $nav->addIcon("members/{$id}/", pht('Members'), 'fa-group', null, null); - $nav->addIcon("edit/{$id}/", pht('Edit Details'), 'fa-pencil', null, null); - - return $nav; - } - protected function isSprint($object) { $validator = new SprintValidator(); $issprint = call_user_func(array($validator, 'checkForSprint'), diff --git a/src/controller/SprintDataViewController.php b/src/controller/SprintDataViewController.php index 50390f6b240bf36445c63a10eebcb0b7a989c5fc..5113cfffae4f3821e19b6f1cedcb7c3e7d7bbce2 100755 --- a/src/controller/SprintDataViewController.php +++ b/src/controller/SprintDataViewController.php @@ -6,12 +6,21 @@ final class SprintDataViewController extends SprintController { private $request; private $viewer; private $project; + private $profileMenu; - public function buildIconNavView(PhabricatorProject $project) { - $id = $project->getID(); - $nav = parent::buildIconNavView($project); - $nav->selectFilter("burn/{$id}/"); - return $nav; + public function getProfileMenu(PhabricatorProject $project) { + if (!$this->profileMenu) { + if ($project) { + $viewer = $this->getViewer(); + + $engine = id(new SprintProjectProfilePanelEngine()) + ->setViewer($viewer) + ->setProfileObject($project); + + $this->profileMenu = $engine->buildNavigation(); + } + } + return $this->profileMenu; } public function handleRequest(AphrontRequest $request) { @@ -35,19 +44,14 @@ final class SprintDataViewController extends SprintController { $can_create = $this->hasApplicationCapability( ProjectCreateProjectsCapability::CAPABILITY); $crumbs = $this->getCrumbs($can_create); - $nav = $this->buildIconNavView($this->project); - $nav->appendChild( - array($crumbs, - $error_box, - $sprintdata_view, - )); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => array(pht('Burndown'), $this->project->getName()), - 'device' => true, - )); + $nav = $this->getProfileMenu($this->project); + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle($this->project->getName()) + ->setPageObjectPHIDs(array($this->project->getPHID())) + ->appendChild($error_box) + ->appendChild($sprintdata_view); } public function loadProject() { diff --git a/src/controller/SprintProjectController.php b/src/controller/SprintProjectController.php index 8866971f7956d6df0524907f420eea50e96e1862..52a9eef5dc107536b33b070f31e6fe309c5f7024 100644 --- a/src/controller/SprintProjectController.php +++ b/src/controller/SprintProjectController.php @@ -3,6 +3,7 @@ abstract class SprintProjectController extends SprintController { private $project; + private $profileMenu; protected function setProject(PhabricatorProject $project) { $this->project = $project; @@ -80,7 +81,33 @@ abstract class SprintProjectController extends SprintController { } public function buildApplicationMenu() { - return $this->buildSideNavView(true)->getMenu(); + $menu = $this->newApplicationMenu(); + + $profile_menu = $this->getProfileMenu(); + if ($profile_menu) { + $menu->setProfileMenu($profile_menu); + } + + $menu->setSearchEngine(new PhabricatorProjectSearchEngine()); + + return $menu; + } + + protected function getProfileMenu() { + if (!$this->profileMenu) { + $project = $this->getProject(); + if ($project) { + $viewer = $this->getViewer(); + + $engine = id(new SprintProjectProfilePanelEngine()) + ->setViewer($viewer) + ->setProfileObject($project); + + $this->profileMenu = $engine->buildNavigation(); + } + } + + return $this->profileMenu; } public function buildSideNavView($for_app = false) { @@ -115,66 +142,6 @@ abstract class SprintProjectController extends SprintController { return $nav; } - public function buildIconNavView(PhabricatorProject $project) { - $this->setProject($project); - $viewer = $this->getViewer(); - $id = $project->getID(); - $picture = $project->getProfileImageURI(); - $name = $project->getName(); - $enable_phragile = PhabricatorEnv::getEnvConfig('sprint.enable-phragile'); - $phragile_base_uri = PhabricatorEnv::getEnvConfig('sprint.phragile-uri'); - $phragile_uri = new PhutilURI($phragile_base_uri.$id); - - $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($viewer) - ->withProjectPHIDs(array($project->getPHID())) - ->execute(); - if ($columns) { - $board_icon = 'fa-columns'; - } else { - $board_icon = 'fa-columns grey'; - } - - $nav = new AphrontSideNavFilterView(); - $nav->setIconNav(true); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - if ($this->isSprint($project) !== false) { - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - $nav->addIcon("profile/{$id}/", $name, null, $picture, null); - $nav->addIcon("burn/{$id}/", pht('Burndown'), 'fa-fire', null, null); - if ($enable_phragile) { - $nav->addIcon("sprints/{$id}/", pht('Phragile'), 'fa-pie-chart', null, $phragile_uri); - } - $nav->addIcon("board/{$id}/", pht('Sprint Board'), $board_icon, null, null); - $nav->addIcon('.', pht('Sprint List'), 'fa-bar-chart', null, null); - } else { - $nav->setBaseURI(new PhutilURI($this->getProjectsURI())); - $nav->addIcon("profile/{$id}/", $name, null, $picture); - $nav->addIcon("board/{$id}/", pht('Workboard'), $board_icon); - } - $class = 'PhabricatorManiphestApplication'; - if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - $phid = $project->getPHID(); - - $query_uri = urisprintf( - '/maniphest/?statuses=open()&projects=%s#R', - $phid); - $nav->addIcon(null, pht('Open Tasks'), 'fa-anchor', null, $query_uri); - } - - $nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o'); - $nav->addIcon("members/{$id}/", pht('Members'), 'fa-group'); - $nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil'); - - if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { - $nav->addIcon("subprojects/{$id}/", pht('Subprojects'), 'fa-sitemap'); - $nav->addIcon("milestones/{$id}/", pht('Milestones'), 'fa-map-marker'); - } - - - return $nav; - } - protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); diff --git a/src/controller/SprintProjectProfileController.php b/src/controller/SprintProjectProfileController.php index addd70f6fbef2c629ae4d2948ff7da936a74fb85..46d00a23a5181eaffb565ffc4e53daf65617d3ba 100644 --- a/src/controller/SprintProjectProfileController.php +++ b/src/controller/SprintProjectProfileController.php @@ -44,8 +44,8 @@ final class SprintProjectProfileController new PhabricatorProjectTransactionQuery()); $timeline->setShouldTerminate(true); - $nav = $this->buildIconNavView($project); - $nav->selectFilter("profile/{$id}/"); + $nav = $this->getProfileMenu(); + $nav->selectFilter(PhabricatorProject::PANEL_PROFILE); $crumbs = $this->buildApplicationCrumbs(); return $this->newPage() @@ -76,7 +76,9 @@ final class SprintProjectProfileController id(new PhabricatorActionView()) ->setName(pht('Edit Details')) ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("edit/{$id}/"))); + ->setHref($this->getApplicationURI("edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) @@ -104,6 +106,26 @@ final class SprintProjectProfileController ->setWorkflow(true)); } + $can_lock = $can_edit && $this->hasApplicationCapability( + ProjectCanLockProjectsCapability::CAPABILITY); + + if ($project->getIsMembershipLocked()) { + $lock_name = pht('Unlock Project'); + $lock_icon = 'fa-unlock'; + } else { + $lock_name = pht('Lock Project'); + $lock_icon = 'fa-lock'; + } + + $view->addAction( + id(new PhabricatorActionView()) + ->setName($lock_name) + ->setIcon($lock_icon) + ->setHref($this->getApplicationURI("lock/{$id}/")) + ->setDisabled(!$can_lock) + ->setWorkflow(true)); + + $action = null; if (!$project->isUserMember($viewer->getPHID())) { $can_join = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -164,7 +186,9 @@ final class SprintProjectProfileController ->setName('#'.$slug->getSlug()); } - $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags)); + if ($hashtags) { + $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags)); + } $view->addProperty( pht('Members'), diff --git a/src/controller/board/SprintBoardColumnDetailController.php b/src/controller/board/SprintBoardColumnDetailController.php index 6f3bfc5a9f9b189290b087ecc10629147d87af67..9e59ecfae2fbe0105ec981ffa236bf4195b70e53 100644 --- a/src/controller/board/SprintBoardColumnDetailController.php +++ b/src/controller/board/SprintBoardColumnDetailController.php @@ -50,15 +50,16 @@ final class SprintBoardColumnDetailController ->setHeader($header) ->addPropertyList($properties); - $nav = $this->buildIconNavView($project); - $nav->appendChild($box); - $nav->appendChild($timeline); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - )); + $nav = $this->getProfileMenu(); + + return $this->newPage() + ->setTitle($title) + ->setNavigation($nav) + ->appendChild( + array( + $box, + $timeline, + )); } private function buildHeaderView(PhabricatorProjectColumn $column) { diff --git a/src/controller/board/SprintBoardColumnEditController.php b/src/controller/board/SprintBoardColumnEditController.php index afd2ea58180f93bd66db2e90738bd3aa47a0b4e5..3c79119f41be4de79bb7526dc8f2ee624238ad43 100644 --- a/src/controller/board/SprintBoardColumnEditController.php +++ b/src/controller/board/SprintBoardColumnEditController.php @@ -144,13 +144,11 @@ final class SprintBoardColumnEditController ->setValidationException($validation_exception) ->setForm($form); - $nav = $this->buildIconNavView($project); - $nav->appendChild($form_box); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - )); + $nav = $this->getProfileMenu(); + + return $this->newPage() + ->setTitle($title) + ->setNavigation($nav) + ->appendChild($form_box); } } diff --git a/src/controller/board/SprintBoardController.php b/src/controller/board/SprintBoardController.php index 423a8423b2cff37d609f22712f4d7970ff94b42b..7fd1809f0ab62b8bafaf6f19b59384c6700afea4 100644 --- a/src/controller/board/SprintBoardController.php +++ b/src/controller/board/SprintBoardController.php @@ -1,7 +1,7 @@ <?php abstract class SprintBoardController - extends SprintController { + extends SprintProjectController { private $project; @@ -13,11 +13,12 @@ abstract class SprintBoardController return $this->project; } - public function buildIconNavView(PhabricatorProject $project) { - $id = $project->getID(); - $nav = parent::buildIconNavView($project); - $nav->selectFilter("board/{$id}/"); - $nav->addClass('project-board-nav'); - return $nav; + protected function getProfileMenu() { + $menu = parent::getProfileMenu(); + + $menu->selectFilter(PhabricatorProject::PANEL_WORKBOARD); + $menu->addClass('project-board-nav'); + + return $menu; } } diff --git a/src/controller/board/SprintBoardViewController.php b/src/controller/board/SprintBoardViewController.php index 76a741dcb73c1552cf1e2e672690bf6cf4e58eca..7b4f7574ed79f829de435b315073d1085fbf5ba1 100755 --- a/src/controller/board/SprintBoardViewController.php +++ b/src/controller/board/SprintBoardViewController.php @@ -69,6 +69,13 @@ final class SprintBoardViewController // TODO: Expand the checks here if we add the ability // to hide the Backlog column if (!$columns) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + return $this->noAccessDialog($project); + } switch ($request->getStr('initialize-type')) { case 'backlog-only': $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); @@ -409,17 +416,22 @@ final class SprintBoardViewController ->appendChild($board) ->addClass('project-board-wrapper'); - $nav = $this->buildIconNavView($project); - $nav->appendChild($header_box); - $nav->appendChild($board_box); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => pht('%s Board', $project->getName()), - 'showFooter' => false, - 'pageObjects' => array($project->getPHID()), - )); + $nav = $this->getProfileMenu(); + + return $this->newPage() + ->setTitle(pht('%s Board', $project->getName())) + ->setPageObjectPHIDs(array($project->getPHID())) + ->setShowFooter(false) + ->setNavigation($nav) + ->addQuicksandConfig( + array( + 'boardConfig' => $behavior_config, + )) + ->appendChild( + array( + $header_box, + $board_box, + )); } private function buildSortMenu( diff --git a/src/engine/SprintProjectDetailsProfilePanel.php b/src/engine/SprintProjectDetailsProfilePanel.php new file mode 100644 index 0000000000000000000000000000000000000000..a468e56cdef1586665f6a68eedee68a1ac0e7926 --- /dev/null +++ b/src/engine/SprintProjectDetailsProfilePanel.php @@ -0,0 +1,59 @@ +<?php + +final class SprintProjectDetailsProfilePanel + extends PhabricatorProfilePanel { + + const PANELKEY = 'sprint.details'; + + public function getPanelTypeName() { + return pht('Sprint Details'); + } + + private function getDefaultName() { + return pht('Sprint Details'); + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + $name = $config->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(); + $picture = $project->getProfileImageURI(); + $name = $project->getName(); + + $href = "/project/sprint/profile/{$id}/"; + + $item = $this->newItem() + ->setHref($href) + ->setName($name) + ->setProfileImage($picture); + + return array( + $item, + ); + } + +} diff --git a/src/engine/SprintProjectProfilePanelEngine.php b/src/engine/SprintProjectProfilePanelEngine.php new file mode 100644 index 0000000000000000000000000000000000000000..ff191c5585bd1300853bc45f246d236866a35fd1 --- /dev/null +++ b/src/engine/SprintProjectProfilePanelEngine.php @@ -0,0 +1,52 @@ +<?php + +final class SprintProjectProfilePanelEngine + extends PhabricatorProfilePanelEngine { + + protected function getPanelURI($path) { + $project = $this->getProfileObject(); + $id = $project->getID(); + return "/project/{$id}/panel/{$path}"; + } + + protected function getBuiltinProfilePanels($object) { + $panels = array(); + + $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_PROFILE) + ->setPanelKey(SprintProjectDetailsProfilePanel::PANELKEY); + + $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_WORKBOARD) + ->setPanelKey(SprintProjectWorkboardProfilePanel::PANELKEY); + + $id = $object->getID(); + // 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); + + // TODO: This is temporary. + + $panels[] = $this->newPanel() + ->setBuiltinKey('feed') + ->setPanelKey(PhabricatorLinkProfilePanel::PANELKEY) + ->setPanelProperty('icon', 'feed') + ->setPanelProperty('name', pht('Feed')) + ->setPanelProperty('uri', "/project/feed/{$id}/"); + + $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_MEMBERS) + ->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY); + + return $panels; + } + +} diff --git a/src/profilepanel/SprintProjectWorkboardProfilePanel.php b/src/profilepanel/SprintProjectWorkboardProfilePanel.php new file mode 100644 index 0000000000000000000000000000000000000000..d1a8a1e429f93ef481c520b2d8a5ecb7d0964958 --- /dev/null +++ b/src/profilepanel/SprintProjectWorkboardProfilePanel.php @@ -0,0 +1,74 @@ +<?php + +final class SprintProjectWorkboardProfilePanel + extends PhabricatorProfilePanel { + + const PANELKEY = 'sprint.workboard'; + + public function getPanelTypeName() { + return pht('Project Workboard'); + } + + private function getDefaultName() { + return pht('Workboard'); + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + $name = $config->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) { + $viewer = $this->getViewer(); + + // Workboards are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return array(); + } + + $project = $config->getProfileObject(); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array($project->getPHID())) + ->execute(); + if ($columns) { + $icon = 'fa-columns'; + } else { + $icon = 'fa-columns grey'; + } + + $id = $project->getID(); + $href = "/project/sprint/board/{$id}/"; + $name = $this->getDisplayName($config); + + $item = $this->newItem() + ->setHref($href) + ->setName($name) + ->setIcon($icon); + + return array( + $item, + ); + } + +}