APIv4 - Support pseudoconstant suffixes in orderBy clause
authorColeman Watts <coleman@civicrm.org>
Wed, 3 Jun 2020 23:09:34 +0000 (19:09 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 3 Jun 2020 23:09:34 +0000 (19:09 -0400)
Civi/Api4/Query/Api4SelectQuery.php
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js
tests/phpunit/api/v4/Action/BasicActionsTest.php
tests/phpunit/api/v4/Action/PseudoconstantTest.php

index 5378454474eecb40b022e83fd8e0fde41df2a290..794df9580a1150b5a0b0da76d5182ea157a97175 100644 (file)
@@ -222,7 +222,20 @@ class Api4SelectQuery extends SelectQuery {
       if ($dir !== 'ASC' && $dir !== 'DESC') {
         throw new \API_Exception("Invalid sort direction. Cannot order by $item $dir");
       }
-      $this->query->orderBy($this->renderExpression($item) . " $dir");
+      $expr = $this->getExpression($item);
+      $column = $expr->render($this->apiFieldSpec);
+
+      // Use FIELD() function to sort on pseudoconstant values
+      $suffix = strstr($item, ':');
+      if ($suffix && $expr->getType() === 'SqlField') {
+        $field = $this->getField($item);
+        $options = FormattingUtil::getPseudoconstantList($field['entity'], $field['name'], substr($suffix, 1));
+        if ($options) {
+          asort($options);
+          $column = "FIELD($column,'" . implode("','", array_keys($options)) . "')";
+        }
+      }
+      $this->query->orderBy("$column $dir");
     }
   }
 
@@ -241,7 +254,7 @@ class Api4SelectQuery extends SelectQuery {
    */
   protected function buildGroupBy() {
     foreach ($this->groupBy as $item) {
-      $this->query->groupBy($this->renderExpression($item));
+      $this->query->groupBy($this->getExpression($item)->render($this->apiFieldSpec));
     }
   }
 
@@ -374,16 +387,6 @@ class Api4SelectQuery extends SelectQuery {
     return $sqlExpr;
   }
 
-  /**
-   * @param string $expr
-   * @return string
-   * @throws \API_Exception
-   */
-  protected function renderExpression(string $expr) {
-    $sqlExpr = $this->getExpression($expr);
-    return $sqlExpr->render($this->apiFieldSpec);
-  }
-
   /**
    * @inheritDoc
    */
index c688ea3b9acec9f15f71793023ff7894f5b792ad..d1379c892949d92713735a79550bbcd8c981d24c 100644 (file)
               </div>
             </div>
             <div class="api4-input form-inline">
-              <input class="collapsible-optgroups form-control huge" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoinsAndFunctions}" placeholder="Add orderBy" />
+              <input class="collapsible-optgroups form-control huge" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoinsAndFunctionsWithSuffixes}" placeholder="Add orderBy" />
             </div>
           </fieldset>
           <fieldset ng-if="::availableParams.limit && availableParams.offset">
index 63cb5dedd5bf9ef682c307189e69c81dedddfbc0..17de0a2958e63a98b4b0e6521012e2017acc6453 100644 (file)
@@ -28,6 +28,7 @@
     $scope.havingOptions = [];
     $scope.fieldsAndJoins = [];
     $scope.fieldsAndJoinsAndFunctions = [];
+    $scope.fieldsAndJoinsAndFunctionsWithSuffixes = [];
     $scope.fieldsAndJoinsAndFunctionsAndWildcards = [];
     $scope.availableParams = {};
     $scope.params = {};
       $scope.action = $routeParams.api4action;
       $scope.fieldsAndJoins.length = 0;
       $scope.fieldsAndJoinsAndFunctions.length = 0;
+      $scope.fieldsAndJoinsAndFunctionsWithSuffixes.length = 0;
       $scope.fieldsAndJoinsAndFunctionsAndWildcards.length = 0;
       if (!actions.length) {
         formatForSelect2(getEntity().actions, actions, 'name', ['description', 'params']);
             });
           }
           $scope.fieldsAndJoinsAndFunctions = addJoins($scope.fields.concat(functions), true);
+          $scope.fieldsAndJoinsAndFunctionsWithSuffixes = addJoins(getFieldList($scope.action, ['name', 'label']).concat(functions), false, ['name', 'label']);
           $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(getFieldList($scope.action, ['name', 'label']).concat(functions), true, ['name', 'label']);
         } else {
           $scope.fieldsAndJoins = getFieldList($scope.action, ['name']);
           $scope.fieldsAndJoinsAndFunctions = $scope.fields;
+          $scope.fieldsAndJoinsAndFunctionsWithSuffixes = getFieldList($scope.action, ['name', 'label']);
           $scope.fieldsAndJoinsAndFunctionsAndWildcards = getFieldList($scope.action, ['name', 'label']);
         }
         $scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
index de06f6882e9c8b46c596b5ac4de52d88eff96e88..daf2b3470eab21374bdc9d995b1a503772a0aeb4 100644 (file)
@@ -266,6 +266,7 @@ class BasicActionsTest extends UnitTestCase {
 
     $results = MockBasicEntity::get()
       ->addSelect('*', 'group:label', 'group:name', 'fruit:name', 'fruit:color', 'fruit:label')
+      ->addOrderBy('fruit:color', "DESC")
       ->execute();
 
     $this->assertEquals('round', $results[0]['shape']);
@@ -277,6 +278,12 @@ class BasicActionsTest extends UnitTestCase {
     $this->assertEquals('banana', $results[0]['fruit:name']);
     $this->assertEquals('yellow', $results[0]['fruit:color']);
 
+    // Reverse order
+    $results = MockBasicEntity::get()
+      ->addOrderBy('fruit:color')
+      ->execute();
+    $this->assertEquals('two', $results[0]['group']);
+
     // Cannot match to a non-unique option property like :color on create
     try {
       MockBasicEntity::create()->addValue('fruit:color', 'yellow')->execute();
index 277c47be6829c0c51cf0951b1a687acf0480c2f7..cd01509d63ecc43516347df958f3a6d84e984cdb 100644 (file)
@@ -196,6 +196,33 @@ class PseudoconstantTest extends BaseCustomValueTest {
     $this->assertEquals('blü', $result['myPseudoconstantTest.Color:label']);
     $this->assertEquals('bl_', $result['myPseudoconstantTest.Color:name']);
     $this->assertEquals('b', $result['myPseudoconstantTest.Color']);
+
+    $cid1 = Contact::create()
+      ->setCheckPermissions(FALSE)
+      ->addValue('first_name', 'two')
+      ->addValue('myPseudoconstantTest.Technicolor:label', 'RED')
+      ->execute()->first()['id'];
+    $cid2 = Contact::create()
+      ->setCheckPermissions(FALSE)
+      ->addValue('first_name', 'two')
+      ->addValue('myPseudoconstantTest.Technicolor:label', 'GREEN')
+      ->execute()->first()['id'];
+
+    // Test ordering by label
+    $result = Contact::get()
+      ->setCheckPermissions(FALSE)
+      ->addWhere('id', 'IN', [$cid1, $cid2])
+      ->addSelect('id')
+      ->addOrderBy('myPseudoconstantTest.Technicolor:label')
+      ->execute()->first()['id'];
+    $this->assertEquals($cid2, $result);
+    $result = Contact::get()
+      ->setCheckPermissions(FALSE)
+      ->addWhere('id', 'IN', [$cid1, $cid2])
+      ->addSelect('id')
+      ->addOrderBy('myPseudoconstantTest.Technicolor:label', 'DESC')
+      ->execute()->first()['id'];
+    $this->assertEquals($cid1, $result);
   }
 
   public function testJoinOptions() {