diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2295,11 +2295,15 @@ 'PhabricatorProjectIcon' => 'applications/project/icon/PhabricatorProjectIcon.php', 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', + 'PhabricatorProjectLogicDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicDatasource.php', + 'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php', + 'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php', 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', + 'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php', 'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php', 'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', @@ -5674,11 +5678,15 @@ 'PhabricatorProjectFeedController' => 'PhabricatorProjectController', 'PhabricatorProjectIcon' => 'Phobject', 'PhabricatorProjectListController' => 'PhabricatorProjectController', + 'PhabricatorProjectLogicDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', + 'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/project/typeahead/PhabricatorProjectLogicDatasource.php @@ -0,0 +1,22 @@ + $result) { + $results[$key] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_AND, + $result); + } + + return $results; + } + +} diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php @@ -0,0 +1,115 @@ +) or not()...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorProjectApplication'; + } + + public function getComponentDatasources() { + return array( + new PhabricatorProjectDatasource(), + ); + } + + public function getDatasourceFunctions() { + return array( + 'any' => array( + 'name' => pht('Find results in any of several projects.'), + ), + 'not' => array( + 'name' => pht('Find results not in specific projects.'), + ), + ); + } + + protected function didLoadResults(array $results) { + $function = $this->getCurrentFunction(); + $return_any = ($function !== 'not'); + $return_not = ($function !== 'any'); + + $return = array(); + foreach ($results as $result) { + $result + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setIcon('fa-asterisk'); + + if ($return_any) { + $return[] = id(clone $result) + ->setPHID('any('.$result->getPHID().')') + ->setDisplayName(pht('In Any: %s', $result->getDisplayName())) + ->setName($result->getName().' any'); + } + + if ($return_not) { + $return[] = id(clone $result) + ->setPHID('not('.$result->getPHID().')') + ->setDisplayName(pht('Not In: %s', $result->getDisplayName())) + ->setName($result->getName().' not'); + } + } + + return $return; + } + + protected function evaluateFunction($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $operator = array( + 'any' => PhabricatorQueryConstraint::OPERATOR_OR, + 'not' => PhabricatorQueryConstraint::OPERATOR_NOT, + ); + + $results = array(); + foreach ($phids as $phid) { + $results[] = new PhabricatorQueryConstraint( + $operator[$function], + $phid); + } + + return $phids; + } + + public function renderFunctionTokens($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $tokens = $this->renderTokens($phids); + foreach ($tokens as $token) { + if ($token->isInvalid()) { + if ($function == 'any') { + $token->setValue(pht('In Any: Invalid Project')); + } else { + $token->setValue(pht('Not In: Invalid Project')); + } + } else { + $token + ->setIcon('fa-asterisk') + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION); + + if ($function == 'any') { + $token + ->setKey('any('.$token->getKey().')') + ->setValue(pht('In Any: %s', $token->getValue())); + } else { + $token + ->setKey('not('.$token->getKey().')') + ->setValue(pht('Not In: %s', $token->getValue())); + } + } + } + + return $tokens; + } + +} diff --git a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php @@ -0,0 +1,62 @@ + array( + 'name' => pht('Find results which are not in any projects.'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->buildNullResult(), + ); + + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_NULL, + 'empty'); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->buildNullResult()); + } + return $results; + } + + private function buildNullResult() { + $name = pht('Not In Any Projects'); + + return $this->newFunctionResult() + ->setUnique(true) + ->setPHID('null()') + ->setIcon('fa-ban') + ->setName('null '.$name) + ->setDisplayName($name); + } + +} diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php @@ -30,17 +30,39 @@ // query to child sources. This makes it easier to implement function // sources in terms of real object sources. $raw_query = $this->getRawQuery(); + + $is_function = false; if (self::isFunctionToken($raw_query)) { - $function = $this->parseFunction($raw_query, $allow_partial = true); - if ($function) { - $raw_query = head($function['argv']); - } + $is_function = true; } + $stack = $this->getFunctionStack(); + $results = array(); foreach ($this->getUsableDatasources() as $source) { + $source_stack = $stack; + + $source_query = $raw_query; + if ($is_function) { + // If this source can't handle the function, skip it. + $function = $source->parseFunction($raw_query, $allow_partial = true); + if (!$function) { + continue; + } + + // If this source handles the function directly, strip the function. + // Otherwise, this is something like a composite source which has + // some internal source which can evaluate the function, but will + // perform stripping later. + if ($source->shouldStripFunction($function['name'])) { + $source_query = head($function['argv']); + $source_stack[] = $function['name']; + } + } + $source - ->setRawQuery($raw_query) + ->setFunctionStack($source_stack) + ->setRawQuery($source_query) ->setQuery($this->getQuery()) ->setViewer($this->getViewer()); @@ -106,7 +128,6 @@ return parent::canEvaluateFunction($function); } - protected function evaluateFunction($function, array $argv) { foreach ($this->getUsableDatasources() as $source) { if ($source->canEvaluateFunction($function)) { diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -11,6 +11,7 @@ private $offset; private $limit; private $parameters = array(); + private $functionStack = array(); public function setLimit($limit) { $this->limit = $limit; @@ -274,6 +275,14 @@ * @task functions */ protected function canEvaluateFunction($function) { + return $this->shouldStripFunction($function); + } + + + /** + * @task functions + */ + protected function shouldStripFunction($function) { $functions = $this->getDatasourceFunctions(); return isset($functions[$function]); } @@ -337,7 +346,7 @@ $matches = null; if ($allow_partial) { - $ok = preg_match('/^([^(]+)\((.*)$/', $token, $matches); + $ok = preg_match('/^([^(]+)\((.*?)\)?$/', $token, $matches); } else { $ok = preg_match('/^([^(]+)\((.*)\)$/', $token, $matches); } @@ -367,4 +376,28 @@ } + /** + * @task functions + */ + public function setFunctionStack(array $function_stack) { + $this->functionStack = $function_stack; + return $this; + } + + + /** + * @task functions + */ + public function getFunctionStack() { + return $this->functionStack; + } + + + /** + * @task functions + */ + protected function getCurrentFunction() { + return nonempty(last($this->functionStack), null); + } + }