@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Give projects a proper on-demand datasource

Summary:
Fixes T5614. Ref T4420. Other than the "users" datasource and a couple of others, many datasources ignore what the user typed and just return all results, then rely on the client to filter them.

This works fine for rarely used ("legalpad documents") or always small ("task priorities", "applications") datasets, but is something we should graudally move away from as datasets get larger.

Add a token table to projects, populate it, and use it to drive the datasource query. Additionally, expose it on the applicationsearch UI.

Test Plan:
- Ran migration.
- Manually checked the table.
- Searched for projects by name from ApplicationSearch.
- Searched for projects by name from typeahead.
- Manually checked the typeahead response.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5614, T4420

Differential Revision: https://secure.phabricator.com/D9896

+145 -23
+7
resources/sql/autopatches/20140711.pnames.1.sql
··· 1 + CREATE TABLE {$NAMESPACE}_project.project_datasourcetoken ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + projectID INT UNSIGNED NOT NULL, 4 + token VARCHAR(128) NOT NULL COLLATE utf8_general_ci, 5 + UNIQUE KEY (token, projectID), 6 + KEY (projectID) 7 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+11
resources/sql/autopatches/20140711.pnames.2.php
··· 1 + <?php 2 + 3 + echo "Updating project datasource tokens...\n"; 4 + 5 + foreach (new LiskMigrationIterator(new PhabricatorProject()) as $project) { 6 + $name = $project->getName(); 7 + echo "Updating project '{$name}'...\n"; 8 + $project->updateDatasourceTokens(); 9 + } 10 + 11 + echo "Done.\n";
+3 -17
src/applications/people/storage/PhabricatorUser.php
··· 459 459 return $this; 460 460 } 461 461 462 - private static function tokenizeName($name) { 463 - if (function_exists('mb_strtolower')) { 464 - $name = mb_strtolower($name, 'UTF-8'); 465 - } else { 466 - $name = strtolower($name); 467 - } 468 - $name = trim($name); 469 - if (!strlen($name)) { 470 - return array(); 471 - } 472 - return preg_split('/\s+/', $name); 473 - } 474 - 475 462 /** 476 463 * Populate the nametoken table, which used to fetch typeahead results. When 477 464 * a user types "linc", we want to match "Abraham Lincoln" from on-demand 478 465 * typeahead sources. To do this, we need a separate table of name fragments. 479 466 */ 480 467 public function updateNameTokens() { 481 - $tokens = array_merge( 482 - self::tokenizeName($this->getRealName()), 483 - self::tokenizeName($this->getUserName())); 484 - $tokens = array_unique($tokens); 485 468 $table = self::NAMETOKEN_TABLE; 486 469 $conn_w = $this->establishConnection('w'); 470 + 471 + $tokens = PhabricatorTypeaheadDatasource::tokenizeString( 472 + $this->getUserName().' '.$this->getRealName()); 487 473 488 474 $sql = array(); 489 475 foreach ($tokens as $token) {
+5
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 125 125 ->setProjectPHID($object->getPHID()) 126 126 ->save(); 127 127 128 + $object->updateDatasourceTokens(); 129 + 128 130 // TODO -- delete all of the below once we sever automagical project 129 131 // to phriction stuff 130 132 if ($xaction->getOldValue() === null) { ··· 182 184 $rem_slug->delete(); 183 185 } 184 186 } 187 + 188 + $object->updateDatasourceTokens(); 189 + 185 190 return; 186 191 case PhabricatorTransactions::TYPE_VIEW_POLICY: 187 192 case PhabricatorTransactions::TYPE_EDIT_POLICY:
+29 -4
src/applications/project/query/PhabricatorProjectQuery.php
··· 9 9 private $slugs; 10 10 private $phrictionSlugs; 11 11 private $names; 12 + private $datasourceQuery; 12 13 13 14 private $status = 'status-any'; 14 15 const STATUS_ANY = 'status-any'; ··· 54 55 55 56 public function withNames(array $names) { 56 57 $this->names = $names; 58 + return $this; 59 + } 60 + 61 + public function withDatasourceQuery($string) { 62 + $this->datasourceQuery = $string; 57 63 return $this; 58 64 } 59 65 ··· 286 292 } 287 293 288 294 private function buildGroupClause($conn_r) { 289 - if ($this->memberPHIDs) { 295 + if ($this->memberPHIDs || $this->datasourceQuery) { 290 296 return 'GROUP BY p.id'; 291 297 } else { 292 298 return $this->buildApplicationSearchGroupClause($conn_r); ··· 296 302 private function buildJoinClause($conn_r) { 297 303 $joins = array(); 298 304 299 - if (!$this->needMembers) { 305 + if (!$this->needMembers !== null) { 300 306 $joins[] = qsprintf( 301 307 $conn_r, 302 308 'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s', ··· 305 311 $this->getViewer()->getPHID()); 306 312 } 307 313 308 - if ($this->memberPHIDs) { 314 + if ($this->memberPHIDs !== null) { 309 315 $joins[] = qsprintf( 310 316 $conn_r, 311 317 'JOIN %T e ON e.src = p.phid AND e.type = %d', ··· 313 319 PhabricatorEdgeConfig::TYPE_PROJ_MEMBER); 314 320 } 315 321 316 - if ($this->slugs) { 322 + if ($this->slugs !== null) { 317 323 $joins[] = qsprintf( 318 324 $conn_r, 319 325 'JOIN %T slug on slug.projectPHID = p.phid', 320 326 id(new PhabricatorProjectSlug())->getTableName()); 327 + } 328 + 329 + if ($this->datasourceQuery !== null) { 330 + $tokens = PhabricatorTypeaheadDatasource::tokenizeString( 331 + $this->datasourceQuery); 332 + if (!$tokens) { 333 + throw new PhabricatorEmptyQueryException(); 334 + } 335 + 336 + $likes = array(); 337 + foreach ($tokens as $token) { 338 + $likes[] = qsprintf($conn_r, 'token.token LIKE %>', $token); 339 + } 340 + 341 + $joins[] = qsprintf( 342 + $conn_r, 343 + 'JOIN %T token ON token.projectID = p.id AND (%Q)', 344 + PhabricatorProject::TABLE_DATASOURCE_TOKEN, 345 + '('.implode(') OR (', $likes).')'); 321 346 } 322 347 323 348 $joins[] = $this->buildApplicationSearchJoinClause($conn_r);
+13
src/applications/project/query/PhabricatorProjectSearchEngine.php
··· 21 21 $saved->setParameter( 22 22 'memberPHIDs', 23 23 $this->readUsersFromRequest($request, 'members')); 24 + 24 25 $saved->setParameter('status', $request->getStr('status')); 26 + $saved->setParameter('name', $request->getStr('name')); 25 27 26 28 $this->readCustomFieldsFromRequest($request, $saved); 27 29 ··· 41 43 $status = idx($this->getStatusValues(), $status); 42 44 if ($status) { 43 45 $query->withStatus($status); 46 + } 47 + 48 + $name = $saved->getParameter('name'); 49 + if (strlen($name)) { 50 + $query->withDatasourceQuery($name); 44 51 } 45 52 46 53 $this->applyCustomFieldsToQuery($query, $saved); ··· 59 66 ->execute(); 60 67 61 68 $status = $saved->getParameter('status'); 69 + $name = $saved->getParameter('name'); 62 70 63 71 $form 72 + ->appendChild( 73 + id(new AphrontFormTextControl()) 74 + ->setName('name') 75 + ->setLabel(pht('Name')) 76 + ->setValue($name)) 64 77 ->appendChild( 65 78 id(new AphrontFormTokenizerControl()) 66 79 ->setDatasource(new PhabricatorPeopleDatasource())
+49
src/applications/project/storage/PhabricatorProject.php
··· 32 32 const DEFAULT_ICON = 'fa-briefcase'; 33 33 const DEFAULT_COLOR = 'blue'; 34 34 35 + const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; 36 + 35 37 public static function initializeNewProject(PhabricatorUser $actor) { 36 38 return id(new PhabricatorProject()) 37 39 ->setName('') ··· 219 221 return $this->color; 220 222 } 221 223 224 + public function save() { 225 + $this->openTransaction(); 226 + $result = parent::save(); 227 + $this->updateDatasourceTokens(); 228 + $this->saveTransaction(); 229 + 230 + return $result; 231 + } 232 + 233 + public function updateDatasourceTokens() { 234 + $table = self::TABLE_DATASOURCE_TOKEN; 235 + $conn_w = $this->establishConnection('w'); 236 + $id = $this->getID(); 237 + 238 + $slugs = queryfx_all( 239 + $conn_w, 240 + 'SELECT * FROM %T WHERE projectPHID = %s', 241 + id(new PhabricatorProjectSlug())->getTableName(), 242 + $this->getPHID()); 243 + 244 + $all_strings = ipull($slugs, 'slug'); 245 + $all_strings[] = $this->getName(); 246 + $all_strings = implode(' ', $all_strings); 247 + 248 + $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings); 249 + 250 + $sql = array(); 251 + foreach ($tokens as $token) { 252 + $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token); 253 + } 254 + 255 + $this->openTransaction(); 256 + queryfx( 257 + $conn_w, 258 + 'DELETE FROM %T WHERE projectID = %d', 259 + $table, 260 + $id); 261 + 262 + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { 263 + queryfx( 264 + $conn_w, 265 + 'INSERT INTO %T (projectID, token) VALUES %Q', 266 + $table, 267 + $chunk); 268 + } 269 + $this->saveTransaction(); 270 + } 222 271 223 272 224 273 /* -( PhabricatorSubscribableInterface )----------------------------------- */
+17 -2
src/applications/project/typeahead/PhabricatorProjectDatasource.php
··· 13 13 14 14 public function loadResults() { 15 15 $viewer = $this->getViewer(); 16 + 16 17 $raw_query = $this->getRawQuery(); 17 18 18 - $results = array(); 19 + // Allow users to type "#qa" or "qa" to find "Quality Assurance". 20 + $raw_query = ltrim($raw_query, '#'); 21 + 22 + if (!strlen($raw_query)) { 23 + return array(); 24 + } 19 25 20 26 $projs = id(new PhabricatorProjectQuery()) 21 27 ->setViewer($viewer) 22 28 ->needImages(true) 29 + ->needSlugs(true) 30 + ->withDatasourceQuery($raw_query) 23 31 ->execute(); 32 + 33 + $results = array(); 24 34 foreach ($projs as $proj) { 25 35 $closed = null; 26 36 if ($proj->isArchived()) { 27 37 $closed = pht('Archived'); 28 38 } 29 39 40 + $all_strings = mpull($proj->getSlugs(), 'getSlug'); 41 + $all_strings[] = $proj->getName(); 42 + $all_strings = implode(' ', $all_strings); 43 + 30 44 $proj_result = id(new PhabricatorTypeaheadResult()) 31 - ->setName($proj->getName()) 45 + ->setName($all_strings) 46 + ->setDisplayName($proj->getName()) 32 47 ->setDisplayType('Project') 33 48 ->setURI('/tag/'.$proj->getPrimarySlug().'/') 34 49 ->setPHID($proj->getPHID())
+11
src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
··· 51 51 abstract public function getDatasourceApplicationClass(); 52 52 abstract public function loadResults(); 53 53 54 + public static function tokenizeString($string) { 55 + $string = phutil_utf8_strtolower($string); 56 + $string = trim($string); 57 + if (!strlen($string)) { 58 + return array(); 59 + } 60 + 61 + $tokens = preg_split('/\s+/', $string); 62 + return array_unique($tokens); 63 + } 64 + 54 65 }