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 @@ -33,11 +33,18 @@ 'index/(?P[^/]+)/' => 'PhabricatorSearchIndexController', 'hovercard/' => 'PhabricatorSearchHovercardController', - 'edit/(?P[^/]+)/' => 'PhabricatorSearchEditController', + 'edit/' => array( + 'key/(?P[^/]+)/' => 'PhabricatorSearchEditController', + 'id/(?P[^/]+)/' => 'PhabricatorSearchEditController', + ), 'default/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorSearchDefaultController', - 'delete/(?P[^/]+)/(?P[^/]+)/' - => 'PhabricatorSearchDeleteController', + 'delete/' => array( + 'key/(?P[^/]+)/(?P[^/]+)/' + => 'PhabricatorSearchDeleteController', + 'id/(?P[^/]+)/' + => 'PhabricatorSearchDeleteController', + ), 'order/(?P[^/]+)/' => 'PhabricatorSearchOrderController', 'rel/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorSearchRelationshipController', 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 @@ -174,7 +174,7 @@ if ($run_query && !$named_query && $user->isLoggedIn()) { $save_button = id(new PHUIButtonView()) ->setTag('a') - ->setHref('/search/edit/'.$saved_query->getQueryKey().'/') + ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/') ->setText(pht('Save Query')) ->setIcon('fa-floppy-o'); $submit->addButton($save_button); @@ -377,7 +377,7 @@ private function processEditRequest() { $parent = $this->getDelegatingController(); $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $engine = $this->getSearchEngine(); $nav = $this->getNavigation(); @@ -387,21 +387,89 @@ $named_queries = $engine->loadAllNamedQueries(); - $list_id = celerity_generate_unique_node_id(); + $can_global = $viewer->getIsAdmin(); + + $groups = array( + 'personal' => array( + 'name' => pht('Personal Saved Queries'), + 'items' => array(), + 'edit' => true, + ), + 'global' => array( + 'name' => pht('Global Saved Queries'), + 'items' => array(), + 'edit' => $can_global, + ), + ); - $list = new PHUIObjectItemListView(); - $list->setUser($user); - $list->setID($list_id); + foreach ($named_queries as $named_query) { + if ($named_query->isGlobal()) { + $group = 'global'; + } else { + $group = 'personal'; + } - Javelin::initBehavior( - 'search-reorder-queries', - array( - 'listID' => $list_id, - 'orderURI' => '/search/order/'.get_class($engine).'/', - )); + $groups[$group]['items'][] = $named_query; + } $default_key = $engine->getDefaultQueryKey(); + $lists = array(); + foreach ($groups as $group) { + $lists[] = $this->newQueryListView( + $group['name'], + $group['items'], + $default_key, + $group['edit']); + } + + $crumbs = $parent + ->buildApplicationCrumbs() + ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI()) + ->setBorder(true); + + $nav->selectFilter('query/edit'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Saved Queries')) + ->setProfileHeader(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter($lists); + + return $this->newPage() + ->setApplicationMenu($this->buildApplicationMenu()) + ->setTitle(pht('Saved Queries')) + ->setCrumbs($crumbs) + ->setNavigation($nav) + ->appendChild($view); + } + + private function newQueryListView( + $list_name, + array $named_queries, + $default_key, + $can_edit) { + + $engine = $this->getSearchEngine(); + $viewer = $this->getViewer(); + + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + + if ($can_edit) { + $list_id = celerity_generate_unique_node_id(); + $list->setID($list_id); + + Javelin::initBehavior( + 'search-reorder-queries', + array( + 'listID' => $list_id, + 'orderURI' => '/search/order/'.get_class($engine).'/', + )); + } + foreach ($named_queries as $named_query) { $class = get_class($engine); $key = $named_query->getQueryKey(); @@ -410,25 +478,43 @@ ->setHeader($named_query->getQueryName()) ->setHref($engine->getQueryResultsPageURI($key)); - 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'); + if ($named_query->getIsDisabled()) { + if ($can_edit) { + $item->setDisabled(true); } else { - $disable_name = pht('Delete'); + // If an item is disabled and you don't have permission to edit it, + // just skip it. + continue; } } - $item->addAction( - id(new PHUIListItemView()) - ->setIcon($icon) - ->setHref('/search/delete/'.$key.'/'.$class.'/') - ->setRenderNameAsTooltip(true) - ->setName($disable_name) - ->setWorkflow(true)); + if ($can_edit) { + 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'); + } + } + + if ($named_query->getID()) { + $disable_href = '/search/delete/id/'.$named_query->getID().'/'; + } else { + $disable_href = '/search/delete/key/'.$key.'/'.$class.'/'; + } + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon($icon) + ->setHref($disable_href) + ->setRenderNameAsTooltip(true) + ->setName($disable_name) + ->setWorkflow(true)); + } $default_disabled = $named_query->getIsDisabled(); $default_icon = 'fa-thumb-tack'; @@ -448,31 +534,29 @@ ->setWorkflow(true) ->setDisabled($default_disabled)); - if ($named_query->getIsBuiltin()) { - $edit_icon = 'fa-lock lightgreytext'; - $edit_disabled = true; - $edit_name = pht('Builtin'); - $edit_href = null; - } else { - $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 ($can_edit) { + if ($named_query->getIsBuiltin()) { + $edit_icon = 'fa-lock lightgreytext'; + $edit_disabled = true; + $edit_name = pht('Builtin'); + $edit_href = null; + } else { + $edit_icon = 'fa-pencil'; + $edit_disabled = false; + $edit_name = pht('Edit'); + $edit_href = '/search/edit/id/'.$named_query->getID().'/'; + } - if ($named_query->getIsDisabled()) { - $item->setDisabled(true); + $item->addAction( + id(new PHUIListItemView()) + ->setIcon($edit_icon) + ->setHref($edit_href) + ->setRenderNameAsTooltip(true) + ->setName($edit_name) + ->setDisabled($edit_disabled)); } - $item->setGrippable(true); + $item->setGrippable($can_edit); $item->addSigil('named-query'); $item->setMetadata( array( @@ -484,31 +568,10 @@ $list->setNoDataString(pht('No saved queries.')); - $crumbs = $parent - ->buildApplicationCrumbs() - ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI()) - ->setBorder(true); - - $nav->selectFilter('query/edit'); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Saved Queries')) - ->setProfileHeader(true); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setObjectList($list) - ->addClass('application-search-results'); - - $nav->addClass('application-search-view'); - require_celerity_resource('application-search-view-css'); - - return $this->newPage() - ->setApplicationMenu($this->buildApplicationMenu()) - ->setTitle(pht('Saved Queries')) - ->setCrumbs($crumbs) - ->setNavigation($nav) - ->appendChild($box); + return id(new PHUIObjectBoxView()) + ->setHeaderText($list_name) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($list); } public function buildApplicationMenu() { diff --git a/src/applications/search/controller/PhabricatorSearchDefaultController.php b/src/applications/search/controller/PhabricatorSearchDefaultController.php --- a/src/applications/search/controller/PhabricatorSearchDefaultController.php +++ b/src/applications/search/controller/PhabricatorSearchDefaultController.php @@ -21,7 +21,11 @@ ->setViewer($viewer) ->withEngineClassNames(array($engine_class)) ->withQueryKeys(array($key)) - ->withUserPHIDs(array($viewer->getPHID())) + ->withUserPHIDs( + array( + $viewer->getPHID(), + PhabricatorNamedQuery::SCOPE_GLOBAL, + )) ->executeOne(); if (!$named_query && $engine->isBuiltinQuery($key)) { diff --git a/src/applications/search/controller/PhabricatorSearchDeleteController.php b/src/applications/search/controller/PhabricatorSearchDeleteController.php --- a/src/applications/search/controller/PhabricatorSearchDeleteController.php +++ b/src/applications/search/controller/PhabricatorSearchDeleteController.php @@ -5,30 +5,43 @@ public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); - $key = $request->getURIData('queryKey'); - $engine_class = $request->getURIData('engine'); - $base_class = 'PhabricatorApplicationSearchEngine'; - if (!is_subclass_of($engine_class, $base_class)) { - return new Aphront400Response(); - } + $id = $request->getURIData('id'); + if ($id) { + $named_query = id(new PhabricatorNamedQueryQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$named_query) { + return new Aphront404Response(); + } - $engine = newv($engine_class, array()); - $engine->setViewer($viewer); + $engine = newv($named_query->getEngineClassName(), array()); + $engine->setViewer($viewer); - $named_query = id(new PhabricatorNamedQueryQuery()) - ->setViewer($viewer) - ->withEngineClassNames(array($engine_class)) - ->withQueryKeys(array($key)) - ->withUserPHIDs(array($viewer->getPHID())) - ->executeOne(); + $key = $named_query->getQueryKey(); + } else { + $key = $request->getURIData('queryKey'); + $engine_class = $request->getURIData('engine'); - if (!$named_query && $engine->isBuiltinQuery($key)) { - $named_query = $engine->getBuiltinQuery($key); - } + $base_class = 'PhabricatorApplicationSearchEngine'; + if (!is_subclass_of($engine_class, $base_class)) { + return new Aphront400Response(); + } + + $engine = newv($engine_class, array()); + $engine->setViewer($viewer); - if (!$named_query) { - return new Aphront404Response(); + if (!$engine->isBuiltinQuery($key)) { + return new Aphront404Response(); + } + + $named_query = $engine->getBuiltinQuery($key); } $builtin = null; 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 @@ -6,9 +6,30 @@ public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + if ($id) { + $named_query = id(new PhabricatorNamedQueryQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$named_query) { + return new Aphront404Response(); + } + + $query_key = $named_query->getQueryKey(); + } else { + $query_key = $request->getURIData('queryKey'); + $named_query = null; + } + $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) - ->withQueryKeys(array($request->getURIData('queryKey'))) + ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved_query) { return new Aphront404Response(); @@ -19,11 +40,6 @@ $complete_uri = $engine->getQueryManagementURI(); $cancel_uri = $complete_uri; - $named_query = id(new PhabricatorNamedQueryQuery()) - ->setViewer($viewer) - ->withQueryKeys(array($saved_query->getQueryKey())) - ->withUserPHIDs(array($viewer->getPHID())) - ->executeOne(); if (!$named_query) { $named_query = id(new PhabricatorNamedQuery()) ->setUserPHID($viewer->getPHID()) @@ -35,12 +51,27 @@ // management interface. $cancel_uri = $engine->getQueryResultsPageURI( $saved_query->getQueryKey()); + + $is_new = true; + } else { + $is_new = false; } + $can_global = ($viewer->getIsAdmin() && $is_new); + + $v_global = false; + $e_name = true; $errors = array(); if ($request->isFormPost()) { + if ($can_global) { + $v_global = $request->getBool('global'); + if ($v_global) { + $named_query->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL); + } + } + $named_query->setQueryName($request->getStr('name')); if (!strlen($named_query->getQueryName())) { $e_name = pht('Required'); @@ -50,6 +81,7 @@ } if (!$errors) { + $named_query->save(); return id(new AphrontRedirectResponse())->setURI($complete_uri); } @@ -65,6 +97,18 @@ ->setValue($named_query->getQueryName()) ->setError($e_name)); + if ($can_global) { + $form->appendChild( + id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'global', + '1', + pht( + 'Save this query as a global query, making it visible to '. + 'all users.'), + $v_global)); + } + $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Query')) 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 @@ -474,8 +474,12 @@ if ($this->namedQueries === null) { $named_queries = id(new PhabricatorNamedQueryQuery()) ->setViewer($viewer) - ->withUserPHIDs(array($viewer->getPHID())) ->withEngineClassNames(array(get_class($this))) + ->withUserPHIDs( + array( + $viewer->getPHID(), + PhabricatorNamedQuery::SCOPE_GLOBAL, + )) ->execute(); $named_queries = mpull($named_queries, null, 'getQueryKey'); @@ -494,7 +498,7 @@ unset($builtin[$key]); } - $named_queries = msort($named_queries, 'getSortKey'); + $named_queries = msortv($named_queries, 'getNamedQuerySortVector'); $this->namedQueries = $named_queries; } @@ -631,7 +635,7 @@ $sequence = 0; foreach ($names as $key => $name) { $queries[$key] = id(new PhabricatorNamedQuery()) - ->setUserPHID($this->requireViewer()->getPHID()) + ->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL) ->setEngineClassName(get_class($this)) ->setQueryName($name) ->setQueryKey($key) diff --git a/src/applications/search/storage/PhabricatorNamedQuery.php b/src/applications/search/storage/PhabricatorNamedQuery.php --- a/src/applications/search/storage/PhabricatorNamedQuery.php +++ b/src/applications/search/storage/PhabricatorNamedQuery.php @@ -12,6 +12,8 @@ protected $isDisabled = 0; protected $sequence = 0; + const SCOPE_GLOBAL = 'scope.global'; + protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( @@ -31,8 +33,29 @@ ) + parent::getConfiguration(); } - public function getSortKey() { - return sprintf('~%010d%010d', $this->sequence, $this->getID()); + public function isGlobal() { + if ($this->getIsBuiltin()) { + return true; + } + + if ($this->getUserPHID() === self::SCOPE_GLOBAL) { + return true; + } + + return false; + } + + public function getNamedQuerySortVector() { + if (!$this->isGlobal()) { + $phase = 0; + } else { + $phase = 1; + } + + return id(new PhutilSortVector()) + ->addInt($phase) + ->addInt($this->sequence) + ->addInt($this->getID()); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -41,6 +64,7 @@ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -49,9 +73,19 @@ } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($viewer->getPHID() == $this->userPHID) { + if ($viewer->getPHID() == $this->getUserPHID()) { return true; } + + if ($this->isGlobal()) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return true; + case PhabricatorPolicyCapability::CAN_EDIT: + return $viewer->getIsAdmin(); + } + } + return false; }