@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.

Implement generalized application search in Macros

Summary: Ref T2625. Works out the last kinks of generalization and gives Macros the more powerful new query engine. Overall, this feels pretty good to me.

Test Plan: Executed, saved and edited a bunch of Macro queries.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2625

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

+226 -151
+7 -1
src/__phutil_library_map__.php
··· 1088 1088 'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php', 1089 1089 'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php', 1090 1090 'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php', 1091 + 'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php', 1091 1092 'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php', 1092 1093 'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php', 1093 1094 'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php', ··· 2874 2875 'PhabricatorMacroDisableController' => 'PhabricatorMacroController', 2875 2876 'PhabricatorMacroEditController' => 'PhabricatorMacroController', 2876 2877 'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor', 2877 - 'PhabricatorMacroListController' => 'PhabricatorMacroController', 2878 + 'PhabricatorMacroListController' => 2879 + array( 2880 + 0 => 'PhabricatorMacroController', 2881 + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', 2882 + ), 2878 2883 'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver', 2879 2884 'PhabricatorMacroMemeController' => 'PhabricatorMacroController', 2880 2885 'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController', 2881 2886 'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 2882 2887 'PhabricatorMacroReplyHandler' => 'PhabricatorMailReplyHandler', 2888 + 'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine', 2883 2889 'PhabricatorMacroTransaction' => 'PhabricatorApplicationTransaction', 2884 2890 'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment', 2885 2891 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+1 -1
src/applications/macro/application/PhabricatorApplicationMacro.php
··· 25 25 public function getRoutes() { 26 26 return array( 27 27 '/macro/' => array( 28 - '((?P<filter>all|active|my)/)?' => 'PhabricatorMacroListController', 28 + '(query/(?P<key>[^/]+)/)?' => 'PhabricatorMacroListController', 29 29 'create/' => 'PhabricatorMacroEditController', 30 30 'view/(?P<id>[1-9]\d*)/' => 'PhabricatorMacroViewController', 31 31 'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorMacroCommentController',
+4 -11
src/applications/macro/controller/PhabricatorMacroController.php
··· 3 3 abstract class PhabricatorMacroController 4 4 extends PhabricatorController { 5 5 6 - protected function buildSideNavView($for_app = false, $has_search = false) { 6 + protected function buildSideNavView($for_app = false) { 7 7 $nav = new AphrontSideNavFilterView(); 8 8 $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 9 9 ··· 14 14 $this->getApplicationURI('/create/')); 15 15 } 16 16 17 - $nav->addLabel(pht('Macros')); 18 - $nav->addFilter('active', pht('Active Macros')); 19 - $nav->addFilter('all', pht('All Macros')); 20 - $nav->addFilter('my', pht('My Macros')); 21 - if ($has_search) { 22 - $nav->addFilter('search', 23 - pht('Search'), 24 - $this->getRequest()->getRequestURI()); 25 - } 26 - 17 + id(new PhabricatorMacroSearchEngine()) 18 + ->setViewer($this->getRequest()->getUser()) 19 + ->addNavigationItems($nav); 27 20 28 21 return $nav; 29 22 }
+36 -114
src/applications/macro/controller/PhabricatorMacroListController.php
··· 1 1 <?php 2 2 3 - final class PhabricatorMacroListController 4 - extends PhabricatorMacroController { 3 + final class PhabricatorMacroListController extends PhabricatorMacroController 4 + implements PhabricatorApplicationSearchResultsControllerInterface { 5 5 6 - private $filter; 6 + private $key; 7 + 8 + public function shouldAllowPublic() { 9 + return true; 10 + } 7 11 8 12 public function willProcessRequest(array $data) { 9 - $this->filter = idx($data, 'filter', 'active'); 13 + $this->key = idx($data, 'key', 'active'); 10 14 } 11 15 12 16 public function processRequest() { 13 - 14 17 $request = $this->getRequest(); 15 - $viewer = $request->getUser(); 16 - 17 - $pager = id(new AphrontCursorPagerView()) 18 - ->readFromRequest($request); 19 - 20 - $query = new PhabricatorMacroQuery(); 21 - $query->setViewer($viewer); 18 + $controller = id(new PhabricatorApplicationSearchController($request)) 19 + ->setQueryKey($this->key) 20 + ->setSearchEngine(new PhabricatorMacroSearchEngine()) 21 + ->setNavigation($this->buildSideNavView()); 22 22 23 - $filter = $request->getStr('name'); 24 - if (strlen($filter)) { 25 - $query->withNameLike($filter); 26 - } 23 + return $this->delegateToController($controller); 24 + } 27 25 28 - $authors = $request->getArr('authors'); 26 + public function renderResultsList(array $macros) { 27 + assert_instances_of($macros, 'PhabricatorFileImageMacro'); 28 + $viewer = $this->getRequest()->getUser(); 29 29 30 - if ($authors) { 31 - $query->withAuthorPHIDs($authors); 32 - } 33 - 34 - $has_search = $filter || $authors; 35 - 36 - if ($this->filter == 'my') { 37 - $query->withAuthorPHIDs(array($viewer->getPHID())); 38 - // For pre-filling the tokenizer 39 - $authors = array($viewer->getPHID()); 40 - } 41 - 42 - if ($this->filter == 'active') { 43 - $query->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE); 44 - } 45 - 46 - $macros = $query->executeWithCursorPager($pager); 47 - if ($has_search) { 48 - $nodata = pht('There are no macros matching the filter.'); 49 - } else { 50 - $nodata = pht('There are no image macros yet.'); 51 - } 52 - 53 - if ($authors) { 54 - $author_phids = array_fuse($authors); 55 - } else { 56 - $author_phids = array(); 57 - } 58 - 59 - $author_phids += mpull($macros, 'getAuthorPHID', 'getAuthorPHID'); 60 - 30 + $author_phids = mpull($macros, 'getAuthorPHID', 'getAuthorPHID'); 61 31 $this->loadHandles($author_phids); 62 - $author_handles = array_select_keys($this->getLoadedHandles(), $authors); 63 - 64 - $filter_form = id(new AphrontFormView()) 65 - ->setMethod('GET') 66 - ->setUser($request->getUser()) 67 - ->setNoShading(true) 68 - ->appendChild( 69 - id(new AphrontFormTextControl()) 70 - ->setName('name') 71 - ->setLabel(pht('Name')) 72 - ->setValue($filter)) 73 - ->appendChild( 74 - id(new AphrontFormTokenizerControl()) 75 - ->setName('authors') 76 - ->setLabel(pht('Authors')) 77 - ->setDatasource('/typeahead/common/users/') 78 - ->setValue(mpull($author_handles, 'getFullName'))) 79 - ->appendChild( 80 - id(new AphrontFormSubmitControl()) 81 - ->setValue(pht('Filter Image Macros'))); 82 - 83 - $filter_view = new AphrontListFilterView(); 84 - $filter_view->appendChild($filter_form); 85 - 86 - $nav = $this->buildSideNavView( 87 - $for_app = false, 88 - $has_search); 89 - $nav->selectFilter($has_search ? 'search' : $this->filter); 90 - 91 - $nav->appendChild($filter_view); 32 + $author_handles = array_select_keys( 33 + $this->getLoadedHandles(), 34 + $author_phids); 92 35 93 36 $pinboard = new PhabricatorPinboardView(); 94 - $pinboard->setNoDataString($nodata); 95 37 foreach ($macros as $macro) { 96 38 $file = $macro->getFile(); 97 39 ··· 108 50 'div', 109 51 array(), 110 52 pht('Created on %s', $datetime))); 53 + } else { 54 + // Very old macros don't have a creation date. Rendering something 55 + // keeps all the pins at the same height and avoids flow issues. 56 + $item->appendChild( 57 + phutil_tag( 58 + 'div', 59 + array(), 60 + pht('Created in ages long past'))); 111 61 } 112 62 113 63 if ($macro->getAuthorPHID()) { ··· 117 67 } 118 68 119 69 $item->setURI($this->getApplicationURI('/view/'.$macro->getID().'/')); 120 - $item->setHeader($macro->getName()); 70 + 71 + $name = $macro->getName(); 72 + if ($macro->getIsDisabled()) { 73 + $name = pht('%s (Disabled)', $name); 74 + } 75 + $item->setHeader($name); 121 76 122 77 $pinboard->addItem($item); 123 78 } 124 - $nav->appendChild($pinboard); 125 79 126 - if (!$has_search) { 127 - $nav->appendChild($pager); 128 - switch ($this->filter) { 129 - case 'all': 130 - $name = pht('All Macros'); 131 - break; 132 - case 'my': 133 - $name = pht('My Macros'); 134 - break; 135 - case 'active': 136 - $name = pht('Active Macros'); 137 - break; 138 - default: 139 - throw new Exception("Unknown filter $this->filter"); 140 - break; 141 - } 142 - } else { 143 - $name = pht('Search'); 144 - } 80 + return $pinboard; 145 81 146 - $crumbs = $this->buildApplicationCrumbs(); 147 - $crumbs->addCrumb( 148 - id(new PhabricatorCrumbView()) 149 - ->setName($name) 150 - ->setHref($request->getRequestURI())); 151 - $nav->setCrumbs($crumbs); 152 - 153 - return $this->buildApplicationPage( 154 - $nav, 155 - array( 156 - 'device' => true, 157 - 'title' => pht('Image Macros'), 158 - 'dust' => true, 159 - )); 160 82 } 161 83 }
+24 -4
src/applications/macro/query/PhabricatorMacroQuery.php
··· 15 15 private $status = 'status-any'; 16 16 const STATUS_ANY = 'status-any'; 17 17 const STATUS_ACTIVE = 'status-active'; 18 + const STATUS_DISABLED = 'status-disabled'; 19 + 20 + public static function getStatusOptions() { 21 + return array( 22 + self::STATUS_ACTIVE => pht('Active Macros'), 23 + self::STATUS_DISABLED => pht('Disabled Macros'), 24 + self::STATUS_ANY => pht('Active and Disabled Macros'), 25 + ); 26 + } 18 27 19 28 public function withIDs(array $ids) { 20 29 $this->ids = $ids; ··· 99 108 $this->names); 100 109 } 101 110 102 - if ($this->status == self::STATUS_ACTIVE) { 103 - $where[] = qsprintf( 104 - $conn, 105 - 'm.isDisabled = 0'); 111 + switch ($this->status) { 112 + case self::STATUS_ACTIVE: 113 + $where[] = qsprintf( 114 + $conn, 115 + 'm.isDisabled = 0'); 116 + break; 117 + case self::STATUS_DISABLED: 118 + $where[] = qsprintf( 119 + $conn, 120 + 'm.isDisabled = 1'); 121 + break; 122 + case self::STATUS_ANY: 123 + break; 124 + default: 125 + throw new Exception("Unknown status '{$this->status}'!"); 106 126 } 107 127 108 128 $where[] = $this->buildPagingClause($conn);
+122
src/applications/macro/query/PhabricatorMacroSearchEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorMacroSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + public function buildSavedQueryFromRequest(AphrontRequest $request) { 7 + $saved = new PhabricatorSavedQuery(); 8 + $saved->setParameter( 9 + 'authorPHIDs', 10 + array_values($request->getArr('authors'))); 11 + 12 + $saved->setParameter('status', $request->getStr('status')); 13 + $saved->setParameter('names', $request->getStrList('names')); 14 + $saved->setParameter('nameLike', $request->getStr('nameLike')); 15 + 16 + return $saved; 17 + } 18 + 19 + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 20 + $query = id(new PhabricatorMacroQuery()) 21 + ->withIDs($saved->getParameter('ids', array())) 22 + ->withPHIDs($saved->getParameter('phids', array())) 23 + ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array())); 24 + 25 + $status = $saved->getParameter('status'); 26 + $options = PhabricatorMacroQuery::getStatusOptions(); 27 + if (empty($options[$status])) { 28 + $status = head_key($options); 29 + } 30 + $query->withStatus($status); 31 + 32 + $names = $saved->getParameter('names', array()); 33 + if ($names) { 34 + $query->withNames($names); 35 + } 36 + 37 + $like = $saved->getParameter('nameLike'); 38 + if (strlen($like)) { 39 + $query->withNameLike($like); 40 + } 41 + 42 + return $query; 43 + } 44 + 45 + public function buildSearchForm( 46 + AphrontFormView $form, 47 + PhabricatorSavedQuery $saved_query) { 48 + 49 + $phids = $saved_query->getParameter('authorPHIDs', array()); 50 + $handles = id(new PhabricatorObjectHandleData($phids)) 51 + ->setViewer($this->requireViewer()) 52 + ->loadHandles(); 53 + $author_tokens = mpull($handles, 'getFullName', 'getPHID'); 54 + 55 + $status = $saved_query->getParameter('status'); 56 + $names = implode(', ', $saved_query->getParameter('names', array())); 57 + $like = $saved_query->getParameter('nameLike'); 58 + 59 + $form 60 + ->appendChild( 61 + id(new AphrontFormSelectControl()) 62 + ->setName('status') 63 + ->setLabel(pht('Status')) 64 + ->setOptions(PhabricatorMacroQuery::getStatusOptions()) 65 + ->setValue($status)) 66 + ->appendChild( 67 + id(new AphrontFormTokenizerControl()) 68 + ->setDatasource('/typeahead/common/users/') 69 + ->setName('authors') 70 + ->setLabel(pht('Authors')) 71 + ->setValue($author_tokens)) 72 + ->appendChild( 73 + id(new AphrontFormTextControl()) 74 + ->setName('nameLike') 75 + ->setLabel(pht('Name Contains')) 76 + ->setValue($like)) 77 + ->appendChild( 78 + id(new AphrontFormTextControl()) 79 + ->setName('names') 80 + ->setLabel(pht('Exact Names')) 81 + ->setValue($names)); 82 + } 83 + 84 + protected function getURI($path) { 85 + return '/macro/'.$path; 86 + } 87 + 88 + public function getBuiltinQueryNames() { 89 + $names = array( 90 + 'active' => pht('Active'), 91 + 'all' => pht('All'), 92 + ); 93 + 94 + if ($this->requireViewer()->isLoggedIn()) { 95 + $names['authored'] = pht('Authored'); 96 + } 97 + 98 + return $names; 99 + } 100 + 101 + public function buildSavedQueryFromBuiltin($query_key) { 102 + 103 + $query = $this->newSavedQuery(); 104 + $query->setQueryKey($query_key); 105 + 106 + switch ($query_key) { 107 + case 'active': 108 + return $query; 109 + case 'all': 110 + return $query->setParameter( 111 + 'status', 112 + PhabricatorMacroQuery::STATUS_ANY); 113 + case 'authored': 114 + return $query->setParameter( 115 + 'authorPHIDs', 116 + array($this->requireViewer()->getPHID())); 117 + } 118 + 119 + return parent::buildSavedQueryFromBuiltin($query_key); 120 + } 121 + 122 + }
+1 -8
src/applications/paste/query/PhabricatorPasteSearchEngine.php
··· 20 20 'authorPHIDs', 21 21 array_values($request->getArr('authors'))); 22 22 23 - try { 24 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 25 - $saved->save(); 26 - unset($unguarded); 27 - } catch (AphrontQueryDuplicateKeyException $ex) { 28 - // Ignore, this is just a repeated search. 29 - } 30 - 31 23 return $saved; 32 24 } 33 25 ··· 39 31 */ 40 32 public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 41 33 $query = id(new PhabricatorPasteQuery()) 34 + ->needContent(true) 42 35 ->withIDs($saved->getParameter('ids', array())) 43 36 ->withPHIDs($saved->getParameter('phids', array())) 44 37 ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array()))
+26 -8
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 84 84 $nav = $this->getNavigation(); 85 85 86 86 if ($request->isFormPost()) { 87 + $saved_query = $engine->buildSavedQueryFromRequest($request); 88 + $this->saveQuery($saved_query); 87 89 return id(new AphrontRedirectResponse())->setURI( 88 - $engine->getQueryResultsPageURI( 89 - $engine->buildSavedQueryFromRequest($request)->getQueryKey())); 90 + $engine->getQueryResultsPageURI($saved_query->getQueryKey())); 90 91 } 91 92 92 93 $named_query = null; ··· 163 164 $nav->appendChild($filter_view); 164 165 165 166 if ($run_query) { 166 - $query = id(new PhabricatorPasteSearchEngine()) 167 - ->buildQueryFromSavedQuery($saved_query); 167 + $query = $engine->buildQueryFromSavedQuery($saved_query); 168 168 169 169 $pager = new AphrontCursorPagerView(); 170 170 $pager->readFromRequest($request); 171 - $pastes = $query->setViewer($request->getUser()) 172 - ->needContent(true) 171 + $objects = $query->setViewer($request->getUser()) 173 172 ->executeWithCursorPager($pager); 174 173 175 - $list = $parent->renderResultsList($pastes); 176 - $list->setPager($pager); 174 + $list = $parent->renderResultsList($objects); 177 175 $list->setNoDataString(pht("No results found for this query.")); 178 176 179 177 $nav->appendChild($list); 178 + 179 + // TODO: This is a bit hacky. 180 + if ($list instanceof PhabricatorObjectItemListView) { 181 + $list->setPager($pager); 182 + } else { 183 + $nav->appendChild($pager); 184 + } 180 185 } 181 186 182 187 if ($named_query) { ··· 269 274 'dust' => true, 270 275 )); 271 276 } 277 + 278 + private function saveQuery(PhabricatorSavedQuery $query) { 279 + $query->setEngineClassName(get_class($this->getSearchEngine())); 280 + 281 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 282 + try { 283 + $query->save(); 284 + } catch (AphrontQueryDuplicateKeyException $ex) { 285 + // Ignore, this is just a repeated search. 286 + } 287 + unset($unguarded); 288 + } 289 + 272 290 }
+5 -4
src/applications/search/storage/PhabricatorSavedQuery.php
··· 7 7 implements PhabricatorPolicyInterface { 8 8 9 9 protected $parameters = array(); 10 - protected $queryKey = ""; 11 - protected $engineClassName = "PhabricatorPasteSearchEngine"; 10 + protected $queryKey; 11 + protected $engineClassName; 12 12 13 13 public function getConfiguration() { 14 14 return array( 15 15 self::CONFIG_SERIALIZATION => array( 16 - 'parameters' => self::SERIALIZATION_JSON), ) 17 - + parent::getConfiguration(); 16 + 'parameters' => self::SERIALIZATION_JSON, 17 + ), 18 + ) + parent::getConfiguration(); 18 19 } 19 20 20 21 public function setParameter($key, $value) {