1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-25 22:18:19 +01:00

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
This commit is contained in:
epriestley 2019-03-29 11:00:23 -07:00
parent 90df4b2bd1
commit d0d49d1efd
8 changed files with 269 additions and 23 deletions

View file

@ -2953,13 +2953,16 @@ phutil_register_library_map(array(
'PhabricatorDashboardPortalEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php', 'PhabricatorDashboardPortalEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php',
'PhabricatorDashboardPortalEditor' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditor.php', 'PhabricatorDashboardPortalEditor' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditor.php',
'PhabricatorDashboardPortalListController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php', 'PhabricatorDashboardPortalListController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php',
'PhabricatorDashboardPortalMenuItem' => 'applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php',
'PhabricatorDashboardPortalNameTransaction' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php', 'PhabricatorDashboardPortalNameTransaction' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php',
'PhabricatorDashboardPortalPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php', 'PhabricatorDashboardPortalPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php',
'PhabricatorDashboardPortalProfileMenuEngine' => 'applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php',
'PhabricatorDashboardPortalQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalQuery.php', 'PhabricatorDashboardPortalQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalQuery.php',
'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php', 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php',
'PhabricatorDashboardPortalSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php', 'PhabricatorDashboardPortalSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php',
'PhabricatorDashboardPortalStatus' => 'applications/dashboard/constants/PhabricatorDashboardPortalStatus.php', 'PhabricatorDashboardPortalStatus' => 'applications/dashboard/constants/PhabricatorDashboardPortalStatus.php',
'PhabricatorDashboardPortalTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php', 'PhabricatorDashboardPortalTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php',
'PhabricatorDashboardPortalTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php',
'PhabricatorDashboardPortalTransactionType' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php', 'PhabricatorDashboardPortalTransactionType' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php',
'PhabricatorDashboardPortalViewController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php', 'PhabricatorDashboardPortalViewController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php',
'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php', 'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php',
@ -8924,13 +8927,16 @@ phutil_register_library_map(array(
'PhabricatorDashboardPortalEditEngine' => 'PhabricatorEditEngine', 'PhabricatorDashboardPortalEditEngine' => 'PhabricatorEditEngine',
'PhabricatorDashboardPortalEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorDashboardPortalEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardPortalListController' => 'PhabricatorDashboardPortalController', 'PhabricatorDashboardPortalListController' => 'PhabricatorDashboardPortalController',
'PhabricatorDashboardPortalMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDashboardPortalNameTransaction' => 'PhabricatorDashboardPortalTransactionType', 'PhabricatorDashboardPortalNameTransaction' => 'PhabricatorDashboardPortalTransactionType',
'PhabricatorDashboardPortalPHIDType' => 'PhabricatorPHIDType', 'PhabricatorDashboardPortalPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardPortalProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorDashboardPortalQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDashboardPortalQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorDashboardPortalSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDashboardPortalSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardPortalStatus' => 'Phobject', 'PhabricatorDashboardPortalStatus' => 'Phobject',
'PhabricatorDashboardPortalTransaction' => 'PhabricatorModularTransaction', 'PhabricatorDashboardPortalTransaction' => 'PhabricatorModularTransaction',
'PhabricatorDashboardPortalTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardPortalTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorDashboardPortalTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorDashboardPortalViewController' => 'PhabricatorDashboardPortalController', 'PhabricatorDashboardPortalViewController' => 'PhabricatorDashboardPortalController',
'PhabricatorDashboardProfileController' => 'PhabricatorController', 'PhabricatorDashboardProfileController' => 'PhabricatorController',

View file

@ -27,6 +27,9 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication {
} }
public function getRoutes() { public function getRoutes() {
$menu_rules = $this->getProfileMenuRouting(
'PhabricatorDashboardPortalViewController');
return array( return array(
'/W(?P<id>\d+)' => 'PhabricatorDashboardPanelViewController', '/W(?P<id>\d+)' => 'PhabricatorDashboardPanelViewController',
'/dashboard/' => array( '/dashboard/' => array(
@ -62,8 +65,10 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication {
'PhabricatorDashboardPortalListController', 'PhabricatorDashboardPortalListController',
$this->getEditRoutePattern('edit/') => $this->getEditRoutePattern('edit/') =>
'PhabricatorDashboardPortalEditController', 'PhabricatorDashboardPortalEditController',
'view/(?P<id>\d)/' => 'view/(?P<portalID>\d)/' => array(
'PhabricatorDashboardPortalViewController', '' => 'PhabricatorDashboardPortalViewController',
) + $menu_rules,
), ),
); );
} }

View file

@ -3,13 +3,24 @@
final class PhabricatorDashboardPortalViewController final class PhabricatorDashboardPortalViewController
extends PhabricatorDashboardPortalController { extends PhabricatorDashboardPortalController {
private $portal;
public function setPortal(PhabricatorDashboardPortal $portal) {
$this->portal = $portal;
return $this;
}
public function getPortal() {
return $this->portal;
}
public function shouldAllowPublic() { public function shouldAllowPublic() {
return true; return true;
} }
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$id = $request->getURIData('id'); $id = $request->getURIData('portalID');
$portal = id(new PhabricatorDashboardPortalQuery()) $portal = id(new PhabricatorDashboardPortalQuery())
->setViewer($viewer) ->setViewer($viewer)
@ -19,16 +30,30 @@ final class PhabricatorDashboardPortalViewController
return new Aphront404Response(); return new Aphront404Response();
} }
$content = $portal->getObjectName(); $this->setPortal($portal);
return $this->newPage() $engine = id(new PhabricatorDashboardPortalProfileMenuEngine())
->setTitle( ->setProfileObject($portal)
array( ->setController($this);
pht('Portal'),
$portal->getName(), return $engine->buildResponse();
)) }
->setPageObjectPHIDs(array($portal->getPHID()))
->appendChild($content); protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$portal = $this->getPortal();
if ($portal) {
$crumbs->addTextCrumb($portal->getName(), $portal->getURI());
}
return $crumbs;
}
public function newTimelineView() {
return $this->buildTransactionTimeline(
$this->getPortal(),
new PhabricatorDashboardPortalTransactionQuery());
} }
} }

View file

@ -58,7 +58,11 @@ final class PhabricatorDashboardPortalEditEngine
} }
protected function getObjectViewURI($object) { protected function getObjectViewURI($object) {
return $object->getURI(); if ($this->getIsCreate()) {
return $object->getURI();
} else {
return '/portal/view/'.$object->getID().'/view/manage/';
}
} }
protected function getEditorURI() { protected function getEditorURI() {

View file

@ -0,0 +1,38 @@
<?php
final class PhabricatorDashboardPortalProfileMenuEngine
extends PhabricatorProfileMenuEngine {
protected function isMenuEngineConfigurable() {
return true;
}
protected function isMenuEnginePersonalizable() {
return false;
}
public function getItemURI($path) {
$portal = $this->getProfileObject();
return $portal->getURI().$path;
}
protected function getBuiltinProfileItems($object) {
$items = array();
$items[] = $this->newManageItem();
$items[] = $this->newItem()
->setMenuItemKey(PhabricatorDashboardPortalMenuItem::MENUITEMKEY)
->setBuiltinKey('manage');
return $items;
}
protected function newNoMenuItemsView() {
return $this->newEmptyView(
pht('New Portal'),
pht('Use "Edit Menu" to add menu items to this portal.'));
}
}

View file

@ -0,0 +1,117 @@
<?php
final class PhabricatorDashboardPortalMenuItem
extends PhabricatorProfileMenuItem {
const MENUITEMKEY = 'portal';
public function getMenuItemTypeIcon() {
return 'fa-compass';
}
public function getDefaultName() {
return pht('Manage Portal');
}
public function getMenuItemTypeName() {
return pht('Manage Portal');
}
public function canHideMenuItem(
PhabricatorProfileMenuItemConfiguration $config) {
return false;
}
public function canMakeDefault(
PhabricatorProfileMenuItemConfiguration $config) {
return false;
}
public function getDisplayName(
PhabricatorProfileMenuItemConfiguration $config) {
$name = $config->getMenuItemProperty('name');
if (strlen($name)) {
return $name;
}
return $this->getDefaultName();
}
public function buildEditEngineFields(
PhabricatorProfileMenuItemConfiguration $config) {
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setPlaceholder($this->getDefaultName())
->setValue($config->getMenuItemProperty('name')),
);
}
protected function newNavigationMenuItems(
PhabricatorProfileMenuItemConfiguration $config) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
return array();
}
$href = $this->getItemViewURI($config);
$name = $this->getDisplayName($config);
$icon = 'fa-pencil';
$item = $this->newItem()
->setHref($href)
->setName($name)
->setIcon($icon);
return array(
$item,
);
}
public function newPageContent(
PhabricatorProfileMenuItemConfiguration $config) {
$viewer = $this->getViewer();
$engine = $this->getEngine();
$portal = $engine->getProfileObject();
$controller = $engine->getController();
$header = id(new PHUIHeaderView())
->setHeader(pht('Manage Portal'));
$edit_uri = urisprintf(
'/portal/edit/%d/',
$portal->getID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$portal,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $controller->newCurtainView($portal)
->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Portal'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$timeline = $controller->newTimelineView()
->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$timeline,
));
return $view;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorDashboardPortalTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorDashboardPortalTransaction();
}
}

View file

@ -181,6 +181,10 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
switch ($item_action) { switch ($item_action) {
case 'view': case 'view':
// If we were not able to select an item, we're still going to render
// a page state. For example, this happens when you create a new
// portal for the first time.
break;
case 'info': case 'info':
case 'hide': case 'hide':
case 'default': case 'default':
@ -231,24 +235,35 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
} }
$page_title = pht('Configure Menu'); $page_title = pht('Configure Menu');
} else { } else {
$page_title = $selected_item->getDisplayName(); if ($selected_item) {
$page_title = $selected_item->getDisplayName();
} else {
$page_title = pht('Empty');
}
} }
switch ($item_action) { switch ($item_action) {
case 'view': case 'view':
$navigation->selectFilter($selected_item->getDefaultMenuItemKey()); if ($selected_item) {
$navigation->selectFilter($selected_item->getDefaultMenuItemKey());
try { try {
$content = $this->buildItemViewContent($selected_item); $content = $this->buildItemViewContent($selected_item);
} catch (Exception $ex) { } catch (Exception $ex) {
$content = id(new PHUIInfoView()) $content = id(new PHUIInfoView())
->setTitle(pht('Unable to Render Dashboard')) ->setTitle(pht('Unable to Render Dashboard'))
->setErrors(array($ex->getMessage())); ->setErrors(array($ex->getMessage()));
}
$crumbs->addTextCrumb($selected_item->getDisplayName());
} else {
$content = $this->newNoMenuItemsView();
} }
$crumbs->addTextCrumb($selected_item->getDisplayName());
if (!$content) { if (!$content) {
return new Aphront404Response(); $content = $this->newEmptyView(
pht('Empty'),
pht('There is nothing here.'));
} }
break; break;
case 'configure': case 'configure':
@ -346,6 +361,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
if ($this->navigation) { if ($this->navigation) {
return $this->navigation; return $this->navigation;
} }
$nav = id(new AphrontSideNavFilterView()) $nav = id(new AphrontSideNavFilterView())
->setIsProfileMenu(true) ->setIsProfileMenu(true)
->setBaseURI(new PhutilURI($this->getItemURI(''))); ->setBaseURI(new PhutilURI($this->getItemURI('')));
@ -365,6 +381,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
$first_item->willBuildNavigationItems($group); $first_item->willBuildNavigationItems($group);
} }
$has_items = false;
foreach ($menu_items as $menu_item) { foreach ($menu_items as $menu_item) {
if ($menu_item->isDisabled()) { if ($menu_item->isDisabled()) {
continue; continue;
@ -389,9 +406,18 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
foreach ($items as $item) { foreach ($items as $item) {
$nav->addMenuItem($item); $nav->addMenuItem($item);
$has_items = true;
} }
} }
if (!$has_items) {
// If the navigation menu has no items, add an empty label item to
// force it to render something.
$empty_item = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL);
$nav->addMenuItem($empty_item);
}
$nav->selectFilter(null); $nav->selectFilter(null);
$this->navigation = $nav; $this->navigation = $nav;
@ -1319,5 +1345,20 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
return $items; return $items;
} }
final protected function newEmptyView($title, $message) {
return id(new PHUIInfoView())
->setTitle($title)
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->setErrors(
array(
$message,
));
}
protected function newNoMenuItemsView() {
return $this->newEmptyView(
pht('No Menu Items'),
pht('There are no menu items.'));
}
} }