From 3203fd9eea7d711418037b1a45b7d49de0ea57d8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Feb 2018 12:23:41 -0800 Subject: [PATCH] Support "Any Value" and "No Value" search constraints for datasource Custom Fields Summary: Depends on D19126. Ref T13090. For datasource custom fields, this proxies the datasource and provides "none()" and "any()" functions to allow you to search for objects with no values or any values. Test Plan: - Created a custom "Owning Group" field in Maniphest using a Projects datasource. - For a task with no owner assigned, searched for "none()" (hit) and "any()" (miss). - Assigned the task to an owning project. - Searched for "none()" (miss), "any()" (hit), the project it is now a member of (hit) and some random other project (miss). Maniphest Tasks: T13090 Differential Revision: https://secure.phabricator.com/D19127 --- src/__phutil_library_map__.php | 8 +++ .../PhabricatorTypeaheadProxyDatasource.php | 58 +++++++++++++++ ...ApplicationSearchAnyFunctionDatasource.php | 70 ++++++++++++++++++ ...CustomFieldApplicationSearchDatasource.php | 17 +++++ ...pplicationSearchNoneFunctionDatasource.php | 72 +++++++++++++++++++ ...habricatorStandardCustomFieldTokenizer.php | 25 ++++++- 6 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php create mode 100644 src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php create mode 100644 src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php create mode 100644 src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f7cda9d8fa..6fd36a1e38 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2617,6 +2617,9 @@ phutil_register_library_map(array( 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', 'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php', + 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php', + 'PhabricatorCustomFieldApplicationSearchDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php', + 'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php', 'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php', 'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php', 'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php', @@ -4387,6 +4390,7 @@ phutil_register_library_map(array( 'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php', 'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php', 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', + 'PhabricatorTypeaheadProxyDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php', 'PhabricatorTypeaheadTestNumbersDatasource' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php', @@ -8122,6 +8126,9 @@ phutil_register_library_map(array( 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorCustomField' => 'Phobject', + 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorCustomFieldApplicationSearchDatasource' => 'PhabricatorTypeaheadProxyDatasource', + 'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorCustomFieldAttachment' => 'Phobject', 'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType', 'PhabricatorCustomFieldDataNotAvailableException' => 'Exception', @@ -10182,6 +10189,7 @@ phutil_register_library_map(array( 'PhabricatorTypeaheadInvalidTokenException' => 'Exception', 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorTypeaheadProxyDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadResult' => 'Phobject', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadTestNumbersDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php new file mode 100644 index 0000000000..3feb2d054d --- /dev/null +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php @@ -0,0 +1,58 @@ +datasource = $datasource; + $this->setParameters( + array( + 'class' => get_class($datasource), + 'parameters' => $datasource->getParameters(), + )); + return $this; + } + + public function getDatasource() { + if (!$this->datasource) { + $class = $this->getParameter('class'); + + $parent = 'PhabricatorTypeaheadDatasource'; + if (!is_subclass_of($class, $parent)) { + throw new Exception( + pht( + 'Configured datasource class "%s" must be a valid subclass of '. + '"%s".', + $class, + $parent)); + } + + $datasource = newv($class, array()); + $datasource->setParameters($this->getParameter('parameters', array())); + $this->datasource = $datasource; + } + + return $this->datasource; + } + + public function getComponentDatasources() { + return array( + $this->getDatasource(), + ); + } + + public function getDatasourceApplicationClass() { + return $this->getDatasource()->getDatasourceApplicationClass(); + } + + public function getBrowseTitle() { + return $this->getDatasource()->getBrowseTitle(); + } + + public function getPlaceholderText() { + return $this->getDatasource()->getPlaceholderText(); + } + +} diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php new file mode 100644 index 0000000000..9560620db4 --- /dev/null +++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php @@ -0,0 +1,70 @@ + array( + 'name' => pht('Any Value'), + 'summary' => pht('Find results with any value.'), + 'description' => pht( + "This function includes results which have any value. Use a query ". + "like this to find results with any value:\n\n%s". + '> any()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->newAnyFunction(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_ANY, + null); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->newAnyFunction()); + } + return $results; + } + + private function newAnyFunction() { + $name = pht('Any Value'); + return $this->newFunctionResult() + ->setName($name.' any') + ->setDisplayName($name) + ->setIcon('fa-circle-o') + ->setPHID('any()') + ->setUnique(true) + ->addAttribute(pht('Select results with any value.')); + } + +} diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php new file mode 100644 index 0000000000..7ba10e84ff --- /dev/null +++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php @@ -0,0 +1,17 @@ + array( + 'name' => pht('No Value'), + 'summary' => pht('Find results with no value.'), + 'description' => pht( + "This function includes results which have no value. Use a query ". + "like this to find results with no value:\n\n%s\n\n", + 'If you combine this function with other constraints, results '. + 'which have no value or the specified values will be returned.', + '> any()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->newNoneFunction(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_NULL, + null); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->newNoneFunction()); + } + return $results; + } + + private function newNoneFunction() { + $name = pht('No Value'); + return $this->newFunctionResult() + ->setName($name.' none') + ->setDisplayName($name) + ->setIcon('fa-ban') + ->setPHID('none()') + ->setUnique(true) + ->addAttribute(pht('Select results with no value.')); + } + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index cdb54fb7e9..9bf59d41f6 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -33,12 +33,28 @@ abstract class PhabricatorStandardCustomFieldTokenizer $control = id(new AphrontFormTokenizerControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) - ->setDatasource($this->getDatasource()) + ->setDatasource($this->newApplicationSearchDatasource()) ->setValue(nonempty($value, array())); $form->appendControl($control); } + public function applyApplicationSearchConstraintToQuery( + PhabricatorApplicationSearchEngine $engine, + PhabricatorCursorPagedPolicyAwareQuery $query, + $value) { + if ($value) { + + $datasource = $this->newApplicationSearchDatasource() + ->setViewer($this->getViewer()); + $value = $datasource->evaluateTokens($value); + + $query->withApplicationSearchContainsConstraint( + $this->newStringIndex(null), + $value); + } + } + public function getHeraldFieldValueType($condition) { return id(new HeraldTokenizerFieldValue()) ->setKey('custom.'.$this->getFieldKey()) @@ -120,4 +136,11 @@ abstract class PhabricatorStandardCustomFieldTokenizer } } + protected function newApplicationSearchDatasource() { + $datasource = $this->getDatasource(); + + return id(new PhabricatorCustomFieldApplicationSearchDatasource()) + ->setDatasource($datasource); + } + }