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

Allow Portals to be edited, and improve empty/blank states

Summary:
Depends on D20348. Ref T13275. Portals are mostly just a "ProfileMenuEngine" menu, and that code is already relatively modular/flexible, so set that up to start with.

The stuff it gets wrong right now is mostly around empty/no-permission states, since the original use cases (project menus) didn't have any of these states: it's not possible to have a project menu with no content.

Let the engine render an "empty" state (when there are no items that can render a content page) and try to make some of the empty behavior a little more user-friendly.

This mostly makes portals work, more or less.

Test Plan: {F6322284}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13275

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

+269 -23
+6
src/__phutil_library_map__.php
··· 2953 2953 'PhabricatorDashboardPortalEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php', 2954 2954 'PhabricatorDashboardPortalEditor' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditor.php', 2955 2955 'PhabricatorDashboardPortalListController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php', 2956 + 'PhabricatorDashboardPortalMenuItem' => 'applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php', 2956 2957 'PhabricatorDashboardPortalNameTransaction' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php', 2957 2958 'PhabricatorDashboardPortalPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php', 2959 + 'PhabricatorDashboardPortalProfileMenuEngine' => 'applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php', 2958 2960 'PhabricatorDashboardPortalQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalQuery.php', 2959 2961 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php', 2960 2962 'PhabricatorDashboardPortalSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php', 2961 2963 'PhabricatorDashboardPortalStatus' => 'applications/dashboard/constants/PhabricatorDashboardPortalStatus.php', 2962 2964 'PhabricatorDashboardPortalTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php', 2965 + 'PhabricatorDashboardPortalTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php', 2963 2966 'PhabricatorDashboardPortalTransactionType' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php', 2964 2967 'PhabricatorDashboardPortalViewController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php', 2965 2968 'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php', ··· 8924 8927 'PhabricatorDashboardPortalEditEngine' => 'PhabricatorEditEngine', 8925 8928 'PhabricatorDashboardPortalEditor' => 'PhabricatorApplicationTransactionEditor', 8926 8929 'PhabricatorDashboardPortalListController' => 'PhabricatorDashboardPortalController', 8930 + 'PhabricatorDashboardPortalMenuItem' => 'PhabricatorProfileMenuItem', 8927 8931 'PhabricatorDashboardPortalNameTransaction' => 'PhabricatorDashboardPortalTransactionType', 8928 8932 'PhabricatorDashboardPortalPHIDType' => 'PhabricatorPHIDType', 8933 + 'PhabricatorDashboardPortalProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 8929 8934 'PhabricatorDashboardPortalQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 8930 8935 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 8931 8936 'PhabricatorDashboardPortalSearchEngine' => 'PhabricatorApplicationSearchEngine', 8932 8937 'PhabricatorDashboardPortalStatus' => 'Phobject', 8933 8938 'PhabricatorDashboardPortalTransaction' => 'PhabricatorModularTransaction', 8939 + 'PhabricatorDashboardPortalTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 8934 8940 'PhabricatorDashboardPortalTransactionType' => 'PhabricatorModularTransactionType', 8935 8941 'PhabricatorDashboardPortalViewController' => 'PhabricatorDashboardPortalController', 8936 8942 'PhabricatorDashboardProfileController' => 'PhabricatorController',
+7 -2
src/applications/dashboard/application/PhabricatorDashboardApplication.php
··· 27 27 } 28 28 29 29 public function getRoutes() { 30 + $menu_rules = $this->getProfileMenuRouting( 31 + 'PhabricatorDashboardPortalViewController'); 32 + 30 33 return array( 31 34 '/W(?P<id>\d+)' => 'PhabricatorDashboardPanelViewController', 32 35 '/dashboard/' => array( ··· 62 65 'PhabricatorDashboardPortalListController', 63 66 $this->getEditRoutePattern('edit/') => 64 67 'PhabricatorDashboardPortalEditController', 65 - 'view/(?P<id>\d)/' => 66 - 'PhabricatorDashboardPortalViewController', 68 + 'view/(?P<portalID>\d)/' => array( 69 + '' => 'PhabricatorDashboardPortalViewController', 70 + ) + $menu_rules, 71 + 67 72 ), 68 73 ); 69 74 }
+35 -10
src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php
··· 3 3 final class PhabricatorDashboardPortalViewController 4 4 extends PhabricatorDashboardPortalController { 5 5 6 + private $portal; 7 + 8 + public function setPortal(PhabricatorDashboardPortal $portal) { 9 + $this->portal = $portal; 10 + return $this; 11 + } 12 + 13 + public function getPortal() { 14 + return $this->portal; 15 + } 16 + 6 17 public function shouldAllowPublic() { 7 18 return true; 8 19 } 9 20 10 21 public function handleRequest(AphrontRequest $request) { 11 22 $viewer = $this->getViewer(); 12 - $id = $request->getURIData('id'); 23 + $id = $request->getURIData('portalID'); 13 24 14 25 $portal = id(new PhabricatorDashboardPortalQuery()) 15 26 ->setViewer($viewer) ··· 19 30 return new Aphront404Response(); 20 31 } 21 32 22 - $content = $portal->getObjectName(); 33 + $this->setPortal($portal); 34 + 35 + $engine = id(new PhabricatorDashboardPortalProfileMenuEngine()) 36 + ->setProfileObject($portal) 37 + ->setController($this); 38 + 39 + return $engine->buildResponse(); 40 + } 41 + 42 + protected function buildApplicationCrumbs() { 43 + $crumbs = parent::buildApplicationCrumbs(); 23 44 24 - return $this->newPage() 25 - ->setTitle( 26 - array( 27 - pht('Portal'), 28 - $portal->getName(), 29 - )) 30 - ->setPageObjectPHIDs(array($portal->getPHID())) 31 - ->appendChild($content); 45 + $portal = $this->getPortal(); 46 + if ($portal) { 47 + $crumbs->addTextCrumb($portal->getName(), $portal->getURI()); 48 + } 49 + 50 + return $crumbs; 51 + } 52 + 53 + public function newTimelineView() { 54 + return $this->buildTransactionTimeline( 55 + $this->getPortal(), 56 + new PhabricatorDashboardPortalTransactionQuery()); 32 57 } 33 58 34 59 }
+5 -1
src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php
··· 58 58 } 59 59 60 60 protected function getObjectViewURI($object) { 61 - return $object->getURI(); 61 + if ($this->getIsCreate()) { 62 + return $object->getURI(); 63 + } else { 64 + return '/portal/view/'.$object->getID().'/view/manage/'; 65 + } 62 66 } 63 67 64 68 protected function getEditorURI() {
+38
src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardPortalProfileMenuEngine 4 + extends PhabricatorProfileMenuEngine { 5 + 6 + protected function isMenuEngineConfigurable() { 7 + return true; 8 + } 9 + 10 + protected function isMenuEnginePersonalizable() { 11 + return false; 12 + } 13 + 14 + public function getItemURI($path) { 15 + $portal = $this->getProfileObject(); 16 + 17 + return $portal->getURI().$path; 18 + } 19 + 20 + protected function getBuiltinProfileItems($object) { 21 + $items = array(); 22 + 23 + $items[] = $this->newManageItem(); 24 + 25 + $items[] = $this->newItem() 26 + ->setMenuItemKey(PhabricatorDashboardPortalMenuItem::MENUITEMKEY) 27 + ->setBuiltinKey('manage'); 28 + 29 + return $items; 30 + } 31 + 32 + protected function newNoMenuItemsView() { 33 + return $this->newEmptyView( 34 + pht('New Portal'), 35 + pht('Use "Edit Menu" to add menu items to this portal.')); 36 + } 37 + 38 + }
+117
src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardPortalMenuItem 4 + extends PhabricatorProfileMenuItem { 5 + 6 + const MENUITEMKEY = 'portal'; 7 + 8 + public function getMenuItemTypeIcon() { 9 + return 'fa-compass'; 10 + } 11 + 12 + public function getDefaultName() { 13 + return pht('Manage Portal'); 14 + } 15 + 16 + public function getMenuItemTypeName() { 17 + return pht('Manage Portal'); 18 + } 19 + 20 + public function canHideMenuItem( 21 + PhabricatorProfileMenuItemConfiguration $config) { 22 + return false; 23 + } 24 + 25 + public function canMakeDefault( 26 + PhabricatorProfileMenuItemConfiguration $config) { 27 + return false; 28 + } 29 + 30 + public function getDisplayName( 31 + PhabricatorProfileMenuItemConfiguration $config) { 32 + $name = $config->getMenuItemProperty('name'); 33 + 34 + if (strlen($name)) { 35 + return $name; 36 + } 37 + 38 + return $this->getDefaultName(); 39 + } 40 + 41 + public function buildEditEngineFields( 42 + PhabricatorProfileMenuItemConfiguration $config) { 43 + return array( 44 + id(new PhabricatorTextEditField()) 45 + ->setKey('name') 46 + ->setLabel(pht('Name')) 47 + ->setPlaceholder($this->getDefaultName()) 48 + ->setValue($config->getMenuItemProperty('name')), 49 + ); 50 + } 51 + 52 + protected function newNavigationMenuItems( 53 + PhabricatorProfileMenuItemConfiguration $config) { 54 + $viewer = $this->getViewer(); 55 + 56 + if (!$viewer->isLoggedIn()) { 57 + return array(); 58 + } 59 + 60 + $href = $this->getItemViewURI($config); 61 + $name = $this->getDisplayName($config); 62 + $icon = 'fa-pencil'; 63 + 64 + $item = $this->newItem() 65 + ->setHref($href) 66 + ->setName($name) 67 + ->setIcon($icon); 68 + 69 + return array( 70 + $item, 71 + ); 72 + } 73 + 74 + public function newPageContent( 75 + PhabricatorProfileMenuItemConfiguration $config) { 76 + $viewer = $this->getViewer(); 77 + $engine = $this->getEngine(); 78 + $portal = $engine->getProfileObject(); 79 + $controller = $engine->getController(); 80 + 81 + $header = id(new PHUIHeaderView()) 82 + ->setHeader(pht('Manage Portal')); 83 + 84 + $edit_uri = urisprintf( 85 + '/portal/edit/%d/', 86 + $portal->getID()); 87 + 88 + $can_edit = PhabricatorPolicyFilter::hasCapability( 89 + $viewer, 90 + $portal, 91 + PhabricatorPolicyCapability::CAN_EDIT); 92 + 93 + $curtain = $controller->newCurtainView($portal) 94 + ->addAction( 95 + id(new PhabricatorActionView()) 96 + ->setName(pht('Edit Portal')) 97 + ->setIcon('fa-pencil') 98 + ->setDisabled(!$can_edit) 99 + ->setWorkflow(!$can_edit) 100 + ->setHref($edit_uri)); 101 + 102 + $timeline = $controller->newTimelineView() 103 + ->setShouldTerminate(true); 104 + 105 + $view = id(new PHUITwoColumnView()) 106 + ->setHeader($header) 107 + ->setCurtain($curtain) 108 + ->setMainColumn( 109 + array( 110 + $timeline, 111 + )); 112 + 113 + return $view; 114 + } 115 + 116 + 117 + }
+10
src/applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardPortalTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + public function getTemplateApplicationTransaction() { 7 + return new PhabricatorDashboardPortalTransaction(); 8 + } 9 + 10 + }
+51 -10
src/applications/search/engine/PhabricatorProfileMenuEngine.php
··· 181 181 182 182 switch ($item_action) { 183 183 case 'view': 184 + // If we were not able to select an item, we're still going to render 185 + // a page state. For example, this happens when you create a new 186 + // portal for the first time. 187 + break; 184 188 case 'info': 185 189 case 'hide': 186 190 case 'default': ··· 231 235 } 232 236 $page_title = pht('Configure Menu'); 233 237 } else { 234 - $page_title = $selected_item->getDisplayName(); 238 + if ($selected_item) { 239 + $page_title = $selected_item->getDisplayName(); 240 + } else { 241 + $page_title = pht('Empty'); 242 + } 235 243 } 236 244 237 245 switch ($item_action) { 238 246 case 'view': 239 - $navigation->selectFilter($selected_item->getDefaultMenuItemKey()); 247 + if ($selected_item) { 248 + $navigation->selectFilter($selected_item->getDefaultMenuItemKey()); 240 249 241 - try { 242 - $content = $this->buildItemViewContent($selected_item); 243 - } catch (Exception $ex) { 244 - $content = id(new PHUIInfoView()) 245 - ->setTitle(pht('Unable to Render Dashboard')) 246 - ->setErrors(array($ex->getMessage())); 250 + try { 251 + $content = $this->buildItemViewContent($selected_item); 252 + } catch (Exception $ex) { 253 + $content = id(new PHUIInfoView()) 254 + ->setTitle(pht('Unable to Render Dashboard')) 255 + ->setErrors(array($ex->getMessage())); 256 + } 257 + 258 + $crumbs->addTextCrumb($selected_item->getDisplayName()); 259 + } else { 260 + $content = $this->newNoMenuItemsView(); 247 261 } 248 262 249 - $crumbs->addTextCrumb($selected_item->getDisplayName()); 250 263 if (!$content) { 251 - return new Aphront404Response(); 264 + $content = $this->newEmptyView( 265 + pht('Empty'), 266 + pht('There is nothing here.')); 252 267 } 253 268 break; 254 269 case 'configure': ··· 346 361 if ($this->navigation) { 347 362 return $this->navigation; 348 363 } 364 + 349 365 $nav = id(new AphrontSideNavFilterView()) 350 366 ->setIsProfileMenu(true) 351 367 ->setBaseURI(new PhutilURI($this->getItemURI(''))); ··· 365 381 $first_item->willBuildNavigationItems($group); 366 382 } 367 383 384 + $has_items = false; 368 385 foreach ($menu_items as $menu_item) { 369 386 if ($menu_item->isDisabled()) { 370 387 continue; ··· 389 406 390 407 foreach ($items as $item) { 391 408 $nav->addMenuItem($item); 409 + $has_items = true; 392 410 } 411 + } 412 + 413 + if (!$has_items) { 414 + // If the navigation menu has no items, add an empty label item to 415 + // force it to render something. 416 + $empty_item = id(new PHUIListItemView()) 417 + ->setType(PHUIListItemView::TYPE_LABEL); 418 + $nav->addMenuItem($empty_item); 393 419 } 394 420 395 421 $nav->selectFilter(null); ··· 1319 1345 return $items; 1320 1346 } 1321 1347 1348 + final protected function newEmptyView($title, $message) { 1349 + return id(new PHUIInfoView()) 1350 + ->setTitle($title) 1351 + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) 1352 + ->setErrors( 1353 + array( 1354 + $message, 1355 + )); 1356 + } 1357 + 1358 + protected function newNoMenuItemsView() { 1359 + return $this->newEmptyView( 1360 + pht('No Menu Items'), 1361 + pht('There are no menu items.')); 1362 + } 1322 1363 1323 1364 }