@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.

Support custom fields in "Order By" for Maniphest

Summary:
Resolves T4659. This implements support for sorting tasks by custom fields.

Some of this feels hacky in the way it's hooked up to the Maniphest search engine and task query.

Test Plan: Queryed on a custom date field, with a small page size, and moved back and forth through the result set.

Reviewers: #blessed_reviewers, epriestley

Reviewed By: #blessed_reviewers, epriestley

Subscribers: epriestley, Korvin

Maniphest Tasks: T4659

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

+290 -74
+92 -66
src/applications/maniphest/query/ManiphestTaskQuery.php
··· 572 572 } 573 573 574 574 private function buildCustomOrderClause(AphrontDatabaseConnection $conn) { 575 + $reverse = ($this->getBeforeID() xor $this->getReversePaging()); 576 + 575 577 $order = array(); 576 578 577 579 switch ($this->groupBy) { ··· 593 595 throw new Exception("Unknown group query '{$this->groupBy}'!"); 594 596 } 595 597 596 - switch ($this->orderBy) { 597 - case self::ORDER_PRIORITY: 598 - $order[] = 'priority'; 599 - $order[] = 'subpriority'; 600 - $order[] = 'dateModified'; 601 - break; 602 - case self::ORDER_CREATED: 603 - $order[] = 'id'; 604 - break; 605 - case self::ORDER_MODIFIED: 606 - $order[] = 'dateModified'; 607 - break; 608 - case self::ORDER_TITLE: 609 - $order[] = 'title'; 610 - break; 611 - default: 612 - throw new Exception("Unknown order query '{$this->orderBy}'!"); 598 + $app_order = $this->buildApplicationSearchOrders($conn, $reverse); 599 + 600 + if (!$app_order) { 601 + switch ($this->orderBy) { 602 + case self::ORDER_PRIORITY: 603 + $order[] = 'priority'; 604 + $order[] = 'subpriority'; 605 + $order[] = 'dateModified'; 606 + break; 607 + case self::ORDER_CREATED: 608 + $order[] = 'id'; 609 + break; 610 + case self::ORDER_MODIFIED: 611 + $order[] = 'dateModified'; 612 + break; 613 + case self::ORDER_TITLE: 614 + $order[] = 'title'; 615 + break; 616 + default: 617 + throw new Exception("Unknown order query '{$this->orderBy}'!"); 618 + } 613 619 } 614 620 615 621 $order = array_unique($order); 616 622 617 - if (empty($order)) { 623 + if (empty($order) && empty($app_order)) { 618 624 return null; 619 625 } 620 - 621 - $reverse = ($this->getBeforeID() xor $this->getReversePaging()); 622 626 623 627 foreach ($order as $k => $column) { 624 628 switch ($column) { ··· 650 654 $order[$k] = "task.{$column} DESC"; 651 655 } 652 656 break; 657 + } 658 + } 659 + 660 + if ($app_order) { 661 + foreach ($app_order as $order_by) { 662 + $order[] = $order_by; 663 + } 664 + 665 + if ($reverse) { 666 + $order[] = 'task.id ASC'; 667 + } else { 668 + $order[] = 'task.id DESC'; 653 669 } 654 670 } 655 671 ··· 903 919 throw new Exception("Unknown group query '{$this->groupBy}'!"); 904 920 } 905 921 906 - switch ($this->orderBy) { 907 - case self::ORDER_PRIORITY: 908 - if ($this->groupBy != self::GROUP_PRIORITY) { 922 + $app_columns = $this->buildApplicationSearchPagination($conn_r, $cursor); 923 + if ($app_columns) { 924 + $columns = array_merge($columns, $app_columns); 925 + $columns[] = array( 926 + 'name' => 'task.id', 927 + 'value' => (int)$cursor->getID(), 928 + 'type' => 'int', 929 + ); 930 + } else { 931 + switch ($this->orderBy) { 932 + case self::ORDER_PRIORITY: 933 + if ($this->groupBy != self::GROUP_PRIORITY) { 934 + $columns[] = array( 935 + 'name' => 'task.priority', 936 + 'value' => (int)$cursor->getPriority(), 937 + 'type' => 'int', 938 + ); 939 + } 909 940 $columns[] = array( 910 - 'name' => 'task.priority', 911 - 'value' => (int)$cursor->getPriority(), 941 + 'name' => 'task.subpriority', 942 + 'value' => (int)$cursor->getSubpriority(), 912 943 'type' => 'int', 944 + 'reverse' => true, 913 945 ); 914 - } 915 - $columns[] = array( 916 - 'name' => 'task.subpriority', 917 - 'value' => (int)$cursor->getSubpriority(), 918 - 'type' => 'int', 919 - 'reverse' => true, 920 - ); 921 - $columns[] = array( 922 - 'name' => 'task.dateModified', 923 - 'value' => (int)$cursor->getDateModified(), 924 - 'type' => 'int', 925 - ); 926 - break; 927 - case self::ORDER_CREATED: 928 - $columns[] = array( 929 - 'name' => 'task.id', 930 - 'value' => (int)$cursor->getID(), 931 - 'type' => 'int', 932 - ); 933 - break; 934 - case self::ORDER_MODIFIED: 935 - $columns[] = array( 936 - 'name' => 'task.dateModified', 937 - 'value' => (int)$cursor->getDateModified(), 938 - 'type' => 'int', 939 - ); 940 - break; 941 - case self::ORDER_TITLE: 942 - $columns[] = array( 943 - 'name' => 'task.title', 944 - 'value' => $cursor->getTitle(), 945 - 'type' => 'string', 946 - ); 947 - $columns[] = array( 948 - 'name' => 'task.id', 949 - 'value' => $cursor->getID(), 950 - 'type' => 'int', 951 - ); 952 - break; 953 - default: 954 - throw new Exception("Unknown order query '{$this->orderBy}'!"); 946 + $columns[] = array( 947 + 'name' => 'task.dateModified', 948 + 'value' => (int)$cursor->getDateModified(), 949 + 'type' => 'int', 950 + ); 951 + break; 952 + case self::ORDER_CREATED: 953 + $columns[] = array( 954 + 'name' => 'task.id', 955 + 'value' => (int)$cursor->getID(), 956 + 'type' => 'int', 957 + ); 958 + break; 959 + case self::ORDER_MODIFIED: 960 + $columns[] = array( 961 + 'name' => 'task.dateModified', 962 + 'value' => (int)$cursor->getDateModified(), 963 + 'type' => 'int', 964 + ); 965 + break; 966 + case self::ORDER_TITLE: 967 + $columns[] = array( 968 + 'name' => 'task.title', 969 + 'value' => $cursor->getTitle(), 970 + 'type' => 'string', 971 + ); 972 + $columns[] = array( 973 + 'name' => 'task.id', 974 + 'value' => $cursor->getID(), 975 + 'type' => 'int', 976 + ); 977 + break; 978 + default: 979 + throw new Exception("Unknown order query '{$this->orderBy}'!"); 980 + } 955 981 } 956 982 957 983 return $this->buildPagingClauseFromMultipleColumns(
+9 -8
src/applications/maniphest/query/ManiphestTaskSearchEngine.php
··· 151 151 $query->withPriorities($priorities); 152 152 } 153 153 154 - $order = $saved->getParameter('order'); 155 - $order = idx($this->getOrderValues(), $order); 156 - if ($order) { 157 - $query->setOrderBy($order); 158 - } else { 159 - $query->setOrderBy(head($this->getOrderValues())); 160 - } 154 + $this->applyOrderByToQuery( 155 + $query, 156 + $this->getOrderValues(), 157 + $saved->getParameter('order')); 161 158 162 159 $group = $saved->getParameter('group'); 163 160 $group = idx($this->getGroupValues(), $group); ··· 306 303 307 304 $ids = $saved->getParameter('ids', array()); 308 305 306 + $builtin_orders = $this->getOrderOptions(); 307 + $custom_orders = $this->getCustomFieldOrderOptions(); 308 + $all_orders = $builtin_orders + $custom_orders; 309 + 309 310 $form 310 311 ->appendChild( 311 312 id(new AphrontFormTokenizerControl()) ··· 385 386 ->setName('order') 386 387 ->setLabel(pht('Order By')) 387 388 ->setValue($saved->getParameter('order')) 388 - ->setOptions($this->getOrderOptions())); 389 + ->setOptions($all_orders)); 389 390 } 390 391 391 392 $form
+59
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 764 764 } 765 765 } 766 766 767 + protected function applyOrderByToQuery( 768 + PhabricatorCursorPagedPolicyAwareQuery $query, 769 + array $standard_values, 770 + $order) { 771 + 772 + if (substr($order, 0, 7) === 'custom:') { 773 + $list = $this->getCustomFieldList(); 774 + if (!$list) { 775 + $query->setOrderBy(head($standard_values)); 776 + return; 777 + } 778 + 779 + foreach ($list->getFields() as $field) { 780 + $key = $this->getKeyForCustomField($field); 781 + 782 + if ($key === $order) { 783 + $index = $field->buildOrderIndex(); 784 + 785 + if ($index === null) { 786 + $query->setOrderBy(head($standard_values)); 787 + return; 788 + } 789 + 790 + $query->withApplicationSearchOrder( 791 + $field, 792 + $index, 793 + false); 794 + break; 795 + } 796 + } 797 + } else { 798 + $order = idx($standard_values, $order); 799 + if ($order) { 800 + $query->setOrderBy($order); 801 + } else { 802 + $query->setOrderBy(head($standard_values)); 803 + } 804 + } 805 + } 806 + 807 + 808 + protected function getCustomFieldOrderOptions() { 809 + $list = $this->getCustomFieldList(); 810 + if (!$list) { 811 + return; 812 + } 813 + 814 + $custom_order = array(); 815 + foreach ($list->getFields() as $field) { 816 + if ($field->shouldAppearInApplicationSearch()) { 817 + if ($field->buildOrderIndex() !== null) { 818 + $key = $this->getKeyForCustomField($field); 819 + $custom_order[$key] = $field->getFieldName(); 820 + } 821 + } 822 + } 823 + 824 + return $custom_order; 825 + } 767 826 768 827 /** 769 828 * Get a unique key identifying a field.
+22
src/infrastructure/customfield/field/PhabricatorCustomField.php
··· 621 621 622 622 623 623 /** 624 + * Return an index against which this field can be meaningfully ordered 625 + * against to implement ApplicationSearch. 626 + * 627 + * This should be a single index, normally built using 628 + * @{method:newStringIndex} and @{method:newNumericIndex}. 629 + * 630 + * The value of the index is not used. 631 + * 632 + * Return null from this method if the field can not be ordered. 633 + * 634 + * @return PhabricatorCustomFieldIndexStorage A single index to order by. 635 + * @task appsearch 636 + */ 637 + public function buildOrderIndex() { 638 + if ($this->proxy) { 639 + return $this->proxy->buildOrderIndex(); 640 + } 641 + return null; 642 + } 643 + 644 + 645 + /** 624 646 * Build a new empty storage object for storing string indexes. Normally, 625 647 * this should be a concrete subclass of 626 648 * @{class:PhabricatorCustomFieldStringIndexStorage}.
+4
src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
··· 260 260 return array(); 261 261 } 262 262 263 + public function buildOrderIndex() { 264 + return null; 265 + } 266 + 263 267 public function readApplicationSearchValueFromRequest( 264 268 PhabricatorApplicationSearchEngine $engine, 265 269 AphrontRequest $request) {
+4
src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php
··· 18 18 return $indexes; 19 19 } 20 20 21 + public function buildOrderIndex() { 22 + return $this->newNumericIndex(0); 23 + } 24 + 21 25 public function getValueForStorage() { 22 26 $value = $this->getFieldValue(); 23 27 if (strlen($value)) {
+4
src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php
··· 18 18 return $indexes; 19 19 } 20 20 21 + public function buildOrderIndex() { 22 + return $this->newNumericIndex(0); 23 + } 24 + 21 25 public function getValueForStorage() { 22 26 $value = $this->getFieldValue(); 23 27 if (strlen($value)) {
+4
src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php
··· 18 18 return $indexes; 19 19 } 20 20 21 + public function buildOrderIndex() { 22 + return $this->newNumericIndex(0); 23 + } 24 + 21 25 public function getValueForStorage() { 22 26 $value = $this->getFieldValue(); 23 27 if (strlen($value)) {
+92
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 12 12 private $afterID; 13 13 private $beforeID; 14 14 private $applicationSearchConstraints = array(); 15 + private $applicationSearchOrders = array(); 15 16 private $internalPaging; 16 17 17 18 protected function getPagingColumn() { ··· 360 361 361 362 362 363 /** 364 + * Order the results by an ApplicationSearch index. 365 + * 366 + * @param PhabricatorCustomField Field to which the index belongs. 367 + * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. 368 + * @param bool True to sort ascending. 369 + * @return this 370 + * @task appsearch 371 + */ 372 + public function withApplicationSearchOrder( 373 + PhabricatorCustomField $field, 374 + PhabricatorCustomFieldIndexStorage $index, 375 + $ascending) { 376 + 377 + $this->applicationSearchOrders[] = array( 378 + 'key' => $field->getFieldKey(), 379 + 'type' => $index->getIndexValueType(), 380 + 'table' => $index->getTableName(), 381 + 'index' => $index->getIndexKey(), 382 + 'ascending' => $ascending, 383 + ); 384 + 385 + return $this; 386 + } 387 + 388 + 389 + /** 363 390 * Get the name of the query's primary object PHID column, for constructing 364 391 * JOIN clauses. Normally (and by default) this is just `"phid"`, but if the 365 392 * query construction requires a table alias it may be something like ··· 535 562 } 536 563 } 537 564 565 + foreach ($this->applicationSearchOrders as $key => $order) { 566 + $table = $order['table']; 567 + $alias = 'appsearch_order_'.$key; 568 + $index = $order['index']; 569 + $phid_column = $this->getApplicationSearchObjectPHIDColumn(); 570 + 571 + $joins[] = qsprintf( 572 + $conn_r, 573 + 'JOIN %T %T ON %T.objectPHID = %Q 574 + AND %T.indexKey = %s', 575 + $table, 576 + $alias, 577 + $alias, 578 + $phid_column, 579 + $alias, 580 + $index); 581 + } 582 + 538 583 return implode(' ', $joins); 584 + } 585 + 586 + protected function buildApplicationSearchOrders( 587 + AphrontDatabaseConnection $conn_r, 588 + $reverse) { 589 + 590 + $orders = array(); 591 + foreach ($this->applicationSearchOrders as $key => $order) { 592 + $alias = 'appsearch_order_'.$key; 593 + 594 + if ($order['ascending'] xor $reverse) { 595 + $orders[] = qsprintf($conn_r, '%T.indexValue ASC', $alias); 596 + } else { 597 + $orders[] = qsprintf($conn_r, '%T.indexValue DESC', $alias); 598 + } 599 + } 600 + 601 + return $orders; 602 + } 603 + 604 + protected function buildApplicationSearchPagination( 605 + AphrontDatabaseConnection $conn_r, 606 + $cursor) { 607 + 608 + // We have to get the current field values on the cursor object. 609 + $fields = PhabricatorCustomField::getObjectFields( 610 + $cursor, 611 + PhabricatorCustomField::ROLE_APPLICATIONSEARCH); 612 + $fields->setViewer($this->getViewer()); 613 + $fields->readFieldsFromStorage($cursor); 614 + 615 + $fields = mpull($fields->getFields(), null, 'getFieldKey'); 616 + 617 + $columns = array(); 618 + foreach ($this->applicationSearchOrders as $key => $order) { 619 + $alias = 'appsearch_order_'.$key; 620 + 621 + $field = idx($fields, $order['key']); 622 + 623 + $columns[] = array( 624 + 'name' => $alias.'.indexValue', 625 + 'value' => $field->getValueForStorage(), 626 + 'type' => $order['type'], 627 + ); 628 + } 629 + 630 + return $columns; 539 631 } 540 632 541 633 }