diff --git a/resources/sql/autopatches/20170814.search.01.qconfig.sql b/resources/sql/autopatches/20170814.search.01.qconfig.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20170814.search.01.qconfig.sql @@ -0,0 +1,9 @@ +CREATE TABLE {$NAMESPACE}_search.search_namedqueryconfig ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + engineClassName VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + scopePHID VARBINARY(64) NOT NULL, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_scope` (engineClassName, scopePHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 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 @@ -3189,6 +3189,8 @@ 'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php', 'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php', 'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php', + 'PhabricatorNamedQueryConfig' => 'applications/search/storage/PhabricatorNamedQueryConfig.php', + 'PhabricatorNamedQueryConfigQuery' => 'applications/search/query/PhabricatorNamedQueryConfigQuery.php', 'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php', 'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php', 'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php', @@ -3908,6 +3910,7 @@ 'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php', 'PhabricatorSearchDateControlField' => 'applications/search/field/PhabricatorSearchDateControlField.php', 'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php', + 'PhabricatorSearchDefaultController' => 'applications/search/controller/PhabricatorSearchDefaultController.php', 'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php', 'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php', 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php', @@ -8552,6 +8555,11 @@ 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorNamedQueryConfig' => array( + 'PhabricatorSearchDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorNamedQueryConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', @@ -9448,6 +9456,7 @@ 'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchDateControlField' => 'PhabricatorSearchField', 'PhabricatorSearchDateField' => 'PhabricatorSearchField', + 'PhabricatorSearchDefaultController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -34,6 +34,8 @@ 'hovercard/' => 'PhabricatorSearchHovercardController', 'edit/(?P[^/]+)/' => 'PhabricatorSearchEditController', + 'default/(?P[^/]+)/(?P[^/]+)/' + => 'PhabricatorSearchDefaultController', 'delete/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorSearchDeleteController', 'order/(?P[^/]+)/' => 'PhabricatorSearchOrderController', diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -127,7 +127,7 @@ if (!$found_query_data) { // Otherwise, there's no query data so just run the user's default // query for this application. - $query_key = head_key($engine->loadEnabledNamedQueries()); + $query_key = $engine->getDefaultQueryKey(); } } @@ -400,6 +400,8 @@ 'orderURI' => '/search/order/'.get_class($engine).'/', )); + $default_key = $engine->getDefaultQueryKey(); + foreach ($named_queries as $named_query) { $class = get_class($engine); $key = $named_query->getQueryKey(); @@ -410,28 +412,64 @@ if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { $icon = 'fa-plus'; + $disable_name = pht('Enable'); } else { $icon = 'fa-times'; + if ($named_query->getIsBuiltin()) { + $disable_name = pht('Disable'); + } else { + $disable_name = pht('Delete'); + } } $item->addAction( id(new PHUIListItemView()) ->setIcon($icon) ->setHref('/search/delete/'.$key.'/'.$class.'/') + ->setRenderNameAsTooltip(true) + ->setName($disable_name) ->setWorkflow(true)); + $default_disabled = $named_query->getIsDisabled(); + $default_icon = 'fa-thumb-tack'; + + if ($default_key === $key) { + $default_color = 'green'; + } else { + $default_color = null; + } + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon("{$default_icon} {$default_color}") + ->setHref('/search/default/'.$key.'/'.$class.'/') + ->setRenderNameAsTooltip(true) + ->setName(pht('Make Default')) + ->setWorkflow(true) + ->setDisabled($default_disabled)); + if ($named_query->getIsBuiltin()) { - if ($named_query->getIsDisabled()) { - $item->addIcon('fa-times lightgreytext', pht('Disabled')); - $item->setDisabled(true); - } else { - $item->addIcon('fa-lock lightgreytext', pht('Builtin')); - } + $edit_icon = 'fa-lock lightgreytext'; + $edit_disabled = true; + $edit_name = pht('Builtin'); + $edit_href = null; } else { - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-pencil') - ->setHref('/search/edit/'.$key.'/')); + $edit_icon = 'fa-pencil'; + $edit_disabled = false; + $edit_name = pht('Edit'); + $edit_href = '/search/edit/'.$key.'/'; + } + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon($edit_icon) + ->setHref($edit_href) + ->setRenderNameAsTooltip(true) + ->setName($edit_name) + ->setDisabled($edit_disabled)); + + if ($named_query->getIsDisabled()) { + $item->setDisabled(true); } $item->setGrippable(true); @@ -610,7 +648,7 @@ $engine_class = get_class($engine); $query_key = $this->getQueryKey(); if (!$query_key) { - $query_key = head_key($engine->loadEnabledNamedQueries()); + $query_key = $engine->getDefaultQueryKey(); } $can_use = $engine->canUseInPanelContext(); diff --git a/src/applications/search/controller/PhabricatorSearchDefaultController.php b/src/applications/search/controller/PhabricatorSearchDefaultController.php new file mode 100644 --- /dev/null +++ b/src/applications/search/controller/PhabricatorSearchDefaultController.php @@ -0,0 +1,81 @@ +getViewer(); + $engine_class = $request->getURIData('engine'); + + $base_class = 'PhabricatorApplicationSearchEngine'; + if (!is_subclass_of($engine_class, $base_class)) { + return new Aphront400Response(); + } + + $engine = newv($engine_class, array()); + $engine->setViewer($viewer); + + $key = $request->getURIData('queryKey'); + + $named_query = id(new PhabricatorNamedQueryQuery()) + ->setViewer($viewer) + ->withEngineClassNames(array($engine_class)) + ->withQueryKeys(array($key)) + ->withUserPHIDs(array($viewer->getPHID())) + ->executeOne(); + + if (!$named_query && $engine->isBuiltinQuery($key)) { + $named_query = $engine->getBuiltinQuery($key); + } + + if (!$named_query) { + return new Aphront404Response(); + } + + $return_uri = $engine->getQueryManagementURI(); + + $builtin = null; + if ($engine->isBuiltinQuery($key)) { + $builtin = $engine->getBuiltinQuery($key); + } + + if ($request->isFormPost()) { + $config = id(new PhabricatorNamedQueryConfigQuery()) + ->setViewer($viewer) + ->withEngineClassNames(array($engine_class)) + ->withScopePHIDs(array($viewer->getPHID())) + ->executeOne(); + if (!$config) { + $config = PhabricatorNamedQueryConfig::initializeNewQueryConfig() + ->setEngineClassName($engine_class) + ->setScopePHID($viewer->getPHID()); + } + + $config->setConfigProperty( + PhabricatorNamedQueryConfig::PROPERTY_PINNED, + $key); + + $config->save(); + + return id(new AphrontRedirectResponse())->setURI($return_uri); + } + + if ($named_query->getIsBuiltin()) { + $query_name = $builtin->getQueryName(); + } else { + $query_name = $named_query->getQueryName(); + } + + $title = pht('Set Default Query'); + $body = pht( + 'This query will become your default query in the current application.'); + $button = pht('Set Default Query'); + + return $this->newDialog() + ->setTitle($title) + ->appendChild($body) + ->addCancelButton($return_uri) + ->addSubmitButton($button); + } + +} diff --git a/src/applications/search/controller/PhabricatorSearchEditController.php b/src/applications/search/controller/PhabricatorSearchEditController.php --- a/src/applications/search/controller/PhabricatorSearchEditController.php +++ b/src/applications/search/controller/PhabricatorSearchEditController.php @@ -10,7 +10,6 @@ ->setViewer($viewer) ->withQueryKeys(array($request->getURIData('queryKey'))) ->executeOne(); - if (!$saved_query) { return new Aphront404Response(); } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -511,6 +511,34 @@ return $named_queries; } + public function getDefaultQueryKey() { + $viewer = $this->requireViewer(); + + $configs = id(new PhabricatorNamedQueryConfigQuery()) + ->setViewer($viewer) + ->withEngineClassNames(array(get_class($this))) + ->withScopePHIDs( + array( + $viewer->getPHID(), + PhabricatorNamedQueryConfig::SCOPE_GLOBAL, + )) + ->execute(); + $configs = msortv($configs, 'getStrengthSortVector'); + + $key_pinned = PhabricatorNamedQueryConfig::PROPERTY_PINNED; + $map = $this->loadEnabledNamedQueries(); + foreach ($configs as $config) { + $pinned = $config->getConfigProperty($key_pinned); + if (!isset($map[$pinned])) { + continue; + } + + return $pinned; + } + + return head_key($map); + } + protected function setQueryProjects( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { diff --git a/src/applications/search/query/PhabricatorNamedQueryConfigQuery.php b/src/applications/search/query/PhabricatorNamedQueryConfigQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/search/query/PhabricatorNamedQueryConfigQuery.php @@ -0,0 +1,64 @@ +ids = $ids; + return $this; + } + + public function withScopePHIDs(array $scope_phids) { + $this->scopePHIDs = $scope_phids; + return $this; + } + + public function withEngineClassNames(array $engine_class_names) { + $this->engineClassNames = $engine_class_names; + return $this; + } + + public function newResultObject() { + return new PhabricatorNamedQueryConfig(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->engineClassNames !== null) { + $where[] = qsprintf( + $conn, + 'engineClassName IN (%Ls)', + $this->engineClassNames); + } + + if ($this->scopePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'scopePHID IN (%Ls)', + $this->scopePHIDs); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorSearchApplication'; + } + +} diff --git a/src/applications/search/storage/PhabricatorNamedQueryConfig.php b/src/applications/search/storage/PhabricatorNamedQueryConfig.php new file mode 100644 --- /dev/null +++ b/src/applications/search/storage/PhabricatorNamedQueryConfig.php @@ -0,0 +1,92 @@ + array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'engineClassName' => 'text128', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_scope' => array( + 'columns' => array('engineClassName', 'scopePHID'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public static function initializeNewQueryConfig() { + return new self(); + } + + public function isGlobal() { + return ($this->getScopePHID() == self::SCOPE_GLOBAL); + } + + public function getConfigProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setConfigProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getStrengthSortVector() { + // Apply personal preferences before global preferences. + if (!$this->isGlobal()) { + $phase = 0; + } else { + $phase = 1; + } + + return id(new PhutilSortVector()) + ->addInt($phase) + ->addInt($this->getID()); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::POLICY_NOONE; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->isGlobal()) { + return true; + } + + if ($viewer->getPHID() == $this->getScopePHID()) { + return true; + } + + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +}