diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php index 98067f435c..e4f30dc345 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -1,176 +1,172 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needMembers(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } - $profile = $project->loadProfile(); - if (empty($profile)) { - $profile = new PhabricatorProjectProfile(); - } $member_phids = $project->getMemberPHIDs(); $errors = array(); if ($request->isFormPost()) { $changed_something = false; $member_map = array_fill_keys($member_phids, true); $remove = $request->getStr('remove'); if ($remove) { if (isset($member_map[$remove])) { unset($member_map[$remove]); $changed_something = true; } } else { $new_members = $request->getArr('phids'); foreach ($new_members as $member) { if (empty($member_map[$member])) { $member_map[$member] = true; $changed_something = true; } } } $xactions = array(); if ($changed_something) { $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_MEMBERS); $xaction->setNewValue(array_keys($member_map)); $xactions[] = $xaction; } if ($xactions) { $editor = new PhabricatorProjectEditor($project); $editor->setActor($user); $editor->applyTransactions($xactions); } return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()); } $member_phids = array_reverse($member_phids); $handles = $this->loadViewerHandles($member_phids); $state = array(); foreach ($handles as $handle) { $state[] = array( 'phid' => $handle->getPHID(), 'name' => $handle->getFullName(), ); } $header_name = pht('Edit Members'); $title = pht('Edit Members'); $list = $this->renderMemberList($handles); $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('phids') ->setLabel(pht('Add Members')) ->setDatasource('/typeahead/common/users/')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue(pht('Add Members'))); $faux_form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormInsetView()) ->appendChild($list)); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Current Members (%d)', count($handles))) ->setForm($faux_form); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($project->getName()) ->setHref('/project/view/'.$project->getID().'/')); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName(pht('Edit Members')) ->setHref($this->getApplicationURI())); return $this->buildApplicationPage( array( $crumbs, $form_box, $box, ), array( 'title' => $title, 'device' => true, )); } private function renderMemberList(array $handles) { $request = $this->getRequest(); $user = $request->getUser(); $list = id(new PhabricatorObjectListView()) ->setHandles($handles); foreach ($handles as $handle) { $hidden_input = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'remove', 'value' => $handle->getPHID(), ), ''); $button = javelin_tag( 'button', array( 'class' => 'grey', ), pht('Remove')); $list->addButton( $handle, phabricator_form( $user, array( 'method' => 'POST', 'action' => $request->getRequestURI(), ), array($hidden_input, $button))); } return $list; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index a1e083eff5..953cbb5c11 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,265 +1,260 @@ id = idx($data, 'id'); $this->page = idx($data, 'page'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $query = id(new PhabricatorProjectQuery()) + $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) - ->needMembers(true); - - $project = $query->executeOne(); - $this->project = $project; + ->needMembers(true) + ->needProfiles(true) + ->executeOne(); if (!$project) { return new Aphront404Response(); } - $profile = $project->loadProfile(); - if (!$profile) { - $profile = new PhabricatorProjectProfile(); - } + $profile = $project->getProfile(); $picture = $profile->loadProfileImageURI(); require_celerity_resource('phabricator-profile-css'); $tasks = $this->renderTasksPage($project, $profile); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $project->getPHID(), )); $query->setLimit(50); $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $feed = $this->renderStories($stories); $people = $this->renderPeoplePage($project, $profile); $content = id(new AphrontMultiColumnView()) ->addColumn($people) ->addColumn($feed) ->setFluidLayout(true); $content = hsprintf( '
%s%s
', $tasks, $content); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) ->setSubheader(phutil_utf8_shorten($profile->getBlurb(), 1024)) ->setImage($picture); $actions = $this->buildActionListView($project); $properties = $this->buildPropertyListView($project); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($project->getName())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setActionList($actions) ->setPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => $project->getName(), 'device' => true, )); } private function renderPeoplePage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $member_phids = $project->getMemberPHIDs(); $handles = $this->loadViewerHandles($member_phids); $affiliated = array(); foreach ($handles as $phids => $handle) { $affiliated[] = phutil_tag('li', array(), $handle->renderLink()); } if ($affiliated) { $affiliated = phutil_tag('ul', array(), $affiliated); } else { $affiliated = hsprintf('

%s

', pht( 'No one is affiliated with this project.')); } return hsprintf( '
'. '

%s

'. '
%s
'. '
', pht('People'), $affiliated); } private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $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 hsprintf( '
'. '%s'. '
', $view->render()); } private function renderTasksPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $user = $this->getRequest()->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withAnyProjects(array($project->getPHID())) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->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); $list = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($task_list); $content = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Open Tasks')) ->appendChild($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('edit') ->setHref($this->getApplicationURI("edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) ->setIcon('edit') ->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('new') ->setDisabled(!$can_join) ->setName(pht('Join Project')); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/update/'.$project->getID().'/leave/') ->setIcon('delete') ->setName(pht('Leave Project...')); } $view->addAction($action); return $view; } private function buildPropertyListView(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PhabricatorPropertyListView()) ->setUser($viewer) ->setObject($project); $view->addProperty( pht('Created'), phabricator_datetime($project->getDateCreated(), $viewer)); return $view; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index 2498231ddf..608438ce96 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -1,247 +1,244 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) + ->needProfiles(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } - $profile = $project->loadProfile(); - if (empty($profile)) { - $profile = new PhabricatorProjectProfile(); - } - + $profile = $project->getProfile(); $img_src = $profile->loadProfileImageURI(); $options = PhabricatorProjectStatus::getStatusMap(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_name = true; $e_image = null; $errors = array(); if ($request->isFormPost()) { try { $xactions = array(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_NAME); $xaction->setNewValue($request->getStr('name')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_STATUS); $xaction->setNewValue($request->getStr('status')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_CAN_VIEW); $xaction->setNewValue($request->getStr('can_view')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_CAN_EDIT); $xaction->setNewValue($request->getStr('can_edit')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_CAN_JOIN); $xaction->setNewValue($request->getStr('can_join')); $xactions[] = $xaction; $editor = new PhabricatorProjectEditor($project); $editor->setActor($user); $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = pht('Not Unique'); $errors[] = $ex->getMessage(); } $profile->setBlurb($request->getStr('blurb')); if (!strlen($project->getName())) { $e_name = pht('Required'); $errors[] = pht('Project name is required.'); } else { $e_name = null; } $default_image = $request->getExists('default_image'); if ($default_image) { $profile->setProfileImagePHID(null); } else if (!empty($_FILES['image'])) { $err = idx($_FILES['image'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $user->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeThumbTransform( $file, $x = 50, $y = 50); $profile->setProfileImagePHID($xformed->getPHID()); } else { $e_image = pht('Not Supported'); $errors[] = pht('This server only supports these image formats:').' '. implode(', ', $supported_formats).'.'; } } } if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } $header_name = pht('Edit Project'); $title = pht('Edit Project'); $action = '/project/edit/'.$project->getID().'/'; $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($project) ->execute(); $form = new AphrontFormView(); $form ->setID('project-edit-form') ->setUser($user) ->setAction($action) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Project Status')) ->setName('status') ->setOptions($options) ->setValue($project->getStatus())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Blurb')) ->setName('blurb') ->setValue($profile->getBlurb())) ->appendChild(hsprintf( '

%s

', pht( 'NOTE: Policy settings are not yet fully implemented. '. 'Some interfaces still ignore these settings, '. 'particularly "Visible To".'))) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_view') ->setCaption(pht('Members can always view a project.')) ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_edit') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_join') ->setCaption( pht('Users who can edit a project can always join a project.')) ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Profile Image')) ->setValue( phutil_tag( 'img', array( 'src' => $img_src, )))) ->appendChild( id(new AphrontFormImageControl()) ->setLabel(pht('Change Image')) ->setName('image') ->setError($e_image) ->setCaption( pht('Supported formats:').' '.implode(', ', $supported_formats))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue(pht('Save'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($project->getName()) ->setHref('/project/view/'.$project->getID().'/')); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName(pht('Edit Project')) ->setHref($this->getApplicationURI())); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 820f159d93..0f4c6440e2 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,207 +1,228 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withPhrictionSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } + public function needProfiles($need_profiles) { + $this->needProfiles = $need_profiles; + return $this; + } + protected function getPagingColumn() { return 'name'; } protected function getPagingValue($result) { return $result->getName(); } protected function getReversePaging() { return true; } protected function loadPage() { $table = new PhabricatorProject(); $conn_r = $table->establishConnection('r'); // NOTE: Because visibility checks for projects depend on whether or not // the user is a project member, we always load their membership. If we're // loading all members anyway we can piggyback on that; otherwise we // do an explicit join. $select_clause = ''; if (!$this->needMembers) { $select_clause = ', vm.dst viewerIsMember'; } $data = queryfx_all( $conn_r, 'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q', $select_clause, $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $projects = $table->loadAllFromArray($data); if ($projects) { $viewer_phid = $this->getViewer()->getPHID(); if ($this->needMembers) { $etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; $members = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($projects, 'getPHID')) ->withEdgeTypes(array($etype)) ->execute(); foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachMemberPHIDs(array_keys($members[$phid][$etype])); $project->setIsUserMember( $viewer_phid, isset($members[$phid][$etype][$viewer_phid])); } } else { foreach ($data as $row) { $projects[$row['id']]->setIsUserMember( $viewer_phid, ($row['viewerIsMember'] !== null)); } } + + if ($this->needProfiles) { + $profiles = id(new PhabricatorProjectProfile())->loadAllWhere( + 'projectPHID IN (%Ls)', + mpull($projects, 'getPHID')); + $profiles = mpull($profiles, null, 'getProjectPHID'); + foreach ($projects as $project) { + $profile = idx($profiles, $project->getPHID()); + if (!$profile) { + $profile = id(new PhabricatorProjectProfile()) + ->setProjectPHID($project->getPHID()); + } + $project->attachProfile($profile); + } + } } return $projects; } private function buildWhereClause($conn_r) { $where = array(); if ($this->status != self::STATUS_ANY) { switch ($this->status) { case self::STATUS_OPEN: case self::STATUS_ACTIVE: $filter = array( PhabricatorProjectStatus::STATUS_ACTIVE, ); break; case self::STATUS_CLOSED: case self::STATUS_ARCHIVED: $filter = array( PhabricatorProjectStatus::STATUS_ARCHIVED, ); break; default: throw new Exception( "Unknown project status '{$this->status}'!"); } $where[] = qsprintf( $conn_r, 'status IN (%Ld)', $filter); } if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs) { $where[] = qsprintf( $conn_r, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->slugs) { $where[] = qsprintf( $conn_r, 'phrictionSlug IN (%Ls)', $this->slugs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function buildGroupClause($conn_r) { if ($this->memberPHIDs) { return 'GROUP BY p.id'; } else { return ''; } } private function buildJoinClause($conn_r) { $joins = array(); if (!$this->needMembers) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_PROJ_MEMBER, $this->getViewer()->getPHID()); } if ($this->memberPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_PROJ_MEMBER); } return implode(' ', $joins); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 78cebb50cf..4d3ec45b6d 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -1,133 +1,136 @@ getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isUserMember($viewer->getPHID())) { // Project members can always view a project. return true; } break; case PhabricatorPolicyCapability::CAN_EDIT: break; case PhabricatorPolicyCapability::CAN_JOIN: $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { // Project editors can always join a project. return true; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht("Members of a project can always view it."); case PhabricatorPolicyCapability::CAN_JOIN: return pht("Users who can edit a project can always join it."); } return null; } public function isUserMember($user_phid) { return $this->assertAttachedKey($this->sparseMembers, $user_phid); } public function setIsUserMember($user_phid, $is_member) { if ($this->sparseMembers === self::ATTACHABLE) { $this->sparseMembers = array(); } $this->sparseMembers[$user_phid] = $is_member; return $this; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'subprojectPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectPHIDTypeProject::TYPECONST); } - public function loadProfile() { - $profile = id(new PhabricatorProjectProfile())->loadOneWhere( - 'projectPHID = %s', - $this->getPHID()); - return $profile; + public function getProfile() { + return $this->assertAttached($this->profile); + } + + public function attachProfile(PhabricatorProjectProfile $profile) { + $this->profile = $profile; + return $this; } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function setPhrictionSlug($slug) { // NOTE: We're doing a little magic here and stripping out '/' so that // project pages always appear at top level under projects/ even if the // display name is "Hack / Slash" or similar (it will become // 'hack_slash' instead of 'hack/slash'). $slug = str_replace('/', ' ', $slug); $slug = PhabricatorSlug::normalize($slug); $this->phrictionSlug = $slug; return $this; } public function getFullPhrictionSlug() { $slug = $this->getPhrictionSlug(); return 'projects/'.$slug; } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php index fb0c5a66d2..74a4f35563 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php @@ -1,379 +1,380 @@ type = $data['type']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $raw_query = $request->getStr('raw'); $need_rich_data = false; $need_users = false; $need_agents = false; $need_applications = false; $need_all_users = false; $need_lists = false; $need_projs = false; $need_repos = false; $need_packages = false; $need_upforgrabs = false; $need_arcanist_projects = false; $need_noproject = false; $need_symbols = false; $need_jump_objects = false; switch ($this->type) { case 'mainsearch': $need_users = true; $need_applications = true; $need_rich_data = true; $need_symbols = true; $need_projs = true; $need_jump_objects = true; break; case 'searchowner': $need_users = true; $need_upforgrabs = true; break; case 'searchproject': $need_projs = true; $need_noproject = true; break; case 'users': $need_users = true; break; case 'authors': $need_users = true; $need_agents = true; break; case 'mailable': $need_users = true; $need_lists = true; break; case 'allmailable': $need_users = true; $need_all_users = true; $need_lists = true; break; case 'projects': $need_projs = true; break; case 'usersorprojects': $need_users = true; $need_projs = true; break; case 'repositories': $need_repos = true; break; case 'packages': $need_packages = true; break; case 'accounts': $need_users = true; $need_all_users = true; break; case 'accountsorprojects': $need_users = true; $need_all_users = true; $need_projs = true; break; case 'arcanistprojects': $need_arcanist_projects = true; break; } $results = array(); if ($need_upforgrabs) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('upforgrabs (Up For Grabs)') ->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS); } if ($need_noproject) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('noproject (No Project)') ->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT); } if ($need_users) { $columns = array( 'isSystemAgent', 'isAdmin', 'isDisabled', 'userName', 'realName', 'phid'); if ($query) { // This is an arbitrary limit which is just larger than any limit we // actually use in the application. // TODO: The datasource should pass this in the query. $limit = 15; $user_table = new PhabricatorUser(); $conn_r = $user_table->establishConnection('r'); $ids = queryfx_all( $conn_r, 'SELECT id FROM %T WHERE username LIKE %> ORDER BY username ASC LIMIT %d', $user_table->getTableName(), $query, $limit); $ids = ipull($ids, 'id'); if (count($ids) < $limit) { // If we didn't find enough username hits, look for real name hits. // We need to pull the entire pagesize so that we end up with the // right number of items if this query returns many duplicate IDs // that we've already selected. $realname_ids = queryfx_all( $conn_r, 'SELECT DISTINCT userID FROM %T WHERE token LIKE %> ORDER BY token ASC LIMIT %d', PhabricatorUser::NAMETOKEN_TABLE, $query, $limit); $realname_ids = ipull($realname_ids, 'userID'); $ids = array_merge($ids, $realname_ids); $ids = array_unique($ids); $ids = array_slice($ids, 0, $limit); } // Always add the logged-in user because some tokenizers autosort them // first. They'll be filtered out on the client side if they don't // match the query. $ids[] = $request->getUser()->getID(); if ($ids) { $users = id(new PhabricatorUser())->loadColumnsWhere( $columns, 'id IN (%Ld)', $ids); } else { $users = array(); } } else { $users = id(new PhabricatorUser())->loadColumns($columns); } if ($need_rich_data) { $phids = mpull($users, 'getPHID'); $handles = $this->loadViewerHandles($phids); } foreach ($users as $user) { if (!$need_all_users) { if (!$need_agents && $user->getIsSystemAgent()) { continue; } if ($user->getIsDisabled()) { continue; } } $result = id(new PhabricatorTypeaheadResult()) ->setName($user->getFullName()) ->setURI('/p/'.$user->getUsername()) ->setPHID($user->getPHID()) ->setPriorityString($user->getUsername()); if ($need_rich_data) { $display_type = 'User'; if ($user->getIsAdmin()) { $display_type = 'Administrator'; } $result->setDisplayType($display_type); $result->setImageURI($handles[$user->getPHID()]->getImageURI()); $result->setPriorityType('user'); } $results[] = $result; } } if ($need_lists) { $lists = id(new PhabricatorMailingListQuery()) ->setViewer($viewer) ->execute(); foreach ($lists as $list) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($list->getName()) ->setURI($list->getURI()) ->setPHID($list->getPHID()); } } if ($need_projs) { $projs = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withStatus(PhabricatorProjectQuery::STATUS_OPEN) + ->needProfiles(true) ->execute(); foreach ($projs as $proj) { $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setDisplayType("Project") ->setURI('/project/view/'.$proj->getID().'/') ->setPHID($proj->getPHID()); - $prof = $proj->loadProfile(); + $prof = $proj->getProfile(); if ($prof) { $proj_result->setImageURI($prof->loadProfileImageURI()); } $results[] = $proj_result; } } if ($need_repos) { $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->execute(); foreach ($repos as $repo) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('r'.$repo->getCallsign().' ('.$repo->getName().')') ->setURI('/diffusion/'.$repo->getCallsign().'/') ->setPHID($repo->getPHID()) ->setPriorityString('r'.$repo->getCallsign()); } } if ($need_packages) { $packages = id(new PhabricatorOwnersPackage())->loadAll(); foreach ($packages as $package) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($package->getName()) ->setURI('/owners/package/'.$package->getID().'/') ->setPHID($package->getPHID()); } } if ($need_arcanist_projects) { $arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll(); foreach ($arcprojs as $proj) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setPHID($proj->getPHID()); } } if ($need_applications) { $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { $uri = $application->getTypeaheadURI(); if (!$uri) { continue; } $name = $application->getName().' '.$application->getShortDescription(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($uri) ->setPHID($application->getPHID()) ->setPriorityString($application->getName()) ->setDisplayName($application->getName()) ->setDisplayType($application->getShortDescription()) ->setImageuRI($application->getIconURI()) ->setPriorityType('apps'); } } if ($need_symbols) { $symbols = id(new DiffusionSymbolQuery()) ->setNamePrefix($query) ->setLimit(15) ->needArcanistProjects(true) ->needRepositories(true) ->needPaths(true) ->execute(); foreach ($symbols as $symbol) { $lang = $symbol->getSymbolLanguage(); $name = $symbol->getSymbolName(); $type = $symbol->getSymbolType(); $proj = $symbol->getArcanistProject()->getName(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($symbol->getURI()) ->setPHID(md5($symbol->getURI())) // Just needs to be unique. ->setDisplayName($name) ->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')') ->setPriorityType('symb'); } } if ($need_jump_objects) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($raw_query)) ->execute(); if ($objects) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($objects, 'getPHID')) ->execute(); $handle = head($handles); if ($handle) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($handle->getFullName()) ->setDisplayType($handle->getTypeName()) ->setURI($handle->getURI()) ->setPHID($handle->getPHID()) ->setPriorityType('jump'); } } } $content = mpull($results, 'getWireFormat'); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } // If there's a non-Ajax request to this endpoint, show results in a tabular // format to make it easier to debug typeahead output. $rows = array(); foreach ($results as $result) { $wire = $result->getWireFormat(); $rows[] = $wire; } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Name', 'URI', 'PHID', 'Priority', 'Display Name', 'Display Type', 'Image URI', 'Priority Type', )); $panel = new AphrontPanelView(); $panel->setHeader('Typeahead Results'); $panel->appendChild($table); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Typeahead Results', )); } }