diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php index 494bd9fa1..0fe314f02 100644 --- a/src/applications/project/application/PhabricatorApplicationProject.php +++ b/src/applications/project/application/PhabricatorApplicationProject.php @@ -1,81 +1,85 @@ array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectListController', 'filter/(?P[^/]+)/' => 'PhabricatorProjectListController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorProjectEditMainController', 'details/(?P[1-9]\d*)/' => 'PhabricatorProjectEditDetailsController', 'archive/(?P[1-9]\d*)/' => 'PhabricatorProjectArchiveController', 'members/(?P[1-9]\d*)/' => 'PhabricatorProjectMembersEditController', 'members/(?P[1-9]\d*)/remove/' => 'PhabricatorProjectMembersRemoveController', 'view/(?P[1-9]\d*)/' => 'PhabricatorProjectProfileController', 'picture/(?P[1-9]\d*)/' => 'PhabricatorProjectEditPictureController', 'create/' => 'PhabricatorProjectCreateController', 'board/(?P[1-9]\d*)/'. '(?Pfilter/)?'. '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', 'board/(?P[1-9]\d*)/edit/(?:(?P\d+)/)?' => 'PhabricatorProjectBoardEditController', 'board/(?P[1-9]\d*)/delete/(?:(?P\d+)/)?' => 'PhabricatorProjectBoardDeleteController', 'board/(?P[1-9]\d*)/column/(?:(?P\d+)/)?' => 'PhabricatorProjectColumnDetailController', 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', 'history/(?P[1-9]\d*)/' => 'PhabricatorProjectHistoryController', '(?Pwatch|unwatch)/(?P[1-9]\d*)/' => 'PhabricatorProjectWatchController', + + ), + '/tag/' => array( + '(?P[^/]+)/' => 'PhabricatorProjectProfileController', ), ); } protected function getCustomCapabilities() { return array( ProjectCapabilityCreateProjects::CAPABILITY => array( ), ); } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 688f78ccd..5d701376f 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,293 +1,305 @@ id = idx($data, 'id'); + // via /tag/$slug/ + $this->slug = idx($data, 'slug'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $project = id(new PhabricatorProjectQuery()) + $query = id(new PhabricatorProjectQuery()) ->setViewer($user) - ->withIDs(array($this->id)) ->needMembers(true) ->needWatchers(true) - ->needImages(true) - ->executeOne(); + ->needImages(true); + if ($this->slug) { + $query->withSlugs(array($this->slug)); + } else { + $query->withIDs(array($this->id)); + } + $project = $query->executeOne(); if (!$project) { return new Aphront404Response(); } + if ($this->slug && $this->slug != $project->getPrimarySlug()) { + return id(new AphrontRedirectResponse()) + ->setURI('/tag/'.$project->getPrimarySlug().'/'); + } $picture = $project->getProfileImageURI(); require_celerity_resource('phabricator-profile-css'); $tasks = $this->renderTasksPage($project); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $project->getPHID(), )); $query->setLimit(50); $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $feed = $this->renderStories($stories); $content = phutil_tag_div( 'phabricator-project-layout', array($tasks, $feed)); $id = $this->id; $icon = id(new PHUIIconView()) ->setIconFont('fa-columns'); $board_btn = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Workboards')) ->setHref($this->getApplicationURI("board/{$id}/")) ->setIcon($icon); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) ->setUser($user) ->setPolicyObject($project) ->setImage($picture) ->addActionLink($board_btn); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { $header->setStatus('fa-ban', 'dark', pht('Archived')); } $actions = $this->buildActionListView($project); $properties = $this->buildPropertyListView($project, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($project->getName()) ->setActionList($actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => $project->getName(), 'device' => true, )); } private function renderFeedPage(PhabricatorProject $project) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); $query->setViewer($this->getRequest()->getUser()); $query->setLimit(100); $stories = $query->execute(); if (!$stories) { return pht('There are no stories about this project.'); } return $this->renderStories($stories); } private function renderStories(array $stories) { assert_instances_of($stories, 'PhabricatorFeedStory'); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $builder->setShowHovercards(true); $view = $builder->buildView(); return phutil_tag_div( 'profile-feed', $view->render()); } private function renderTasksPage(PhabricatorProject $project) { $user = $this->getRequest()->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withAnyProjects(array($project->getPHID())) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10); $tasks = $query->execute(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_merge( $phids, array_mergev(mpull($tasks, 'getProjectPHIDs'))); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $task_list = new ManiphestTaskListView(); $task_list->setUser($user); $task_list->setTasks($tasks); $task_list->setHandles($handles); $phid = $project->getPHID(); $view_uri = urisprintf( '/maniphest/?statuses=%s&allProjects=%s#R', implode(',', ManiphestTaskStatus::getOpenStatusConstants()), $phid); $create_uri = '/maniphest/task/create/?projects='.$phid; $icon = id(new PHUIIconView()) ->setIconFont('fa-list'); $button_view = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('View All')) ->setHref($view_uri) ->setIcon($icon); $icon_new = id(new PHUIIconView()) ->setIconFont('fa-plus'); $button_add = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('New Task')) ->setHref($create_uri) ->setIcon($icon_new); $header = id(new PHUIHeaderView()) ->setHeader(pht('Open Tasks')) ->addActionLink($button_add) ->addActionLink($button_view); $content = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($task_list); return $content; } private function buildActionListView(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $project->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($project) ->setObjectURI($request->getRequestURI()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Project')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("edit/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) ->setIcon('fa-users') ->setHref($this->getApplicationURI("members/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $action = null; if (!$project->isUserMember($viewer->getPHID())) { $can_join = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_JOIN); $action = id(new PhabricatorActionView()) ->setUser($viewer) ->setRenderAsForm(true) ->setHref('/project/update/'.$project->getID().'/join/') ->setIcon('fa-plus') ->setDisabled(!$can_join) ->setName(pht('Join Project')); $view->addAction($action); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/update/'.$project->getID().'/leave/') ->setIcon('fa-times') ->setName(pht('Leave Project...')); $view->addAction($action); if (!$project->isUserWatcher($viewer->getPHID())) { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/watch/'.$project->getID().'/') ->setIcon('fa-eye') ->setName(pht('Watch Project')); $view->addAction($action); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/unwatch/'.$project->getID().'/') ->setIcon('fa-eye-slash') ->setName(pht('Unwatch Project')); $view->addAction($action); } } return $view; } private function buildPropertyListView( PhabricatorProject $project, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $this->loadHandles( array_merge( $project->getMemberPHIDs(), $project->getWatcherPHIDs())); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($project) ->setActionList($actions); $view->addProperty( pht('Members'), $project->getMemberPHIDs() ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',') : phutil_tag('em', array(), pht('None'))); $view->addProperty( pht('Watchers'), $project->getWatcherPHIDs() ? $this->renderHandlesForPHIDs($project->getWatcherPHIDs(), ',') : phutil_tag('em', array(), pht('None'))); $field_list = PhabricatorCustomField::getObjectFields( $project, PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($project, $viewer, $view); return $view; } } diff --git a/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php b/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php index f67d36f63..bb7a5a1a0 100644 --- a/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php +++ b/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php @@ -1,112 +1,113 @@ withPHIDs($phids) ->needImages(true); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $project = $objects[$phid]; $name = $project->getName(); $id = $project->getID(); + $slug = $project->getPrimarySlug(); $handle->setName($name); - $handle->setObjectName('#'.rtrim($project->getPhrictionSlug(), '/')); - $handle->setURI("/project/view/{$id}/"); + $handle->setObjectName('#'.$slug); + $handle->setURI("/tag/{$slug}/"); $handle->setImageURI($project->getProfileImageURI()); if ($project->isArchived()) { $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); } } } public static function getProjectMonogramPatternFragment() { // NOTE: See some discussion in ProjectRemarkupRule. return '[^\s,#]+'; } public function canLoadNamedObject($name) { $fragment = self::getProjectMonogramPatternFragment(); return preg_match('/^#'.$fragment.'$/i', $name); } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { // If the user types "#YoloSwag", we still want to match "#yoloswag", so // we normalize, query, and then map back to the original inputs. $map = array(); foreach ($names as $key => $slug) { $map[$this->normalizeSlug(substr($slug, 1))][] = $slug; } $projects = id(new PhabricatorProjectQuery()) ->setViewer($query->getViewer()) ->withSlugs(array_keys($map)) ->needSlugs(true) ->execute(); $result = array(); foreach ($projects as $project) { $slugs = $project->getSlugs(); $slug_strs = mpull($slugs, 'getSlug'); foreach ($slug_strs as $slug) { $slug_map = idx($map, $slug, array()); foreach ($slug_map as $original) { $result[$original] = $project; } } } return $result; } private function normalizeSlug($slug) { // NOTE: We're using phutil_utf8_strtolower() (and not PhabricatorSlug's // normalize() method) because this normalization should be only somewhat // liberal. We want "#YOLO" to match against "#yolo", but "#\\yo!!lo" // should not. normalize() strips out most punctuation and leads to // excessively aggressive matches. return phutil_utf8_strtolower($slug); } }