@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

Make mobile navigation work properly by default in more cases

Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:

- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.

Specifically, here's what this does:

- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.

Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T5752

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

+428 -199
+6 -1
src/__phutil_library_map__.php
··· 1403 1403 'PHUI' => 'view/phui/PHUI.php', 1404 1404 'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php', 1405 1405 'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php', 1406 + 'PHUIApplicationMenuView' => 'view/layout/PHUIApplicationMenuView.php', 1406 1407 'PHUIBadgeBoxView' => 'view/phui/PHUIBadgeBoxView.php', 1407 1408 'PHUIBadgeExample' => 'applications/uiexample/examples/PHUIBadgeExample.php', 1408 1409 'PHUIBadgeMiniView' => 'view/phui/PHUIBadgeMiniView.php', ··· 5310 5311 'PHUI' => 'Phobject', 5311 5312 'PHUIActionPanelExample' => 'PhabricatorUIExample', 5312 5313 'PHUIActionPanelView' => 'AphrontTagView', 5314 + 'PHUIApplicationMenuView' => 'Phobject', 5313 5315 'PHUIBadgeBoxView' => 'AphrontTagView', 5314 5316 'PHUIBadgeExample' => 'PhabricatorUIExample', 5315 5317 'PHUIBadgeMiniView' => 'AphrontTagView', ··· 7182 7184 'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField', 7183 7185 'PhabricatorStandardCustomFieldTokenizer' => 'PhabricatorStandardCustomFieldPHIDs', 7184 7186 'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldTokenizer', 7185 - 'PhabricatorStandardPageView' => 'PhabricatorBarePageView', 7187 + 'PhabricatorStandardPageView' => array( 7188 + 'PhabricatorBarePageView', 7189 + 'AphrontResponseProducerInterface', 7190 + ), 7186 7191 'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource', 7187 7192 'PhabricatorStatusController' => 'PhabricatorController', 7188 7193 'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
+106 -92
src/applications/base/controller/PhabricatorController.php
··· 3 3 abstract class PhabricatorController extends AphrontController { 4 4 5 5 private $handles; 6 - private $extraQuicksandConfig = array(); 7 6 8 7 public function shouldRequireLogin() { 9 8 return true; ··· 60 59 61 60 public function isGlobalDragAndDropUploadEnabled() { 62 61 return false; 63 - } 64 - 65 - public function addExtraQuicksandConfig($config) { 66 - $this->extraQuicksandConfig += $config; 67 - return $this; 68 - } 69 - 70 - private function getExtraQuicksandConfig() { 71 - return $this->extraQuicksandConfig; 72 62 } 73 63 74 64 public function willBeginExecution() { ··· 285 275 } 286 276 } 287 277 288 - public function buildStandardPageView() { 289 - $view = new PhabricatorStandardPageView(); 290 - $view->setRequest($this->getRequest()); 291 - $view->setController($this); 292 - return $view; 293 - } 294 - 295 - public function buildStandardPageResponse($view, array $data) { 296 - $page = $this->buildStandardPageView(); 297 - $page->appendChild($view); 298 - return $this->buildPageResponse($page); 299 - } 300 - 301 - private function buildPageResponse($page) { 302 - if ($this->getRequest()->isQuicksand()) { 303 - $response = id(new AphrontAjaxResponse()) 304 - ->setContent($page->renderForQuicksand( 305 - $this->getExtraQuicksandConfig())); 306 - } else { 307 - $response = id(new AphrontWebpageResponse()) 308 - ->setContent($page->render()); 309 - } 310 - 311 - return $response; 312 - } 313 - 314 278 public function getApplicationURI($path = '') { 315 279 if (!$this->getCurrentApplication()) { 316 280 throw new Exception(pht('No application!')); ··· 318 282 return $this->getCurrentApplication()->getApplicationURI($path); 319 283 } 320 284 321 - public function buildApplicationPage($view, array $options) { 322 - $page = $this->buildStandardPageView(); 323 - 324 - $title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ? 325 - 'Phabricator' : 326 - pht('Bacon Ice Cream for Breakfast'); 327 - 328 - $application = $this->getCurrentApplication(); 329 - $page->setTitle(idx($options, 'title', $title)); 330 - if ($application) { 331 - $page->setApplicationName($application->getName()); 332 - if ($application->getTitleGlyph()) { 333 - $page->setGlyph($application->getTitleGlyph()); 334 - } 335 - } 336 - 337 - if (idx($options, 'class')) { 338 - $page->addClass($options['class']); 339 - } 340 - 341 - if (!($view instanceof AphrontSideNavFilterView)) { 342 - $nav = new AphrontSideNavFilterView(); 343 - $nav->appendChild($view); 344 - $view = $nav; 345 - } 346 - 347 - $user = $this->getRequest()->getUser(); 348 - $view->setUser($user); 349 - 350 - $page->appendChild($view); 351 - 352 - $object_phids = idx($options, 'pageObjects', array()); 353 - if ($object_phids) { 354 - $page->appendPageObjects($object_phids); 355 - foreach ($object_phids as $object_phid) { 356 - PhabricatorFeedStoryNotification::updateObjectNotificationViews( 357 - $user, 358 - $object_phid); 359 - } 360 - } 361 - 362 - if (idx($options, 'device', true)) { 363 - $page->setDeviceReady(true); 364 - } 365 - 366 - $page->setShowFooter(idx($options, 'showFooter', true)); 367 - $page->setShowChrome(idx($options, 'chrome', true)); 368 - 369 - $application_menu = $this->buildApplicationMenu(); 370 - if ($application_menu) { 371 - $page->setApplicationMenu($application_menu); 372 - } 373 - 374 - return $this->buildPageResponse($page); 375 - } 376 - 377 285 public function willSendResponse(AphrontResponse $response) { 378 286 $request = $this->getRequest(); 379 287 ··· 536 444 ->setSubmitURI($submit_uri); 537 445 } 538 446 447 + public function newPage() { 448 + $page = id(new PhabricatorStandardPageView()) 449 + ->setRequest($this->getRequest()) 450 + ->setController($this); 451 + 452 + $application = $this->getCurrentApplication(); 453 + if ($application) { 454 + $page->setApplicationName($application->getName()); 455 + if ($application->getTitleGlyph()) { 456 + $page->setGlyph($application->getTitleGlyph()); 457 + } 458 + } 459 + 460 + $viewer = $this->getRequest()->getUser(); 461 + if ($viewer) { 462 + $page->setUser($viewer); 463 + } 464 + 465 + // TODO: Remove after removing callsites to addExtraQuicksandConfig(). 466 + $page->addQuicksandConfig($this->extraQuicksandConfig); 467 + 468 + return $page; 469 + } 470 + 471 + public function newApplicationMenu() { 472 + return id(new PHUIApplicationMenuView()) 473 + ->setViewer($this->getRequest()->getUser()); 474 + } 475 + 539 476 protected function buildTransactionTimeline( 540 477 PhabricatorApplicationTransactionInterface $object, 541 478 PhabricatorApplicationTransactionQuery $query, ··· 581 518 $object->willRenderTimeline($timeline, $this->getRequest()); 582 519 583 520 return $timeline; 521 + } 522 + 523 + 524 + /* -( Deprecated )--------------------------------------------------------- */ 525 + 526 + 527 + /** 528 + * DEPRECATED. 529 + */ 530 + private $extraQuicksandConfig = array(); 531 + 532 + 533 + /** 534 + * DEPRECATED. Use @{method:newPage} and call addQuicksandConfig(). 535 + */ 536 + public function addExtraQuicksandConfig($config) { 537 + // TODO: When this method is removed, 538 + $this->extraQuicksandConfig += $config; 539 + return $this; 540 + } 541 + 542 + 543 + /** 544 + * DEPRECATED. Use @{method:newPage}. 545 + */ 546 + public function buildStandardPageView() { 547 + return $this->newPage(); 548 + } 549 + 550 + 551 + /** 552 + * DEPRECATED. Use @{method:newPage}. 553 + */ 554 + public function buildStandardPageResponse($view, array $data) { 555 + $page = $this->buildStandardPageView(); 556 + $page->appendChild($view); 557 + return $page->produceAphrontResponse(); 558 + } 559 + 560 + 561 + /** 562 + * DEPRECATED. Use @{method:newPage}. 563 + */ 564 + public function buildApplicationPage($view, array $options) { 565 + $page = $this->newPage(); 566 + 567 + $title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ? 568 + 'Phabricator' : 569 + pht('Bacon Ice Cream for Breakfast'); 570 + 571 + $page->setTitle(idx($options, 'title', $title)); 572 + 573 + if (idx($options, 'class')) { 574 + $page->addClass($options['class']); 575 + } 576 + 577 + if (!($view instanceof AphrontSideNavFilterView)) { 578 + $nav = new AphrontSideNavFilterView(); 579 + $nav->appendChild($view); 580 + $view = $nav; 581 + } 582 + 583 + $page->appendChild($view); 584 + 585 + $object_phids = idx($options, 'pageObjects', array()); 586 + if ($object_phids) { 587 + $page->setPageObjectPHIDs($object_phids); 588 + } 589 + 590 + if (idx($options, 'device', true)) { 591 + $page->setDeviceReady(true); 592 + } 593 + 594 + $page->setShowFooter(idx($options, 'showFooter', true)); 595 + $page->setShowChrome(idx($options, 'chrome', true)); 596 + 597 + return $page->produceAphrontResponse(); 584 598 } 585 599 586 600 }
+2 -32
src/applications/paste/controller/PhabricatorPasteController.php
··· 2 2 3 3 abstract class PhabricatorPasteController extends PhabricatorController { 4 4 5 - public function buildSideNavView($for_app = false) { 6 - $user = $this->getRequest()->getUser(); 7 - 8 - $nav = new AphrontSideNavFilterView(); 9 - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 10 - 11 - if ($for_app) { 12 - $nav->addFilter('create', pht('Create Paste')); 13 - } 14 - 15 - id(new PhabricatorPasteSearchEngine()) 16 - ->setViewer($user) 17 - ->addNavigationItems($nav->getMenu()); 18 - 19 - $nav->selectFilter(null); 20 - 21 - return $nav; 22 - } 23 - 24 5 public function buildApplicationMenu() { 25 - return $this->buildSideNavView(true)->getMenu(); 26 - } 27 - 28 - protected function buildApplicationCrumbs() { 29 - $crumbs = parent::buildApplicationCrumbs(); 30 - 31 - $crumbs->addAction( 32 - id(new PHUIListItemView()) 33 - ->setName(pht('Create Paste')) 34 - ->setHref($this->getApplicationURI('create/')) 35 - ->setIcon('fa-plus-square')); 36 - 37 - return $crumbs; 6 + return $this->newApplicationMenu() 7 + ->setSearchEngine(new PhabricatorPasteSearchEngine()); 38 8 } 39 9 40 10 public function buildSourceCodeView(
+1 -1
src/applications/paste/controller/PhabricatorPasteEditController.php
··· 231 231 $form_box->setValidationException($validation_exception); 232 232 } 233 233 234 - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); 234 + $crumbs = $this->buildApplicationCrumbs(); 235 235 if (!$is_create) { 236 236 $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); 237 237 }
+13 -7
src/applications/paste/controller/PhabricatorPasteListController.php
··· 7 7 } 8 8 9 9 public function handleRequest(AphrontRequest $request) { 10 - $querykey = $request->getURIData('queryKey'); 10 + return id(new PhabricatorPasteSearchEngine()) 11 + ->setController($this) 12 + ->buildResponse(); 13 + } 11 14 12 - $controller = id(new PhabricatorApplicationSearchController()) 13 - ->setQueryKey($querykey) 14 - ->setSearchEngine(new PhabricatorPasteSearchEngine()) 15 - ->setNavigation($this->buildSideNavView()); 15 + protected function buildApplicationCrumbs() { 16 + $crumbs = parent::buildApplicationCrumbs(); 16 17 17 - return $this->delegateToController($controller); 18 - } 18 + $crumbs->addAction( 19 + id(new PHUIListItemView()) 20 + ->setName(pht('Create Paste')) 21 + ->setHref($this->getApplicationURI('create/')) 22 + ->setIcon('fa-plus-square')); 19 23 24 + return $crumbs; 25 + } 20 26 21 27 }
+15 -13
src/applications/paste/controller/PhabricatorPasteViewController.php
··· 66 66 ), 67 67 $source_code); 68 68 69 - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) 69 + $crumbs = $this->buildApplicationCrumbs() 70 70 ->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); 71 71 72 72 $timeline = $this->buildTransactionTimeline( ··· 89 89 ->setAction($this->getApplicationURI('/comment/'.$paste->getID().'/')) 90 90 ->setSubmitButtonName(pht('Add Comment')); 91 91 92 - return $this->buildApplicationPage( 93 - array( 94 - $crumbs, 95 - $object_box, 96 - $source_code, 97 - $timeline, 98 - $add_comment_form, 99 - ), 100 - array( 101 - 'title' => $paste->getFullName(), 102 - 'pageObjects' => array($paste->getPHID()), 103 - )); 92 + return $this->newPage() 93 + ->setTitle($paste->getFullName()) 94 + ->setCrumbs($crumbs) 95 + ->setPageObjectPHIDs( 96 + array( 97 + $paste->getPHID(), 98 + )) 99 + ->appendChild( 100 + array( 101 + $object_box, 102 + $source_code, 103 + $timeline, 104 + $add_comment_form, 105 + )); 104 106 } 105 107 106 108 private function buildHeaderView(PhabricatorPaste $paste) {
+44 -24
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 58 58 throw new PhutilInvalidStateException('setEngine'); 59 59 } 60 60 61 - $nav = $this->getNavigation(); 62 - if (!$nav) { 63 - throw new PhutilInvalidStateException('setNavigation'); 64 - } 65 - 66 61 $engine->setViewer($this->getRequest()->getUser()); 67 62 68 63 $parent = $this->getDelegatingController(); ··· 85 80 $user = $request->getUser(); 86 81 $engine = $this->getSearchEngine(); 87 82 $nav = $this->getNavigation(); 83 + if (!$nav) { 84 + $nav = $this->buildNavigation(); 85 + } 88 86 89 87 if ($request->isFormPost()) { 90 88 $saved_query = $engine->buildSavedQueryFromRequest($request); ··· 174 172 // we sort out T5307. 175 173 176 174 $form->appendChild($submit); 175 + $body = array(); 177 176 178 177 if ($this->getPreface()) { 179 - $nav->appendChild($this->getPreface()); 178 + $body[] = $this->getPreface(); 180 179 } 181 180 182 181 if ($named_query) { ··· 202 201 $box->setForm($form); 203 202 } 204 203 205 - $nav->appendChild($box); 204 + $body[] = $box; 206 205 207 206 if ($run_query) { 208 207 $box->setAnchor( ··· 257 256 ->addMargin(PHUI::MARGIN_LARGE) 258 257 ->setBorder(true) 259 258 ->appendChild($pager); 260 - $nav->appendChild($pager_box); 259 + $body[] = $pager_box; 261 260 } 262 261 263 262 } catch (PhabricatorTypeaheadInvalidTokenException $ex) { ··· 275 274 ->buildApplicationCrumbs() 276 275 ->addTextCrumb($title); 277 276 278 - $nav->setCrumbs($crumbs); 279 - 280 - return $this->buildApplicationPage( 281 - $nav, 282 - array( 283 - 'title' => pht('Query: %s', $title), 284 - )); 277 + return $this->newPage() 278 + ->setApplicationMenu($this->buildApplicationMenu()) 279 + ->setTitle(pht('Query: %s', $title)) 280 + ->setCrumbs($crumbs) 281 + ->setNavigation($nav) 282 + ->appendChild($body); 285 283 } 286 284 287 285 private function processEditRequest() { ··· 289 287 $request = $this->getRequest(); 290 288 $user = $request->getUser(); 291 289 $engine = $this->getSearchEngine(); 290 + 292 291 $nav = $this->getNavigation(); 292 + if (!$nav) { 293 + $nav = $this->buildNavigation(); 294 + } 293 295 294 296 $named_queries = $engine->loadAllNamedQueries(); 295 297 ··· 357 359 ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI()); 358 360 359 361 $nav->selectFilter('query/edit'); 360 - $nav->setCrumbs($crumbs); 361 362 362 363 $box = id(new PHUIObjectBoxView()) 363 364 ->setHeaderText(pht('Saved Queries')) 364 365 ->setObjectList($list); 365 366 366 - $nav->appendChild($box); 367 - 368 - return $parent->buildApplicationPage( 369 - $nav, 370 - array( 371 - 'title' => pht('Saved Queries'), 372 - )); 367 + return $this->newPage() 368 + ->setApplicationMenu($this->buildApplicationMenu()) 369 + ->setTitle(pht('Saved Queries')) 370 + ->setCrumbs($crumbs) 371 + ->setNavigation($nav) 372 + ->appendChild($box); 373 373 } 374 374 375 375 public function buildApplicationMenu() { 376 - return $this->getDelegatingController()->buildApplicationMenu(); 376 + $menu = $this->getDelegatingController() 377 + ->buildApplicationMenu(); 378 + 379 + if ($menu instanceof PHUIApplicationMenuView) { 380 + $menu->setSearchEngine($this->getSearchEngine()); 381 + } 382 + 383 + return $menu; 384 + } 385 + 386 + private function buildNavigation() { 387 + $viewer = $this->getViewer(); 388 + $engine = $this->getSearchEngine(); 389 + 390 + $nav = id(new AphrontSideNavFilterView()) 391 + ->setUser($viewer) 392 + ->setBaseURI(new PhutilURI($this->getApplicationURI())); 393 + 394 + $engine->addNavigationItems($nav->getMenu()); 395 + 396 + return $nav; 377 397 } 378 398 379 399 }
+44 -19
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 22 22 private $customFields = false; 23 23 private $request; 24 24 private $context; 25 + private $controller; 26 + private $namedQueries; 25 27 26 28 const CONTEXT_LIST = 'list'; 27 29 const CONTEXT_PANEL = 'panel'; 30 + 31 + public function setController(PhabricatorController $controller) { 32 + $this->controller = $controller; 33 + return $this; 34 + } 35 + 36 + public function getController() { 37 + return $this->controller; 38 + } 39 + 40 + public function buildResponse() { 41 + $controller = $this->getController(); 42 + $request = $controller->getRequest(); 43 + 44 + $search = id(new PhabricatorApplicationSearchController()) 45 + ->setQueryKey($request->getURIData('queryKey')) 46 + ->setSearchEngine($this); 47 + 48 + return $controller->delegateToController($search); 49 + } 28 50 29 51 public function newResultObject() { 30 52 // We may be able to get this automatically if newQuery() is implemented. ··· 459 481 460 482 public function loadAllNamedQueries() { 461 483 $viewer = $this->requireViewer(); 484 + $builtin = $this->getBuiltinQueries($viewer); 462 485 463 - $named_queries = id(new PhabricatorNamedQueryQuery()) 464 - ->setViewer($viewer) 465 - ->withUserPHIDs(array($viewer->getPHID())) 466 - ->withEngineClassNames(array(get_class($this))) 467 - ->execute(); 468 - $named_queries = mpull($named_queries, null, 'getQueryKey'); 486 + if ($this->namedQueries === null) { 487 + $named_queries = id(new PhabricatorNamedQueryQuery()) 488 + ->setViewer($viewer) 489 + ->withUserPHIDs(array($viewer->getPHID())) 490 + ->withEngineClassNames(array(get_class($this))) 491 + ->execute(); 492 + $named_queries = mpull($named_queries, null, 'getQueryKey'); 469 493 470 - $builtin = $this->getBuiltinQueries($viewer); 471 - $builtin = mpull($builtin, null, 'getQueryKey'); 494 + $builtin = mpull($builtin, null, 'getQueryKey'); 472 495 473 - foreach ($named_queries as $key => $named_query) { 474 - if ($named_query->getIsBuiltin()) { 475 - if (isset($builtin[$key])) { 476 - $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); 477 - unset($builtin[$key]); 478 - } else { 479 - unset($named_queries[$key]); 496 + foreach ($named_queries as $key => $named_query) { 497 + if ($named_query->getIsBuiltin()) { 498 + if (isset($builtin[$key])) { 499 + $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); 500 + unset($builtin[$key]); 501 + } else { 502 + unset($named_queries[$key]); 503 + } 480 504 } 505 + 506 + unset($builtin[$key]); 481 507 } 482 508 483 - unset($builtin[$key]); 509 + $named_queries = msort($named_queries, 'getSortKey'); 510 + $this->namedQueries = $named_queries; 484 511 } 485 512 486 - $named_queries = msort($named_queries, 'getSortKey'); 487 - 488 - return $named_queries + $builtin; 513 + return $this->namedQueries + $builtin; 489 514 } 490 515 491 516 public function loadEnabledNamedQueries() {
+89
src/view/layout/PHUIApplicationMenuView.php
··· 1 + <?php 2 + 3 + final class PHUIApplicationMenuView extends Phobject { 4 + 5 + private $viewer; 6 + private $crumbs; 7 + private $searchEngine; 8 + 9 + private $items = array(); 10 + 11 + public function setViewer(PhabricatorUser $viewer) { 12 + $this->viewer = $viewer; 13 + return $this; 14 + } 15 + 16 + public function getViewer() { 17 + return $this->viewer; 18 + } 19 + 20 + public function addLabel($name) { 21 + $item = id(new PHUIListItemView()) 22 + ->setName($name); 23 + 24 + return $this->addItem($item); 25 + } 26 + 27 + public function addLink($name, $href) { 28 + $item = id(new PHUIListItemView()) 29 + ->setName($name) 30 + ->setHref($href); 31 + 32 + return $this->addItem($item); 33 + } 34 + 35 + public function addItem(PHUIListItemView $item) { 36 + $this->items[] = $item; 37 + return $this; 38 + } 39 + 40 + public function setSearchEngine(PhabricatorApplicationSearchEngine $engine) { 41 + $this->searchEngine = $engine; 42 + return $this; 43 + } 44 + 45 + public function getSearchEngine() { 46 + return $this->searchEngine; 47 + } 48 + 49 + public function setCrumbs(PHUICrumbsView $crumbs) { 50 + $this->crumbs = $crumbs; 51 + return $this; 52 + } 53 + 54 + public function getCrumbs() { 55 + return $this->crumbs; 56 + } 57 + 58 + public function buildListView() { 59 + $viewer = $this->getViewer(); 60 + 61 + $view = id(new PHUIListView()) 62 + ->setUser($viewer); 63 + 64 + $crumbs = $this->getCrumbs(); 65 + if ($crumbs) { 66 + $actions = $crumbs->getActions(); 67 + if ($actions) { 68 + $view->newLabel(pht('Create')); 69 + foreach ($crumbs->getActions() as $action) { 70 + $view->addMenuItem($action); 71 + } 72 + } 73 + } 74 + 75 + $engine = $this->getSearchEngine(); 76 + if ($engine) { 77 + $engine 78 + ->setViewer($viewer) 79 + ->addNavigationItems($view); 80 + } 81 + 82 + foreach ($this->items as $item) { 83 + $view->addMenuItem($item); 84 + } 85 + 86 + return $view; 87 + } 88 + 89 + }
+104 -10
src/view/page/PhabricatorStandardPageView.php
··· 4 4 * This is a standard Phabricator page with menus, Javelin, DarkConsole, and 5 5 * basic styles. 6 6 */ 7 - final class PhabricatorStandardPageView extends PhabricatorBarePageView { 7 + final class PhabricatorStandardPageView extends PhabricatorBarePageView 8 + implements AphrontResponseProducerInterface { 8 9 9 10 private $baseURI; 10 11 private $applicationName; ··· 17 18 private $applicationMenu; 18 19 private $showFooter = true; 19 20 private $showDurableColumn = true; 21 + private $quicksandConfig = array(); 22 + private $crumbs; 23 + private $navigation; 20 24 21 25 public function setShowFooter($show_footer) { 22 26 $this->showFooter = $show_footer; ··· 27 31 return $this->showFooter; 28 32 } 29 33 30 - public function setApplicationMenu(PHUIListView $application_menu) { 34 + public function setApplicationMenu($application_menu) { 35 + // NOTE: For now, this can either be a PHUIListView or a 36 + // PHUIApplicationMenuView. 37 + 31 38 $this->applicationMenu = $application_menu; 32 39 return $this; 33 40 } ··· 73 80 return $this; 74 81 } 75 82 76 - public function appendPageObjects(array $objs) { 77 - foreach ($objs as $obj) { 78 - $this->pageObjects[] = $obj; 79 - } 83 + public function setPageObjectPHIDs(array $phids) { 84 + $this->pageObjects = $phids; 85 + return $this; 80 86 } 81 87 82 88 public function setShowDurableColumn($show) { ··· 130 136 return (bool)$this->getUserPreference($column_key, 0); 131 137 } 132 138 139 + public function addQuicksandConfig(array $config) { 140 + $this->quicksandConfig = $config + $this->quicksandConfig; 141 + return $this; 142 + } 143 + 144 + public function getQuicksandConfig() { 145 + return $this->quicksandConfig; 146 + } 147 + 148 + public function setCrumbs(PHUICrumbsView $crumbs) { 149 + $this->crumbs = $crumbs; 150 + return $this; 151 + } 152 + 153 + public function getCrumbs() { 154 + return $this->crumbs; 155 + } 156 + 157 + public function setNavigation(AphrontSideNavFilterView $navigation) { 158 + $this->navigation = $navigation; 159 + return $this; 160 + } 161 + 162 + public function getNavigation() { 163 + return $this->navigation; 164 + } 133 165 134 166 public function getTitle() { 135 167 $glyph_key = PhabricatorUserPreferences::PREFERENCE_TITLES; ··· 271 303 $menu->setController($this->getController()); 272 304 } 273 305 274 - if ($this->getApplicationMenu()) { 275 - $menu->setApplicationMenu($this->getApplicationMenu()); 306 + $application_menu = $this->getApplicationMenu(); 307 + if ($application_menu) { 308 + if ($application_menu instanceof PHUIApplicationMenuView) { 309 + $crumbs = $this->getCrumbs(); 310 + if ($crumbs) { 311 + $application_menu->setCrumbs($crumbs); 312 + } 313 + 314 + $application_menu = $application_menu->buildListView(); 315 + } 316 + 317 + $menu->setApplicationMenu($application_menu); 276 318 } 277 319 278 320 $this->menuContent = $menu->render(); ··· 433 475 private function renderPageBodyContent() { 434 476 $console = $this->getConsole(); 435 477 478 + $body = parent::getBody(); 479 + 480 + $nav = $this->getNavigation(); 481 + if ($nav) { 482 + $crumbs = $this->getCrumbs(); 483 + if ($crumbs) { 484 + $nav->setCrumbs($crumbs); 485 + } 486 + $nav->appendChild($body); 487 + $body = phutil_implode_html('', array($nav->render())); 488 + } else { 489 + $crumbs = $this->getCrumbs(); 490 + if ($crumbs) { 491 + $body = phutil_implode_html('', array($crumbs, $body)); 492 + } 493 + } 494 + 436 495 return array( 437 496 ($console ? hsprintf('<darkconsole />') : null), 438 - parent::getBody(), 497 + $body, 439 498 $this->renderFooter(), 440 499 ); 441 500 } ··· 619 678 $foot); 620 679 } 621 680 622 - public function renderForQuicksand(array $extra_config) { 681 + public function renderForQuicksand() { 623 682 parent::willRenderPage(); 624 683 $response = $this->renderPageBodyContent(); 625 684 $response = $this->willSendResponse($response); 685 + 686 + $extra_config = $this->getQuicksandConfig(); 626 687 627 688 return array( 628 689 'content' => hsprintf('%s', $response), ··· 730 791 } 731 792 732 793 return $user->loadPreferences()->getPreference($key, $default); 794 + } 795 + 796 + public function produceAphrontResponse() { 797 + $controller = $this->getController(); 798 + 799 + if (!$this->getApplicationMenu()) { 800 + $application_menu = $controller->buildApplicationMenu(); 801 + if ($application_menu) { 802 + $this->setApplicationMenu($application_menu); 803 + } 804 + } 805 + 806 + $viewer = $this->getUser(); 807 + if ($viewer && $viewer->getPHID()) { 808 + $object_phids = $this->pageObjects; 809 + foreach ($object_phids as $object_phid) { 810 + PhabricatorFeedStoryNotification::updateObjectNotificationViews( 811 + $viewer, 812 + $object_phid); 813 + } 814 + } 815 + 816 + if ($this->getRequest()->isQuicksand()) { 817 + $content = $this->renderForQuicksand(); 818 + $response = id(new AphrontAjaxResponse()) 819 + ->setContent($content); 820 + } else { 821 + $content = $this->render(); 822 + $response = id(new AphrontWebpageResponse()) 823 + ->setContent($content); 824 + } 825 + 826 + return $response; 733 827 } 734 828 735 829 }
+4
src/view/phui/PHUICrumbsView.php
··· 41 41 return $this; 42 42 } 43 43 44 + public function getActions() { 45 + return $this->actions; 46 + } 47 + 44 48 public function render() { 45 49 require_celerity_resource('phui-crumbs-view-css'); 46 50