diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 1dfc466712..4b6ad566a7 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,398 +1,393 @@ 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 withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withPhrictionSlugs(array $slugs) { $this->phrictionSlugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } - public function withDatasourceQuery($string) { - $this->datasourceQuery = $string; + public function withNameTokens(array $tokens) { + $this->nameTokens = array_values($tokens); return $this; } public function withIcons(array $icons) { $this->icons = $icons; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } public function needWatchers($need_watchers) { $this->needWatchers = $need_watchers; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needSlugs($need_slugs) { $this->needSlugs = $need_slugs; return $this; } protected function getDefaultOrderVector() { return array('name'); } public function getOrderableColumns() { return array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'reverse' => true, 'type' => 'string', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $project = $this->loadCursorObject($cursor); return array( 'name' => $project->getName(), ); } 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(); $project_phids = mpull($projects, 'getPHID'); $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $need_edge_types = array(); if ($this->needMembers) { $need_edge_types[] = $member_type; } else { foreach ($data as $row) { $projects[$row['id']]->setIsUserMember( $viewer_phid, ($row['viewerIsMember'] !== null)); } } if ($this->needWatchers) { $need_edge_types[] = $watcher_type; } if ($need_edge_types) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($project_phids) ->withEdgeTypes($need_edge_types) ->execute(); if ($this->needMembers) { foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachMemberPHIDs( array_keys($edges[$phid][$member_type])); $project->setIsUserMember( $viewer_phid, isset($edges[$phid][$member_type][$viewer_phid])); } } if ($this->needWatchers) { foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachWatcherPHIDs( array_keys($edges[$phid][$watcher_type])); $project->setIsUserWatcher( $viewer_phid, isset($edges[$phid][$watcher_type][$viewer_phid])); } } } } return $projects; } protected function didFilterPage(array $projects) { if ($this->needImages) { $default = null; $file_phids = mpull($projects, 'getProfileImagePHID'); $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($projects as $project) { $file = idx($files, $project->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'project.png'); } $file = $default; } $project->attachProfileImageFile($file); } } if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug()) ->loadAllWhere( 'projectPHID IN (%Ls)', mpull($projects, 'getPHID')); $slugs = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slugs, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } 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 !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn_r, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->slugs !== null) { $where[] = qsprintf( $conn_r, 'slug.slug IN (%Ls)', $this->slugs); } if ($this->phrictionSlugs !== null) { $where[] = qsprintf( $conn_r, 'phrictionSlug IN (%Ls)', $this->phrictionSlugs); } if ($this->names !== null) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } if ($this->icons !== null) { $where[] = qsprintf( $conn_r, 'icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn_r, 'color IN (%Ls)', $this->colors); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function buildGroupClause($conn_r) { - if ($this->memberPHIDs || $this->datasourceQuery) { + if ($this->memberPHIDs || $this->nameTokens) { return 'GROUP BY p.id'; } else { return $this->buildApplicationSearchGroupClause($conn_r); } } private function buildJoinClause($conn_r) { $joins = array(); if (!$this->needMembers !== null) { $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, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $this->getViewer()->getPHID()); } if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } - if ($this->datasourceQuery !== null) { - $tokens = PhabricatorTypeaheadDatasource::tokenizeString( - $this->datasourceQuery); - if (!$tokens) { - throw new PhabricatorEmptyQueryException(); + if ($this->nameTokens !== null) { + foreach ($this->nameTokens as $key => $token) { + $token_table = 'token_'.$key; + $joins[] = qsprintf( + $conn_r, + 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', + PhabricatorProject::TABLE_DATASOURCE_TOKEN, + $token_table, + $token_table, + $token_table, + $token); } - - $likes = array(); - foreach ($tokens as $token) { - $likes[] = qsprintf($conn_r, 'token.token LIKE %>', $token); - } - - $joins[] = qsprintf( - $conn_r, - 'JOIN %T token ON token.projectID = p.id AND (%Q)', - PhabricatorProject::TABLE_DATASOURCE_TOKEN, - '('.implode(') OR (', $likes).')'); } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getPrimaryTableAlias() { return 'p'; } } diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index ff2b1c790f..66462d5ecc 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -1,240 +1,241 @@ setParameter( 'memberPHIDs', $this->readUsersFromRequest($request, 'members')); $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('name', $request->getStr('name')); $saved->setParameter( 'icons', $this->readListFromRequest($request, 'icons')); $saved->setParameter( 'colors', $this->readListFromRequest($request, 'colors')); $this->readCustomFieldsFromRequest($request, $saved); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorProjectQuery()) ->needImages(true); $member_phids = $saved->getParameter('memberPHIDs', array()); if ($member_phids && is_array($member_phids)) { $query->withMemberPHIDs($member_phids); } $status = $saved->getParameter('status'); $status = idx($this->getStatusValues(), $status); if ($status) { $query->withStatus($status); } $name = $saved->getParameter('name'); if (strlen($name)) { - $query->withDatasourceQuery($name); + $tokens = PhabricatorTypeaheadDatasource::tokenizeString($name); + $query->withNameTokens($tokens); } $icons = $saved->getParameter('icons'); if ($icons) { $query->withIcons($icons); } $colors = $saved->getParameter('colors'); if ($colors) { $query->withColors($colors); } $this->applyCustomFieldsToQuery($query, $saved); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $member_phids = $saved->getParameter('memberPHIDs', array()); $status = $saved->getParameter('status'); $name_match = $saved->getParameter('name'); $icons = array_fuse($saved->getParameter('icons', array())); $colors = array_fuse($saved->getParameter('colors', array())); $icon_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Icons')); foreach (PhabricatorProjectIcon::getIconMap() as $icon => $name) { $image = id(new PHUIIconView()) ->setIconFont($icon); $icon_control->addCheckbox( 'icons[]', $icon, array($image, ' ', $name), isset($icons[$icon])); } $color_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Colors')); foreach (PhabricatorProjectIcon::getColorMap() as $color => $name) { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade($color) ->setName($name); $color_control->addCheckbox( 'colors[]', $color, $tag, isset($colors[$color])); } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($name_match)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('members') ->setLabel(pht('Members')) ->setValue($member_phids)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setOptions($this->getStatusOptions()) ->setValue($status)) ->appendChild($icon_control) ->appendChild($color_control); $this->appendCustomFieldsToForm($form, $saved); } protected function getURI($path) { return '/project/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['joined'] = pht('Joined'); } $names['active'] = pht('Active'); $names['all'] = pht('All'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'active': return $query ->setParameter('status', 'active'); case 'joined': return $query ->setParameter('memberPHIDs', array($viewer_phid)) ->setParameter('status', 'active'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( 'active' => pht('Show Only Active Projects'), 'all' => pht('Show All Projects'), ); } private function getStatusValues() { return array( 'active' => PhabricatorProjectQuery::STATUS_ACTIVE, 'all' => PhabricatorProjectQuery::STATUS_ANY, ); } private function getColorValues() {} private function getIconValues() {} protected function getRequiredHandlePHIDsForResultList( array $projects, PhabricatorSavedQuery $query) { return mpull($projects, 'getPHID'); } protected function renderResultList( array $projects, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); $can_edit_projects = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($projects); foreach ($projects as $key => $project) { $id = $project->getID(); $tag_list = id(new PHUIHandleTagListView()) ->setSlim(true) ->setHandles(array($handles[$project->getPHID()])); $item = id(new PHUIObjectItemView()) ->setHeader($project->getName()) ->setHref($this->getApplicationURI("view/{$id}/")) ->setImageURI($project->getProfileImageURI()) ->addAttribute($tag_list); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { $item->addIcon('delete-grey', pht('Archived')); $item->setDisabled(true); } $list->addItem($item); } return $list; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 4b31e4bc80..9b92c66620 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -1,77 +1,78 @@ getViewer(); $raw_query = $this->getRawQuery(); // Allow users to type "#qa" or "qa" to find "Quality Assurance". $raw_query = ltrim($raw_query, '#'); + $tokens = self::tokenizeString($raw_query); - if (!strlen($raw_query)) { - return array(); + $query = id(new PhabricatorProjectQuery()) + ->needImages(true) + ->needSlugs(true); + + if ($tokens) { + $query->withNameTokens($tokens); } - $projs = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->needImages(true) - ->needSlugs(true) - ->withDatasourceQuery($raw_query) - ->execute(); + $projs = $this->executeQuery($query); + $projs = mpull($projs, null, 'getPHID'); $must_have_cols = $this->getParameter('mustHaveColumns', false); if ($must_have_cols) { $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($projs)) ->execute(); $has_cols = mgroup($columns, 'getProjectPHID'); } else { $has_cols = array_fill_keys(array_keys($projs), true); } $results = array(); foreach ($projs as $proj) { if (!isset($has_cols[$proj->getPHID()])) { continue; } $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); } $all_strings = mpull($proj->getSlugs(), 'getSlug'); $all_strings[] = $proj->getName(); $all_strings = implode(' ', $all_strings); $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($all_strings) ->setDisplayName($proj->getName()) ->setDisplayType('Project') ->setURI('/tag/'.$proj->getPrimarySlug().'/') ->setPHID($proj->getPHID()) ->setIcon($proj->getIcon().' bluegrey') ->setPriorityType('proj') ->setClosed($closed); $proj_result->setImageURI($proj->getProfileImageURI()); $results[] = $proj_result; } return $results; } }