@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Separate saved queries in applications into "personal" and "global" queries

Summary:
Ref T12956. UI changes:

- Administrators get a new `[X] Save as global query` option when saving a query.
- "Edit Queries..." is split into "Personal" and "Global" sections. For administrators, each section can be edited. For non-admins, only the top section can be edited, but any query can be pinned.

A couple notes:

- This doesn't support "pin for everyone by default". New users just get the first query from the bottom set. That seems reasonable for now.
- Reordering is currently a little buggy (it works if you've reordered before, but not if you're reordering for the first time), but I need to migrate before I can fix / test that properly. So that'll get cleaned up in the next change or two.

Test Plan:
- As an admin and non-admin, viewed, edited, disabled, saved-as-personal and saved-as-global various queries.

{F5098581}

{F5098582}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12956

Differential Revision: https://secure.phabricator.com/D18426

+278 -109
+10 -3
src/applications/search/application/PhabricatorSearchApplication.php
··· 33 33 'index/(?P<phid>[^/]+)/' => 'PhabricatorSearchIndexController', 34 34 'hovercard/' 35 35 => 'PhabricatorSearchHovercardController', 36 - 'edit/(?P<queryKey>[^/]+)/' => 'PhabricatorSearchEditController', 36 + 'edit/' => array( 37 + 'key/(?P<queryKey>[^/]+)/' => 'PhabricatorSearchEditController', 38 + 'id/(?P<id>[^/]+)/' => 'PhabricatorSearchEditController', 39 + ), 37 40 'default/(?P<queryKey>[^/]+)/(?P<engine>[^/]+)/' 38 41 => 'PhabricatorSearchDefaultController', 39 - 'delete/(?P<queryKey>[^/]+)/(?P<engine>[^/]+)/' 40 - => 'PhabricatorSearchDeleteController', 42 + 'delete/' => array( 43 + 'key/(?P<queryKey>[^/]+)/(?P<engine>[^/]+)/' 44 + => 'PhabricatorSearchDeleteController', 45 + 'id/(?P<id>[^/]+)/' 46 + => 'PhabricatorSearchDeleteController', 47 + ), 41 48 'order/(?P<engine>[^/]+)/' => 'PhabricatorSearchOrderController', 42 49 'rel/(?P<relationshipKey>[^/]+)/(?P<sourcePHID>[^/]+)/' 43 50 => 'PhabricatorSearchRelationshipController',
+137 -74
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 174 174 if ($run_query && !$named_query && $user->isLoggedIn()) { 175 175 $save_button = id(new PHUIButtonView()) 176 176 ->setTag('a') 177 - ->setHref('/search/edit/'.$saved_query->getQueryKey().'/') 177 + ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/') 178 178 ->setText(pht('Save Query')) 179 179 ->setIcon('fa-floppy-o'); 180 180 $submit->addButton($save_button); ··· 377 377 private function processEditRequest() { 378 378 $parent = $this->getDelegatingController(); 379 379 $request = $this->getRequest(); 380 - $user = $request->getUser(); 380 + $viewer = $request->getUser(); 381 381 $engine = $this->getSearchEngine(); 382 382 383 383 $nav = $this->getNavigation(); ··· 387 387 388 388 $named_queries = $engine->loadAllNamedQueries(); 389 389 390 - $list_id = celerity_generate_unique_node_id(); 390 + $can_global = $viewer->getIsAdmin(); 391 + 392 + $groups = array( 393 + 'personal' => array( 394 + 'name' => pht('Personal Saved Queries'), 395 + 'items' => array(), 396 + 'edit' => true, 397 + ), 398 + 'global' => array( 399 + 'name' => pht('Global Saved Queries'), 400 + 'items' => array(), 401 + 'edit' => $can_global, 402 + ), 403 + ); 391 404 392 - $list = new PHUIObjectItemListView(); 393 - $list->setUser($user); 394 - $list->setID($list_id); 405 + foreach ($named_queries as $named_query) { 406 + if ($named_query->isGlobal()) { 407 + $group = 'global'; 408 + } else { 409 + $group = 'personal'; 410 + } 395 411 396 - Javelin::initBehavior( 397 - 'search-reorder-queries', 398 - array( 399 - 'listID' => $list_id, 400 - 'orderURI' => '/search/order/'.get_class($engine).'/', 401 - )); 412 + $groups[$group]['items'][] = $named_query; 413 + } 402 414 403 415 $default_key = $engine->getDefaultQueryKey(); 404 416 417 + $lists = array(); 418 + foreach ($groups as $group) { 419 + $lists[] = $this->newQueryListView( 420 + $group['name'], 421 + $group['items'], 422 + $default_key, 423 + $group['edit']); 424 + } 425 + 426 + $crumbs = $parent 427 + ->buildApplicationCrumbs() 428 + ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI()) 429 + ->setBorder(true); 430 + 431 + $nav->selectFilter('query/edit'); 432 + 433 + $header = id(new PHUIHeaderView()) 434 + ->setHeader(pht('Saved Queries')) 435 + ->setProfileHeader(true); 436 + 437 + $view = id(new PHUITwoColumnView()) 438 + ->setHeader($header) 439 + ->setFooter($lists); 440 + 441 + return $this->newPage() 442 + ->setApplicationMenu($this->buildApplicationMenu()) 443 + ->setTitle(pht('Saved Queries')) 444 + ->setCrumbs($crumbs) 445 + ->setNavigation($nav) 446 + ->appendChild($view); 447 + } 448 + 449 + private function newQueryListView( 450 + $list_name, 451 + array $named_queries, 452 + $default_key, 453 + $can_edit) { 454 + 455 + $engine = $this->getSearchEngine(); 456 + $viewer = $this->getViewer(); 457 + 458 + $list = id(new PHUIObjectItemListView()) 459 + ->setViewer($viewer); 460 + 461 + if ($can_edit) { 462 + $list_id = celerity_generate_unique_node_id(); 463 + $list->setID($list_id); 464 + 465 + Javelin::initBehavior( 466 + 'search-reorder-queries', 467 + array( 468 + 'listID' => $list_id, 469 + 'orderURI' => '/search/order/'.get_class($engine).'/', 470 + )); 471 + } 472 + 405 473 foreach ($named_queries as $named_query) { 406 474 $class = get_class($engine); 407 475 $key = $named_query->getQueryKey(); ··· 410 478 ->setHeader($named_query->getQueryName()) 411 479 ->setHref($engine->getQueryResultsPageURI($key)); 412 480 413 - if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { 414 - $icon = 'fa-plus'; 415 - $disable_name = pht('Enable'); 416 - } else { 417 - $icon = 'fa-times'; 418 - if ($named_query->getIsBuiltin()) { 419 - $disable_name = pht('Disable'); 481 + if ($named_query->getIsDisabled()) { 482 + if ($can_edit) { 483 + $item->setDisabled(true); 420 484 } else { 421 - $disable_name = pht('Delete'); 485 + // If an item is disabled and you don't have permission to edit it, 486 + // just skip it. 487 + continue; 422 488 } 423 489 } 424 490 425 - $item->addAction( 426 - id(new PHUIListItemView()) 427 - ->setIcon($icon) 428 - ->setHref('/search/delete/'.$key.'/'.$class.'/') 429 - ->setRenderNameAsTooltip(true) 430 - ->setName($disable_name) 431 - ->setWorkflow(true)); 491 + if ($can_edit) { 492 + if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { 493 + $icon = 'fa-plus'; 494 + $disable_name = pht('Enable'); 495 + } else { 496 + $icon = 'fa-times'; 497 + if ($named_query->getIsBuiltin()) { 498 + $disable_name = pht('Disable'); 499 + } else { 500 + $disable_name = pht('Delete'); 501 + } 502 + } 503 + 504 + if ($named_query->getID()) { 505 + $disable_href = '/search/delete/id/'.$named_query->getID().'/'; 506 + } else { 507 + $disable_href = '/search/delete/key/'.$key.'/'.$class.'/'; 508 + } 509 + 510 + $item->addAction( 511 + id(new PHUIListItemView()) 512 + ->setIcon($icon) 513 + ->setHref($disable_href) 514 + ->setRenderNameAsTooltip(true) 515 + ->setName($disable_name) 516 + ->setWorkflow(true)); 517 + } 432 518 433 519 $default_disabled = $named_query->getIsDisabled(); 434 520 $default_icon = 'fa-thumb-tack'; ··· 448 534 ->setWorkflow(true) 449 535 ->setDisabled($default_disabled)); 450 536 451 - if ($named_query->getIsBuiltin()) { 452 - $edit_icon = 'fa-lock lightgreytext'; 453 - $edit_disabled = true; 454 - $edit_name = pht('Builtin'); 455 - $edit_href = null; 456 - } else { 457 - $edit_icon = 'fa-pencil'; 458 - $edit_disabled = false; 459 - $edit_name = pht('Edit'); 460 - $edit_href = '/search/edit/'.$key.'/'; 461 - } 462 - 463 - $item->addAction( 464 - id(new PHUIListItemView()) 465 - ->setIcon($edit_icon) 466 - ->setHref($edit_href) 467 - ->setRenderNameAsTooltip(true) 468 - ->setName($edit_name) 469 - ->setDisabled($edit_disabled)); 537 + if ($can_edit) { 538 + if ($named_query->getIsBuiltin()) { 539 + $edit_icon = 'fa-lock lightgreytext'; 540 + $edit_disabled = true; 541 + $edit_name = pht('Builtin'); 542 + $edit_href = null; 543 + } else { 544 + $edit_icon = 'fa-pencil'; 545 + $edit_disabled = false; 546 + $edit_name = pht('Edit'); 547 + $edit_href = '/search/edit/id/'.$named_query->getID().'/'; 548 + } 470 549 471 - if ($named_query->getIsDisabled()) { 472 - $item->setDisabled(true); 550 + $item->addAction( 551 + id(new PHUIListItemView()) 552 + ->setIcon($edit_icon) 553 + ->setHref($edit_href) 554 + ->setRenderNameAsTooltip(true) 555 + ->setName($edit_name) 556 + ->setDisabled($edit_disabled)); 473 557 } 474 558 475 - $item->setGrippable(true); 559 + $item->setGrippable($can_edit); 476 560 $item->addSigil('named-query'); 477 561 $item->setMetadata( 478 562 array( ··· 484 568 485 569 $list->setNoDataString(pht('No saved queries.')); 486 570 487 - $crumbs = $parent 488 - ->buildApplicationCrumbs() 489 - ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI()) 490 - ->setBorder(true); 491 - 492 - $nav->selectFilter('query/edit'); 493 - 494 - $header = id(new PHUIHeaderView()) 495 - ->setHeader(pht('Saved Queries')) 496 - ->setProfileHeader(true); 497 - 498 - $box = id(new PHUIObjectBoxView()) 499 - ->setHeader($header) 500 - ->setObjectList($list) 501 - ->addClass('application-search-results'); 502 - 503 - $nav->addClass('application-search-view'); 504 - require_celerity_resource('application-search-view-css'); 505 - 506 - return $this->newPage() 507 - ->setApplicationMenu($this->buildApplicationMenu()) 508 - ->setTitle(pht('Saved Queries')) 509 - ->setCrumbs($crumbs) 510 - ->setNavigation($nav) 511 - ->appendChild($box); 571 + return id(new PHUIObjectBoxView()) 572 + ->setHeaderText($list_name) 573 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 574 + ->setObjectList($list); 512 575 } 513 576 514 577 public function buildApplicationMenu() {
+5 -1
src/applications/search/controller/PhabricatorSearchDefaultController.php
··· 21 21 ->setViewer($viewer) 22 22 ->withEngineClassNames(array($engine_class)) 23 23 ->withQueryKeys(array($key)) 24 - ->withUserPHIDs(array($viewer->getPHID())) 24 + ->withUserPHIDs( 25 + array( 26 + $viewer->getPHID(), 27 + PhabricatorNamedQuery::SCOPE_GLOBAL, 28 + )) 25 29 ->executeOne(); 26 30 27 31 if (!$named_query && $engine->isBuiltinQuery($key)) {
+32 -19
src/applications/search/controller/PhabricatorSearchDeleteController.php
··· 5 5 6 6 public function handleRequest(AphrontRequest $request) { 7 7 $viewer = $this->getViewer(); 8 - $key = $request->getURIData('queryKey'); 9 - $engine_class = $request->getURIData('engine'); 10 8 11 - $base_class = 'PhabricatorApplicationSearchEngine'; 12 - if (!is_subclass_of($engine_class, $base_class)) { 13 - return new Aphront400Response(); 14 - } 9 + $id = $request->getURIData('id'); 10 + if ($id) { 11 + $named_query = id(new PhabricatorNamedQueryQuery()) 12 + ->setViewer($viewer) 13 + ->withIDs(array($id)) 14 + ->requireCapabilities( 15 + array( 16 + PhabricatorPolicyCapability::CAN_VIEW, 17 + PhabricatorPolicyCapability::CAN_EDIT, 18 + )) 19 + ->executeOne(); 20 + if (!$named_query) { 21 + return new Aphront404Response(); 22 + } 23 + 24 + $engine = newv($named_query->getEngineClassName(), array()); 25 + $engine->setViewer($viewer); 26 + 27 + $key = $named_query->getQueryKey(); 28 + } else { 29 + $key = $request->getURIData('queryKey'); 30 + $engine_class = $request->getURIData('engine'); 31 + 32 + $base_class = 'PhabricatorApplicationSearchEngine'; 33 + if (!is_subclass_of($engine_class, $base_class)) { 34 + return new Aphront400Response(); 35 + } 15 36 16 - $engine = newv($engine_class, array()); 17 - $engine->setViewer($viewer); 37 + $engine = newv($engine_class, array()); 38 + $engine->setViewer($viewer); 18 39 19 - $named_query = id(new PhabricatorNamedQueryQuery()) 20 - ->setViewer($viewer) 21 - ->withEngineClassNames(array($engine_class)) 22 - ->withQueryKeys(array($key)) 23 - ->withUserPHIDs(array($viewer->getPHID())) 24 - ->executeOne(); 40 + if (!$engine->isBuiltinQuery($key)) { 41 + return new Aphront404Response(); 42 + } 25 43 26 - if (!$named_query && $engine->isBuiltinQuery($key)) { 27 44 $named_query = $engine->getBuiltinQuery($key); 28 - } 29 - 30 - if (!$named_query) { 31 - return new Aphront404Response(); 32 45 } 33 46 34 47 $builtin = null;
+50 -6
src/applications/search/controller/PhabricatorSearchEditController.php
··· 6 6 public function handleRequest(AphrontRequest $request) { 7 7 $viewer = $this->getViewer(); 8 8 9 + $id = $request->getURIData('id'); 10 + if ($id) { 11 + $named_query = id(new PhabricatorNamedQueryQuery()) 12 + ->setViewer($viewer) 13 + ->withIDs(array($id)) 14 + ->requireCapabilities( 15 + array( 16 + PhabricatorPolicyCapability::CAN_VIEW, 17 + PhabricatorPolicyCapability::CAN_EDIT, 18 + )) 19 + ->executeOne(); 20 + if (!$named_query) { 21 + return new Aphront404Response(); 22 + } 23 + 24 + $query_key = $named_query->getQueryKey(); 25 + } else { 26 + $query_key = $request->getURIData('queryKey'); 27 + $named_query = null; 28 + } 29 + 9 30 $saved_query = id(new PhabricatorSavedQueryQuery()) 10 31 ->setViewer($viewer) 11 - ->withQueryKeys(array($request->getURIData('queryKey'))) 32 + ->withQueryKeys(array($query_key)) 12 33 ->executeOne(); 13 34 if (!$saved_query) { 14 35 return new Aphront404Response(); ··· 19 40 $complete_uri = $engine->getQueryManagementURI(); 20 41 $cancel_uri = $complete_uri; 21 42 22 - $named_query = id(new PhabricatorNamedQueryQuery()) 23 - ->setViewer($viewer) 24 - ->withQueryKeys(array($saved_query->getQueryKey())) 25 - ->withUserPHIDs(array($viewer->getPHID())) 26 - ->executeOne(); 27 43 if (!$named_query) { 28 44 $named_query = id(new PhabricatorNamedQuery()) 29 45 ->setUserPHID($viewer->getPHID()) ··· 35 51 // management interface. 36 52 $cancel_uri = $engine->getQueryResultsPageURI( 37 53 $saved_query->getQueryKey()); 54 + 55 + $is_new = true; 56 + } else { 57 + $is_new = false; 38 58 } 39 59 60 + $can_global = ($viewer->getIsAdmin() && $is_new); 61 + 62 + $v_global = false; 63 + 40 64 $e_name = true; 41 65 $errors = array(); 42 66 43 67 if ($request->isFormPost()) { 68 + if ($can_global) { 69 + $v_global = $request->getBool('global'); 70 + if ($v_global) { 71 + $named_query->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL); 72 + } 73 + } 74 + 44 75 $named_query->setQueryName($request->getStr('name')); 45 76 if (!strlen($named_query->getQueryName())) { 46 77 $e_name = pht('Required'); ··· 50 81 } 51 82 52 83 if (!$errors) { 84 + 53 85 $named_query->save(); 54 86 return id(new AphrontRedirectResponse())->setURI($complete_uri); 55 87 } ··· 64 96 ->setLabel(pht('Query Name')) 65 97 ->setValue($named_query->getQueryName()) 66 98 ->setError($e_name)); 99 + 100 + if ($can_global) { 101 + $form->appendChild( 102 + id(new AphrontFormCheckboxControl()) 103 + ->addCheckbox( 104 + 'global', 105 + '1', 106 + pht( 107 + 'Save this query as a global query, making it visible to '. 108 + 'all users.'), 109 + $v_global)); 110 + } 67 111 68 112 $form->appendChild( 69 113 id(new AphrontFormSubmitControl())
+7 -3
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 474 474 if ($this->namedQueries === null) { 475 475 $named_queries = id(new PhabricatorNamedQueryQuery()) 476 476 ->setViewer($viewer) 477 - ->withUserPHIDs(array($viewer->getPHID())) 478 477 ->withEngineClassNames(array(get_class($this))) 478 + ->withUserPHIDs( 479 + array( 480 + $viewer->getPHID(), 481 + PhabricatorNamedQuery::SCOPE_GLOBAL, 482 + )) 479 483 ->execute(); 480 484 $named_queries = mpull($named_queries, null, 'getQueryKey'); 481 485 ··· 494 498 unset($builtin[$key]); 495 499 } 496 500 497 - $named_queries = msort($named_queries, 'getSortKey'); 501 + $named_queries = msortv($named_queries, 'getNamedQuerySortVector'); 498 502 $this->namedQueries = $named_queries; 499 503 } 500 504 ··· 631 635 $sequence = 0; 632 636 foreach ($names as $key => $name) { 633 637 $queries[$key] = id(new PhabricatorNamedQuery()) 634 - ->setUserPHID($this->requireViewer()->getPHID()) 638 + ->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL) 635 639 ->setEngineClassName(get_class($this)) 636 640 ->setQueryName($name) 637 641 ->setQueryKey($key)
+37 -3
src/applications/search/storage/PhabricatorNamedQuery.php
··· 12 12 protected $isDisabled = 0; 13 13 protected $sequence = 0; 14 14 15 + const SCOPE_GLOBAL = 'scope.global'; 16 + 15 17 protected function getConfiguration() { 16 18 return array( 17 19 self::CONFIG_COLUMN_SCHEMA => array( ··· 31 33 ) + parent::getConfiguration(); 32 34 } 33 35 34 - public function getSortKey() { 35 - return sprintf('~%010d%010d', $this->sequence, $this->getID()); 36 + public function isGlobal() { 37 + if ($this->getIsBuiltin()) { 38 + return true; 39 + } 40 + 41 + if ($this->getUserPHID() === self::SCOPE_GLOBAL) { 42 + return true; 43 + } 44 + 45 + return false; 46 + } 47 + 48 + public function getNamedQuerySortVector() { 49 + if (!$this->isGlobal()) { 50 + $phase = 0; 51 + } else { 52 + $phase = 1; 53 + } 54 + 55 + return id(new PhutilSortVector()) 56 + ->addInt($phase) 57 + ->addInt($this->sequence) 58 + ->addInt($this->getID()); 36 59 } 37 60 38 61 /* -( PhabricatorPolicyInterface )----------------------------------------- */ ··· 41 64 public function getCapabilities() { 42 65 return array( 43 66 PhabricatorPolicyCapability::CAN_VIEW, 67 + PhabricatorPolicyCapability::CAN_EDIT, 44 68 ); 45 69 } 46 70 ··· 49 73 } 50 74 51 75 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 52 - if ($viewer->getPHID() == $this->userPHID) { 76 + if ($viewer->getPHID() == $this->getUserPHID()) { 53 77 return true; 54 78 } 79 + 80 + if ($this->isGlobal()) { 81 + switch ($capability) { 82 + case PhabricatorPolicyCapability::CAN_VIEW: 83 + return true; 84 + case PhabricatorPolicyCapability::CAN_EDIT: 85 + return $viewer->getIsAdmin(); 86 + } 87 + } 88 + 55 89 return false; 56 90 } 57 91