diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index f79957dfa2..73a951bd3a 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -1,299 +1,350 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withEmails(array $emails) { $this->emails = $emails; return $this; } public function withRealnames(array $realnames) { $this->realnames = $realnames; return $this; } public function withUsernames(array $usernames) { $this->usernames = $usernames; return $this; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withIsAdmin($admin) { $this->isAdmin = $admin; return $this; } public function withIsSystemAgent($system_agent) { $this->isSystemAgent = $system_agent; return $this; } public function withIsDisabled($disabled) { $this->isDisabled = $disabled; return $this; } public function withIsApproved($approved) { $this->isApproved = $approved; return $this; } public function withNameLike($like) { $this->nameLike = $like; return $this; } + public function withNameTokens(array $tokens) { + $this->nameTokens = array_values($tokens); + return $this; + } + public function needPrimaryEmail($need) { $this->needPrimaryEmail = $need; return $this; } public function needProfile($need) { $this->needProfile = $need; return $this; } public function needProfileImage($need) { $this->needProfileImage = $need; return $this; } public function needStatus($need) { $this->needStatus = $need; return $this; } protected function loadPage() { $table = new PhabricatorUser(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T user %Q %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinsClause($conn_r), $this->buildWhereClause($conn_r), - $this->buildApplicationSearchGroupClause($conn_r), + $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); if ($this->needPrimaryEmail) { $table->putInSet(new LiskDAOSet()); } return $table->loadAllFromArray($data); } protected function didFilterPage(array $users) { if ($this->needProfile) { $user_list = mpull($users, null, 'getPHID'); $profiles = new PhabricatorUserProfile(); $profiles = $profiles->loadAllWhere('userPHID IN (%Ls)', array_keys($user_list)); $profiles = mpull($profiles, null, 'getUserPHID'); foreach ($user_list as $user_phid => $user) { $profile = idx($profiles, $user_phid); if (!$profile) { $profile = new PhabricatorUserProfile(); $profile->setUserPHID($user_phid); } $user->attachUserProfile($profile); } } if ($this->needProfileImage) { $user_profile_file_phids = mpull($users, 'getProfileImagePHID'); $user_profile_file_phids = array_filter($user_profile_file_phids); if ($user_profile_file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($user_profile_file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($users as $user) { $image_phid = $user->getProfileImagePHID(); if (isset($files[$image_phid])) { $profile_image_uri = $files[$image_phid]->getBestURI(); } else { $profile_image_uri = PhabricatorUser::getDefaultProfileImageURI(); } $user->attachProfileImageURI($profile_image_uri); } } if ($this->needStatus) { $user_list = mpull($users, null, 'getPHID'); $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( array_keys($user_list)); foreach ($user_list as $phid => $user) { $status = idx($statuses, $phid); if ($status) { $user->attachStatus($status); } } } return $users; } + private function buildGroupClause(AphrontDatabaseConnection $conn) { + if ($this->nameTokens) { + return qsprintf( + $conn, + 'GROUP BY user.id'); + } else { + return $this->buildApplicationSearchGroupClause($conn); + } + } + private function buildJoinsClause($conn_r) { $joins = array(); if ($this->emails) { $email_table = new PhabricatorUserEmail(); $joins[] = qsprintf( $conn_r, 'JOIN %T email ON email.userPHID = user.PHID', $email_table->getTableName()); } + if ($this->nameTokens) { + foreach ($this->nameTokens as $key => $token) { + $token_table = 'token_'.$key; + $joins[] = qsprintf( + $conn_r, + 'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>', + PhabricatorUser::NAMETOKEN_TABLE, + $token_table, + $token_table, + $token_table, + $token); + } + } + $joins[] = $this->buildApplicationSearchJoinClause($conn_r); $joins = implode(' ', $joins); return $joins; } private function buildWhereClause($conn_r) { $where = array(); if ($this->usernames !== null) { $where[] = qsprintf( $conn_r, 'user.userName IN (%Ls)', $this->usernames); } if ($this->emails !== null) { $where[] = qsprintf( $conn_r, 'email.address IN (%Ls)', $this->emails); } if ($this->realnames !== null) { $where[] = qsprintf( $conn_r, 'user.realName IN (%Ls)', $this->realnames); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'user.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'user.id IN (%Ld)', $this->ids); } if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn_r, 'user.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn_r, 'user.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->isAdmin) { $where[] = qsprintf( $conn_r, 'user.isAdmin = 1'); } if ($this->isDisabled !== null) { $where[] = qsprintf( $conn_r, 'user.isDisabled = %d', (int)$this->isDisabled); } if ($this->isApproved !== null) { $where[] = qsprintf( $conn_r, 'user.isApproved = %d', (int)$this->isApproved); } if ($this->isSystemAgent) { $where[] = qsprintf( $conn_r, 'user.isSystemAgent = 1'); } if (strlen($this->nameLike)) { $where[] = qsprintf( $conn_r, 'user.username LIKE %~ OR user.realname LIKE %~', $this->nameLike, $this->nameLike); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } protected function getPrimaryTableAlias() { return 'user'; } public function getQueryApplicationClass() { return 'PhabricatorPeopleApplication'; } + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'username' => array( + 'table' => 'user', + 'column' => 'username', + 'type' => 'string', + 'reverse' => true, + 'unique' => true, + ), + ); + } + + public function getPagingValueMap($cursor, array $keys) { + $user = $this->loadCursorObject($cursor); + return array( + 'id' => $user->getID(), + 'username' => $user->getUsername(), + ); + } + + } diff --git a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php index c9e8381c35..a2df2750ad 100644 --- a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php +++ b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php @@ -1,125 +1,79 @@ enrichResults = $enrich; return $this; } public function getPlaceholderText() { return pht('Type a username...'); } public function getDatasourceApplicationClass() { return 'PhabricatorPeopleApplication'; } public function loadResults() { $viewer = $this->getViewer(); - $raw_query = $this->getRawQuery(); + $tokens = $this->getTokens(); - $results = array(); - - $users = array(); - if (strlen($raw_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(), - $raw_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, - $raw_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. - if ($viewer->getID()) { - $ids[] = $viewer->getID(); - } + $query = id(new PhabricatorPeopleQuery()) + ->setOrderVector(array('username')); - if ($ids) { - $users = id(new PhabricatorPeopleQuery()) - ->setViewer($viewer) - ->withIDs($ids) - ->execute(); - } + if ($tokens) { + $query->withNameTokens($tokens); } + $users = $this->executeQuery($query); + if ($this->enrichResults && $users) { $phids = mpull($users, 'getPHID'); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); } + $results = array(); foreach ($users as $user) { $closed = null; if ($user->getIsDisabled()) { $closed = pht('Disabled'); } else if ($user->getIsSystemAgent()) { $closed = pht('Bot/Script'); } $result = id(new PhabricatorTypeaheadResult()) ->setName($user->getFullName()) ->setURI('/p/'.$user->getUsername()) ->setPHID($user->getPHID()) ->setPriorityString($user->getUsername()) ->setPriorityType('user') ->setClosed($closed); if ($this->enrichResults) { $display_type = 'User'; if ($user->getIsAdmin()) { $display_type = 'Administrator'; } $result->setDisplayType($display_type); $result->setImageURI($handles[$user->getPHID()]->getImageURI()); } $results[] = $result; } return $results; } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php index 8990460505..b1f55190c9 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php @@ -1,66 +1,68 @@ getOffset(); $limit = $this->getLimit(); $results = array(); foreach ($this->getUsableDatasources() as $source) { $source ->setRawQuery($this->getRawQuery()) ->setQuery($this->getQuery()) ->setViewer($this->getViewer()); if ($limit) { $source->setLimit($offset + $limit); } $results[] = $source->loadResults(); } $results = array_mergev($results); - $results = msort($results, 'getName'); + $results = msort($results, 'getSortKey'); + $count = count($results); if ($offset || $limit) { if (!$limit) { $limit = count($results); } + $results = array_slice($results, $offset, $limit, $preserve_keys = true); } return $results; } private function getUsableDatasources() { $sources = $this->getComponentDatasources(); $usable = array(); foreach ($sources as $source) { $application_class = $source->getDatasourceApplicationClass(); if ($application_class) { $result = id(new PhabricatorApplicationQuery()) ->setViewer($this->getViewer()) ->withClasses(array($application_class)) ->execute(); if (!$result) { continue; } } $usable[] = $source; } return $usable; } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index ac29954fff..3d6d2b75f2 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -1,91 +1,105 @@ limit = $limit; return $this; } public function getLimit() { return $this->limit; } public function setOffset($offset) { $this->offset = $offset; return $this; } public function getOffset() { return $this->offset; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setRawQuery($raw_query) { $this->rawQuery = $raw_query; return $this; } public function getRawQuery() { return $this->rawQuery; } public function setQuery($query) { $this->query = $query; return $this; } public function getQuery() { return $this->query; } public function setParameters(array $params) { $this->parameters = $params; return $this; } public function getParameters() { return $this->parameters; } public function getParameter($name, $default = null) { return idx($this->parameters, $name, $default); } public function getDatasourceURI() { $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/'); $uri->setQueryParams($this->parameters); return (string)$uri; } abstract public function getPlaceholderText(); abstract public function getDatasourceApplicationClass(); abstract public function loadResults(); public static function tokenizeString($string) { $string = phutil_utf8_strtolower($string); $string = trim($string); if (!strlen($string)) { return array(); } $tokens = preg_split('/\s+|[-\[\]]/', $string); return array_unique($tokens); } + public function getTokens() { + return self::tokenizeString($this->getRawQuery()); + } + + protected function executeQuery( + PhabricatorCursorPagedPolicyAwareQuery $query) { + + return $query + ->setViewer($this->getViewer()) + ->setOffset($this->getOffset()) + ->setLimit($this->getLimit()) + ->execute(); + } + } diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php index 134bebbf90..22a1f21b18 100644 --- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php +++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php @@ -1,136 +1,140 @@ icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } public function setURI($uri) { $this->uri = $uri; return $this; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function setPriorityString($priority_string) { $this->priorityString = $priority_string; return $this; } public function setDisplayName($display_name) { $this->displayName = $display_name; return $this; } public function setDisplayType($display_type) { $this->displayType = $display_type; return $this; } public function setImageURI($image_uri) { $this->imageURI = $image_uri; return $this; } public function setPriorityType($priority_type) { $this->priorityType = $priority_type; return $this; } public function setImageSprite($image_sprite) { $this->imageSprite = $image_sprite; return $this; } public function setClosed($closed) { $this->closed = $closed; return $this; } public function getName() { return $this->name; } public function getDisplayName() { return coalesce($this->displayName, $this->getName()); } public function getIcon() { return nonempty($this->icon, $this->getDefaultIcon()); } public function getPHID() { return $this->phid; } + public function getSortKey() { + return phutil_utf8_strtolower($this->getName()); + } + public function getWireFormat() { $data = array( $this->name, $this->uri ? (string)$this->uri : null, $this->phid, $this->priorityString, $this->displayName, $this->displayType, $this->imageURI ? (string)$this->imageURI : null, $this->priorityType, $this->getIcon(), $this->closed, $this->imageSprite ? (string)$this->imageSprite : null, ); while (end($data) === null) { array_pop($data); } return $data; } /** * If the datasource did not specify an icon explicitly, try to select a * default based on PHID type. */ private function getDefaultIcon() { static $icon_map; if ($icon_map === null) { $types = PhabricatorPHIDType::getAllTypes(); $map = array(); foreach ($types as $type) { $icon = $type->getTypeIcon(); if ($icon !== null) { $map[$type->getTypeConstant()] = "{$icon} bluegrey"; } } $icon_map = $map; } $phid_type = phid_get_type($this->phid); if (isset($icon_map[$phid_type])) { return $icon_map[$phid_type]; } return null; } }