Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php
index 7b5d6d0720..f26f4603e1 100644
--- a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php
+++ b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php
@@ -1,85 +1,90 @@
<?php
final class PhabricatorFavoritesMainMenuBarExtension
extends PhabricatorMainMenuBarExtension {
const MAINMENUBARKEY = 'favorites';
public function isExtensionEnabledForViewer(PhabricatorUser $viewer) {
return PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorFavoritesApplication',
$viewer);
}
public function getExtensionOrder() {
return 1100;
}
public function buildMainMenus() {
$viewer = $this->getViewer();
$dropdown = $this->newDropdown($viewer);
if (!$dropdown) {
return null;
}
$favorites_menu = id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setIcon('fa-star')
->addClass('phabricator-core-user-menu')
->setNoCSS(true)
->setDropdown(true)
->setDropdownMenu($dropdown)
->setAuralLabel(pht('Favorites Menu'));
return array(
$favorites_menu,
);
}
private function newDropdown(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array('PhabricatorFavoritesApplication'))
->withInstalled(true)
->execute();
$favorites = head($applications);
if (!$favorites) {
return null;
}
$menu_engine = id(new PhabricatorFavoritesProfileMenuEngine())
->setViewer($viewer)
->setProfileObject($favorites)
->setCustomPHID($viewer->getPHID());
+ $controller = $this->getController();
+ if ($controller) {
+ $menu_engine->setController($controller);
+ }
+
$filter_view = $menu_engine->buildNavigation();
$menu_view = $filter_view->getMenu();
$item_views = $menu_view->getItems();
$view = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($item_views as $item) {
$action = id(new PhabricatorActionView())
->setName($item->getName())
->setHref($item->getHref())
->setType($item->getType());
$view->addAction($action);
}
if ($viewer->isLoggedIn()) {
$view->addAction(
id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Favorites'))
->setHref('/favorites/menu/configure/'));
}
return $view;
}
}
diff --git a/src/applications/home/controller/PhabricatorHomeController.php b/src/applications/home/controller/PhabricatorHomeController.php
index 7c46525ee0..9cd3b5b91d 100644
--- a/src/applications/home/controller/PhabricatorHomeController.php
+++ b/src/applications/home/controller/PhabricatorHomeController.php
@@ -1,43 +1,44 @@
<?php
abstract class PhabricatorHomeController extends PhabricatorController {
private $home;
private $profileMenu;
public function buildApplicationMenu() {
$menu = $this->newApplicationMenu();
$profile_menu = $this->getProfileMenu();
if ($profile_menu) {
$menu->setProfileMenu($profile_menu);
}
return $menu;
}
protected function getProfileMenu() {
if (!$this->profileMenu) {
$viewer = $this->getViewer();
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array('PhabricatorHomeApplication'))
->withInstalled(true)
->execute();
$home = head($applications);
if (!$home) {
return null;
}
$engine = id(new PhabricatorHomeProfileMenuEngine())
->setViewer($viewer)
+ ->setController($this)
->setProfileObject($home)
->setCustomPHID($viewer->getPHID());
$this->profileMenu = $engine->buildNavigation();
}
return $this->profileMenu;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php
index d0c6abf882..b889bc75da 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardController.php
@@ -1,14 +1,4 @@
<?php
abstract class PhabricatorProjectBoardController
- extends PhabricatorProjectController {
-
- protected function getProfileMenu() {
- $menu = parent::getProfileMenu();
-
- $menu->selectFilter(PhabricatorProject::ITEM_WORKBOARD);
- $menu->addClass('project-board-nav');
-
- return $menu;
- }
-}
+ extends PhabricatorProjectController {}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
index 775ff1b61a..bd14033d3f 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -1,1507 +1,1517 @@
<?php
final class PhabricatorProjectBoardViewController
extends PhabricatorProjectBoardController {
const BATCH_EDIT_ALL = 'all';
private $id;
private $slug;
private $queryKey;
private $sortKey;
private $showHidden;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$response = $this->loadProject();
if ($response) {
return $response;
}
$project = $this->getProject();
$this->readRequestState();
$board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
$search_engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer)
->setBaseURI($board_uri)
->setIsBoardView(true);
if ($request->isFormPost()
&& !$request->getBool('initialize')
&& !$request->getStr('move')
&& !$request->getStr('queryColumnID')) {
$saved = $search_engine->buildSavedQueryFromRequest($request);
$search_engine->saveQuery($saved);
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$search_engine->buildSearchForm($filter_form, $saved);
if ($search_engine->getErrors()) {
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setErrors($search_engine->getErrors())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
return id(new AphrontRedirectResponse())->setURI(
$this->getURIWithState(
$search_engine->getQueryResultsPageURI($saved->getQueryKey())));
}
$query_key = $this->getDefaultFilter($project);
$request_query = $request->getStr('filter');
if (strlen($request_query)) {
$query_key = $request_query;
}
$uri_query = $request->getURIData('queryKey');
if (strlen($uri_query)) {
$query_key = $uri_query;
}
$this->queryKey = $query_key;
$custom_query = null;
if ($search_engine->isBuiltinQuery($query_key)) {
$saved = $search_engine->buildSavedQueryFromBuiltin($query_key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
return new Aphront404Response();
}
$custom_query = $saved;
}
if ($request->getURIData('filter')) {
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$search_engine->buildSearchForm($filter_form, $saved);
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
$task_query = $search_engine->buildQueryFromSavedQuery($saved);
$select_phids = array($project->getPHID());
if ($project->getHasSubprojects() || $project->getHasMilestones()) {
$descendants = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
}
$tasks = $task_query
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->setViewer($viewer)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
$board_phid = $project->getPHID();
// Regardless of display order, pass tasks to the layout engine in ID order
// so layout is consistent.
$board_tasks = msort($tasks, 'getID');
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs(array_keys($board_tasks))
->setFetchAllBoards(true)
->executeLayout();
$columns = $layout_engine->getColumns($board_phid);
if (!$columns || !$project->getHasWorkboard()) {
$has_normal_columns = false;
foreach ($columns as $column) {
if (!$column->getProxyPHID()) {
$has_normal_columns = true;
break;
}
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$has_normal_columns) {
if (!$can_edit) {
$content = $this->buildNoAccessContent($project);
} else {
$content = $this->buildInitializeContent($project);
}
} else {
if (!$can_edit) {
$content = $this->buildDisabledContent($project);
} else {
$content = $this->buildEnableContent($project);
}
}
if ($content instanceof AphrontResponse) {
return $content;
}
- $nav = $this->getProfileMenu();
- $nav->selectFilter(PhabricatorProject::ITEM_WORKBOARD);
+ $nav = $this->newWorkboardProfileMenu();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Workboard'));
return $this->newPage()
->setTitle(
array(
$project->getDisplayName(),
pht('Workboard'),
))
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild($content);
}
// If the user wants to turn a particular column into a query, build an
// apropriate filter and redirect them to the query results page.
$query_column_id = $request->getInt('queryColumnID');
if ($query_column_id) {
$column_id_map = mpull($columns, null, 'getID');
$query_column = idx($column_id_map, $query_column_id);
if (!$query_column) {
return new Aphront404Response();
}
// Create a saved query to combine the active filter on the workboard
// with the column filter. If the user currently has constraints on the
// board, we want to add a new column or project constraint, not
// completely replace the constraints.
$saved_query = $saved->newCopy();
if ($query_column->getProxyPHID()) {
$project_phids = $saved_query->getParameter('projectPHIDs');
if (!$project_phids) {
$project_phids = array();
}
$project_phids[] = $query_column->getProxyPHID();
$saved_query->setParameter('projectPHIDs', $project_phids);
} else {
$saved_query->setParameter(
'columnPHIDs',
array($query_column->getPHID()));
}
$search_engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer);
$search_engine->saveQuery($saved_query);
$query_key = $saved_query->getQueryKey();
$query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R");
return id(new AphrontRedirectResponse())
->setURI($query_uri);
}
$task_can_edit_map = id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
->apply($tasks);
// If this is a batch edit, select the editable tasks in the chosen column
// and ship the user into the batch editor.
$batch_edit = $request->getStr('batch');
if ($batch_edit) {
if ($batch_edit !== self::BATCH_EDIT_ALL) {
$column_id_map = mpull($columns, null, 'getID');
$batch_column = idx($column_id_map, $batch_edit);
if (!$batch_column) {
return new Aphront404Response();
}
$batch_task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$batch_column->getPHID());
foreach ($batch_task_phids as $key => $batch_task_phid) {
if (empty($task_can_edit_map[$batch_task_phid])) {
unset($batch_task_phids[$key]);
}
}
$batch_tasks = array_select_keys($tasks, $batch_task_phids);
} else {
$batch_tasks = $task_can_edit_map;
}
if (!$batch_tasks) {
$cancel_uri = $this->getURIWithState($board_uri);
return $this->newDialog()
->setTitle(pht('No Editable Tasks'))
->appendParagraph(
pht(
'The selected column contains no visible tasks which you '.
'have permission to edit.'))
->addCancelButton($board_uri);
}
// Create a saved query to hold the working set. This allows us to get
// around URI length limitations with a long "?ids=..." query string.
// For details, see T10268.
$search_engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer);
$saved_query = $search_engine->newSavedQuery();
$saved_query->setParameter('ids', mpull($batch_tasks, 'getID'));
$search_engine->saveQuery($saved_query);
$query_key = $saved_query->getQueryKey();
$bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/");
$bulk_uri->replaceQueryParam('board', $this->id);
return id(new AphrontRedirectResponse())
->setURI($bulk_uri);
}
$move_id = $request->getStr('move');
if (strlen($move_id)) {
$column_id_map = mpull($columns, null, 'getID');
$move_column = idx($column_id_map, $move_id);
if (!$move_column) {
return new Aphront404Response();
}
$move_task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$move_column->getPHID());
foreach ($move_task_phids as $key => $move_task_phid) {
if (empty($task_can_edit_map[$move_task_phid])) {
unset($move_task_phids[$key]);
}
}
$move_tasks = array_select_keys($tasks, $move_task_phids);
$cancel_uri = $this->getURIWithState($board_uri);
if (!$move_tasks) {
return $this->newDialog()
->setTitle(pht('No Movable Tasks'))
->appendParagraph(
pht(
'The selected column contains no visible tasks which you '.
'have permission to move.'))
->addCancelButton($cancel_uri);
}
$move_project_phid = $project->getPHID();
$move_column_phid = null;
$move_project = null;
$move_column = null;
$columns = null;
$errors = array();
if ($request->isFormOrHiSecPost()) {
$move_project_phid = head($request->getArr('moveProjectPHID'));
if (!$move_project_phid) {
$move_project_phid = $request->getStr('moveProjectPHID');
}
if (!$move_project_phid) {
if ($request->getBool('hasProject')) {
$errors[] = pht('Choose a project to move tasks to.');
}
} else {
$target_project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array($move_project_phid))
->executeOne();
if (!$target_project) {
$errors[] = pht('You must choose a valid project.');
} else if (!$project->getHasWorkboard()) {
$errors[] = pht(
'You must choose a project with a workboard.');
} else {
$move_project = $target_project;
}
}
if ($move_project) {
$move_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($move_project->getPHID()))
->setFetchAllBoards(true)
->executeLayout();
$columns = $move_engine->getColumns($move_project->getPHID());
$columns = mpull($columns, null, 'getPHID');
foreach ($columns as $key => $column) {
if ($column->isHidden()) {
unset($columns[$key]);
}
}
$move_column_phid = $request->getStr('moveColumnPHID');
if (!$move_column_phid) {
if ($request->getBool('hasColumn')) {
$errors[] = pht('Choose a column to move tasks to.');
}
} else {
if (empty($columns[$move_column_phid])) {
$errors[] = pht(
'Choose a valid column on the target workboard to move '.
'tasks to.');
} else if ($columns[$move_column_phid]->getID() == $move_id) {
$errors[] = pht(
'You can not move tasks from a column to itself.');
} else {
$move_column = $columns[$move_column_phid];
}
}
}
}
if ($move_column && $move_project) {
foreach ($move_tasks as $move_task) {
$xactions = array();
// If we're switching projects, get out of the old project first
// and move to the new project.
if ($move_project->getID() != $project->getID()) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'-' => array(
$project->getPHID() => $project->getPHID(),
),
'+' => array(
$move_project->getPHID() => $move_project->getPHID(),
),
));
}
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(
array(
array(
'columnPHID' => $move_column->getPHID(),
),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setCancelURI($cancel_uri);
$editor->applyTransactions($move_task, $xactions);
}
return id(new AphrontRedirectResponse())
->setURI($cancel_uri);
}
if ($move_project) {
$column_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('moveColumnPHID')
->setLabel(pht('Move to Column'))
->setValue($move_column_phid)
->setOptions(mpull($columns, 'getDisplayName', 'getPHID')));
return $this->newDialog()
->setTitle(pht('Move Tasks'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->addHiddenInput('move', $move_id)
->addHiddenInput('moveProjectPHID', $move_project->getPHID())
->addHiddenInput('hasColumn', true)
->addHiddenInput('hasProject', true)
->appendParagraph(
pht(
'Choose a column on the %s workboard to move tasks to:',
$viewer->renderHandle($move_project->getPHID())))
->appendForm($column_form)
->addSubmitButton(pht('Move Tasks'))
->addCancelButton($cancel_uri);
}
if ($move_project_phid) {
$move_project_phid_value = array($move_project_phid);
} else {
$move_project_phid_value = array();
}
$project_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('moveProjectPHID')
->setLimit(1)
->setLabel(pht('Move to Project'))
->setValue($move_project_phid_value)
->setDatasource(new PhabricatorProjectDatasource()));
return $this->newDialog()
->setTitle(pht('Move Tasks'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->addHiddenInput('move', $move_id)
->addHiddenInput('hasProject', true)
->appendForm($project_form)
->addSubmitButton(pht('Continue'))
->addCancelButton($cancel_uri);
}
$board_id = celerity_generate_unique_node_id();
$board = id(new PHUIWorkboardView())
->setUser($viewer)
->setID($board_id)
->addSigil('jx-workboard')
->setMetadata(
array(
'boardPHID' => $project->getPHID(),
));
$visible_columns = array();
$column_phids = array();
$visible_phids = array();
foreach ($columns as $column) {
if (!$this->showHidden) {
if ($column->isHidden()) {
continue;
}
}
$proxy = $column->getProxy();
if ($proxy && !$proxy->isMilestone()) {
// TODO: For now, don't show subproject columns because we can't
// handle tasks with multiple positions yet.
continue;
}
$task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$column->getPHID());
$column_tasks = array_select_keys($tasks, $task_phids);
$column_phid = $column->getPHID();
$visible_columns[$column_phid] = $column;
$column_phids[$column_phid] = $column_tasks;
foreach ($column_tasks as $phid => $task) {
$visible_phids[$phid] = $phid;
}
}
$rendering_engine = id(new PhabricatorBoardRenderingEngine())
->setViewer($viewer)
->setObjects(array_select_keys($tasks, $visible_phids))
->setEditMap($task_can_edit_map)
->setExcludedProjectPHIDs($select_phids);
$templates = array();
$all_tasks = array();
$column_templates = array();
$sounds = array();
foreach ($visible_columns as $column_phid => $column) {
$column_tasks = $column_phids[$column_phid];
$panel = id(new PHUIWorkpanelView())
->setHeader($column->getDisplayName())
->setSubHeader($column->getDisplayType())
->addSigil('workpanel');
$proxy = $column->getProxy();
if ($proxy) {
$proxy_id = $proxy->getID();
$href = $this->getApplicationURI("view/{$proxy_id}/");
$panel->setHref($href);
}
$header_icon = $column->getHeaderIcon();
if ($header_icon) {
$panel->setHeaderIcon($header_icon);
}
$display_class = $column->getDisplayClass();
if ($display_class) {
$panel->addClass($display_class);
}
if ($column->isHidden()) {
$panel->addClass('project-panel-hidden');
}
$column_menu = $this->buildColumnMenu($project, $column);
$panel->addHeaderAction($column_menu);
if ($column->canHaveTrigger()) {
$trigger_menu = $this->buildTriggerMenu($column);
$panel->addHeaderAction($trigger_menu);
}
$count_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_BLUE)
->addSigil('column-points')
->setName(
javelin_tag(
'span',
array(
'sigil' => 'column-points-content',
),
pht('-')))
->setStyle('display: none');
$panel->setHeaderTag($count_tag);
$cards = id(new PHUIObjectItemListView())
->setUser($viewer)
->setFlush(true)
->setAllowEmptyList(true)
->addSigil('project-column')
->setItemClass('phui-workcard')
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
'pointLimit' => $column->getPointLimit(),
));
$card_phids = array();
foreach ($column_tasks as $task) {
$object_phid = $task->getPHID();
$card = $rendering_engine->renderCard($object_phid);
$templates[$object_phid] = hsprintf('%s', $card->getItem());
$card_phids[] = $object_phid;
$all_tasks[$object_phid] = $task;
}
$panel->setCards($cards);
$board->addPanel($panel);
$drop_effects = $column->getDropEffects();
$drop_effects = mpull($drop_effects, 'toDictionary');
$preview_effect = null;
if ($column->canHaveTrigger()) {
$trigger = $column->getTrigger();
if ($trigger) {
$preview_effect = $trigger->getPreviewEffect()
->toDictionary();
foreach ($trigger->getSoundEffects() as $sound) {
$sounds[] = $sound;
}
}
}
$column_templates[] = array(
'columnPHID' => $column_phid,
'effects' => $drop_effects,
'cardPHIDs' => $card_phids,
'triggerPreviewEffect' => $preview_effect,
);
}
$order_key = $this->sortKey;
$ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders();
$ordering = id(clone $ordering_map[$order_key])
->setViewer($viewer);
$headers = $ordering->getHeadersForObjects($all_tasks);
$headers = mpull($headers, 'toDictionary');
$vectors = $ordering->getSortVectorsForObjects($all_tasks);
$vector_map = array();
foreach ($vectors as $task_phid => $vector) {
$vector_map[$task_phid][$order_key] = $vector;
}
$header_keys = $ordering->getHeaderKeysForObjects($all_tasks);
$order_maps = array();
$order_maps[] = $ordering->toDictionary();
$properties = array();
foreach ($all_tasks as $task) {
$properties[$task->getPHID()] =
PhabricatorBoardResponseEngine::newTaskProperties($task);
}
$behavior_config = array(
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'uploadURI' => '/file/dropupload/',
'coverURI' => $this->getApplicationURI('cover/'),
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(),
'boardPHID' => $project->getPHID(),
'order' => $this->sortKey,
'orders' => $order_maps,
'headers' => $headers,
'headerKeys' => $header_keys,
'templateMap' => $templates,
'orderMaps' => $vector_map,
'propertyMaps' => $properties,
'columnTemplates' => $column_templates,
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
'preloadSounds' => $sounds,
);
$this->initBehavior('project-boards', $behavior_config);
$sort_menu = $this->buildSortMenu(
$viewer,
$project,
$this->sortKey,
$ordering_map);
$filter_menu = $this->buildFilterMenu(
$viewer,
$project,
$custom_query,
$search_engine,
$query_key);
$manage_menu = $this->buildManageMenu($project, $this->showHidden);
$header_link = phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('profile/'.$project->getID().'/'),
),
$project->getName());
$board_box = id(new PHUIBoxView())
->appendChild($board)
->addClass('project-board-wrapper');
- $nav = $this->getProfileMenu();
+ $nav = $this->newWorkboardProfileMenu();
$divider = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_DIVIDER);
$fullscreen = $this->buildFullscreenMenu();
$crumbs = $this->newWorkboardCrumbs();
$crumbs->addTextCrumb(pht('Workboard'));
$crumbs->setBorder(true);
$crumbs->addAction($sort_menu);
$crumbs->addAction($filter_menu);
$crumbs->addAction($divider);
$crumbs->addAction($manage_menu);
$crumbs->addAction($fullscreen);
$page = $this->newPage()
->setTitle(
array(
$project->getDisplayName(),
pht('Workboard'),
))
->setPageObjectPHIDs(array($project->getPHID()))
->setShowFooter(false)
->setNavigation($nav)
->setCrumbs($crumbs)
->addQuicksandConfig(
array(
'boardConfig' => $behavior_config,
))
->appendChild(
array(
$board_box,
));
$background = $project->getDisplayWorkboardBackgroundColor();
require_celerity_resource('phui-workboard-color-css');
if ($background !== null) {
$background_color_class = "phui-workboard-{$background}";
$page->addClass('phui-workboard-color');
$page->addClass($background_color_class);
} else {
$page->addClass('phui-workboard-no-color');
}
return $page;
}
private function readRequestState() {
$request = $this->getRequest();
$project = $this->getProject();
$this->showHidden = $request->getBool('hidden');
$this->id = $project->getID();
$sort_key = $this->getDefaultSort($project);
$request_sort = $request->getStr('order');
if ($this->isValidSort($request_sort)) {
$sort_key = $request_sort;
}
$this->sortKey = $sort_key;
}
private function getDefaultSort(PhabricatorProject $project) {
$default_sort = $project->getDefaultWorkboardSort();
if ($this->isValidSort($default_sort)) {
return $default_sort;
}
return PhabricatorProjectColumnNaturalOrder::ORDERKEY;
}
private function getDefaultFilter(PhabricatorProject $project) {
$default_filter = $project->getDefaultWorkboardFilter();
if (strlen($default_filter)) {
return $default_filter;
}
return 'open';
}
private function isValidSort($sort) {
$map = PhabricatorProjectColumnOrder::getEnabledOrders();
return isset($map[$sort]);
}
private function buildSortMenu(
PhabricatorUser $viewer,
PhabricatorProject $project,
$sort_key,
array $ordering_map) {
$base_uri = $this->getURIWithState();
$items = array();
foreach ($ordering_map as $key => $ordering) {
// TODO: It would be desirable to build a real "PHUIIconView" here, but
// the pathway for threading that through all the view classes ends up
// being fairly complex, since some callers read the icon out of other
// views. For now, just stick with a string.
$ordering_icon = $ordering->getMenuIconIcon();
$ordering_name = $ordering->getDisplayName();
$is_selected = ($key === $sort_key);
if ($is_selected) {
$active_name = $ordering_name;
$active_icon = $ordering_icon;
}
$item = id(new PhabricatorActionView())
->setIcon($ordering_icon)
->setSelected($is_selected)
->setName($ordering_name);
$uri = $base_uri->alter('order', $key);
$item->setHref($uri);
$items[] = $item;
}
$id = $project->getID();
$save_uri = "default/{$id}/sort/";
$save_uri = $this->getApplicationURI($save_uri);
$save_uri = $this->getURIWithState($save_uri, $force = true);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-floppy-o')
->setName(pht('Save as Default'))
->setHref($save_uri)
->setWorkflow(true)
->setDisabled(!$can_edit);
$sort_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$sort_menu->addAction($item);
}
$sort_button = id(new PHUIListItemView())
->setName($active_name)
->setIcon($active_icon)
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $sort_menu),
));
return $sort_button;
}
private function buildFilterMenu(
PhabricatorUser $viewer,
PhabricatorProject $project,
$custom_query,
PhabricatorApplicationSearchEngine $engine,
$query_key) {
$named = array(
'open' => pht('Open Tasks'),
'all' => pht('All Tasks'),
);
if ($viewer->isLoggedIn()) {
$named['assigned'] = pht('Assigned to Me');
}
if ($custom_query) {
$named[$custom_query->getQueryKey()] = pht('Custom Filter');
}
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $query_key);
if ($is_selected) {
$active_filter = $name;
}
$is_custom = false;
if ($custom_query) {
$is_custom = ($key == $custom_query->getQueryKey());
}
$item = id(new PhabricatorActionView())
->setIcon('fa-search')
->setSelected($is_selected)
->setName($name);
if ($is_custom) {
$uri = $this->getApplicationURI(
'board/'.$this->id.'/filter/query/'.$key.'/');
$item->setWorkflow(true);
} else {
$uri = $engine->getQueryResultsPageURI($key);
}
$uri = $this->getURIWithState($uri)
->removeQueryParam('filter');
$item->setHref($uri);
$items[] = $item;
}
$id = $project->getID();
$filter_uri = $this->getApplicationURI("board/{$id}/filter/");
$filter_uri = $this->getURIWithState($filter_uri, $force = true);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-cog')
->setHref($filter_uri)
->setWorkflow(true)
->setName(pht('Advanced Filter...'));
$save_uri = "default/{$id}/filter/";
$save_uri = $this->getApplicationURI($save_uri);
$save_uri = $this->getURIWithState($save_uri, $force = true);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-floppy-o')
->setName(pht('Save as Default'))
->setHref($save_uri)
->setWorkflow(true)
->setDisabled(!$can_edit);
$filter_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$filter_menu->addAction($item);
}
$filter_button = id(new PHUIListItemView())
->setName($active_filter)
->setIcon('fa-search')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $filter_menu),
));
return $filter_button;
}
private function buildManageMenu(
PhabricatorProject $project,
$show_hidden) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $project->getID();
$manage_uri = $this->getApplicationURI("board/{$id}/manage/");
$add_uri = $this->getApplicationURI("board/{$id}/edit/");
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$manage_items = array();
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-plus')
->setName(pht('Add Column'))
->setHref($add_uri)
->setDisabled(!$can_edit)
->setWorkflow(true);
$reorder_uri = $this->getApplicationURI("board/{$id}/reorder/");
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-exchange')
->setName(pht('Reorder Columns'))
->setHref($reorder_uri)
->setDisabled(!$can_edit)
->setWorkflow(true);
if ($show_hidden) {
$hidden_uri = $this->getURIWithState()
->removeQueryParam('hidden');
$hidden_icon = 'fa-eye-slash';
$hidden_text = pht('Hide Hidden Columns');
} else {
$hidden_uri = $this->getURIWithState()
->replaceQueryParam('hidden', 'true');
$hidden_icon = 'fa-eye';
$hidden_text = pht('Show Hidden Columns');
}
$manage_items[] = id(new PhabricatorActionView())
->setIcon($hidden_icon)
->setName($hidden_text)
->setHref($hidden_uri);
$manage_items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$background_uri = $this->getApplicationURI("board/{$id}/background/");
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-paint-brush')
->setName(pht('Change Background Color'))
->setHref($background_uri)
->setDisabled(!$can_edit)
->setWorkflow(false);
$manage_uri = $this->getApplicationURI("board/{$id}/manage/");
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-gear')
->setName(pht('Manage Workboard'))
->setHref($manage_uri);
$batch_edit_uri = $request->getRequestURI();
$batch_edit_uri->replaceQueryParam('batch', self::BATCH_EDIT_ALL);
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
ManiphestBulkEditCapability::CAPABILITY);
$manage_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($manage_items as $item) {
$manage_menu->addAction($item);
}
$manage_button = id(new PHUIListItemView())
->setIcon('fa-cog')
->setHref('#')
->addSigil('boards-dropdown-menu')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Manage'),
'align' => 'S',
'items' => hsprintf('%s', $manage_menu),
));
return $manage_button;
}
private function buildFullscreenMenu() {
$up = id(new PHUIListItemView())
->setIcon('fa-arrows-alt')
->setHref('#')
->addClass('phui-workboard-expand-icon')
->addSigil('jx-toggle-class')
->addSigil('has-tooltip')
->setMetaData(array(
'tip' => pht('Fullscreen'),
'map' => array(
'phabricator-standard-page' => 'phui-workboard-fullscreen',
),
));
return $up;
}
private function buildColumnMenu(
PhabricatorProject $project,
PhabricatorProjectColumn $column) {
$request = $this->getRequest();
$viewer = $request->getUser();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$column_items = array();
if ($column->getProxyPHID()) {
$default_phid = $column->getProxyPHID();
} else {
$default_phid = $column->getProjectPHID();
}
$specs = id(new ManiphestEditEngine())
->setViewer($viewer)
->newCreateActionSpecifications(array());
foreach ($specs as $spec) {
$column_items[] = id(new PhabricatorActionView())
->setIcon($spec['icon'])
->setName($spec['name'])
->setHref($spec['uri'])
->setDisabled($spec['disabled'])
->addSigil('column-add-task')
->setMetadata(
array(
'createURI' => $spec['uri'],
'columnPHID' => $column->getPHID(),
'boardPHID' => $project->getPHID(),
'projectPHID' => $default_phid,
));
}
$column_items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$batch_edit_uri = $request->getRequestURI();
$batch_edit_uri->replaceQueryParam('batch', $column->getID());
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
ManiphestBulkEditCapability::CAPABILITY);
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-list-ul')
->setName(pht('Bulk Edit Tasks...'))
->setHref($batch_edit_uri)
->setDisabled(!$can_batch_edit);
$batch_move_uri = $request->getRequestURI();
$batch_move_uri->replaceQueryParam('move', $column->getID());
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-arrow-right')
->setName(pht('Move Tasks to Column...'))
->setHref($batch_move_uri)
->setWorkflow(true);
$query_uri = $request->getRequestURI();
$query_uri->replaceQueryParam('queryColumnID', $column->getID());
$column_items[] = id(new PhabricatorActionView())
->setName(pht('View as Query'))
->setIcon('fa-search')
->setHref($query_uri);
$edit_uri = 'board/'.$this->id.'/edit/'.$column->getID().'/';
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Edit Column'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI($edit_uri))
->setDisabled(!$can_edit)
->setWorkflow(true);
$can_hide = ($can_edit && !$column->isDefaultColumn());
$hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/';
$hide_uri = $this->getApplicationURI($hide_uri);
$hide_uri = $this->getURIWithState($hide_uri);
if (!$column->isHidden()) {
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Hide Column'))
->setIcon('fa-eye-slash')
->setHref($hide_uri)
->setDisabled(!$can_hide)
->setWorkflow(true);
} else {
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Show Column'))
->setIcon('fa-eye')
->setHref($hide_uri)
->setDisabled(!$can_hide)
->setWorkflow(true);
}
$column_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($column_items as $item) {
$column_menu->addAction($item);
}
$column_button = id(new PHUIIconView())
->setIcon('fa-pencil')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $column_menu),
));
return $column_button;
}
private function buildTriggerMenu(PhabricatorProjectColumn $column) {
$viewer = $this->getViewer();
$trigger = $column->getTrigger();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$trigger_items = array();
if (!$trigger) {
$set_uri = $this->getApplicationURI(
new PhutilURI(
'trigger/edit/',
array(
'columnPHID' => $column->getPHID(),
)));
$trigger_items[] = id(new PhabricatorActionView())
->setIcon('fa-cogs')
->setName(pht('New Trigger...'))
->setHref($set_uri)
->setDisabled(!$can_edit);
} else {
$trigger_items[] = id(new PhabricatorActionView())
->setIcon('fa-cogs')
->setName(pht('View Trigger'))
->setHref($trigger->getURI())
->setDisabled(!$can_edit);
}
$remove_uri = $this->getApplicationURI(
new PhutilURI(
urisprintf(
'column/remove/%d/',
$column->getID())));
$trigger_items[] = id(new PhabricatorActionView())
->setIcon('fa-times')
->setName(pht('Remove Trigger'))
->setHref($remove_uri)
->setWorkflow(true)
->setDisabled(!$can_edit || !$trigger);
$trigger_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($trigger_items as $item) {
$trigger_menu->addAction($item);
}
if ($trigger) {
$trigger_icon = 'fa-cogs';
} else {
$trigger_icon = 'fa-cogs grey';
}
$trigger_button = id(new PHUIIconView())
->setIcon($trigger_icon)
->setHref('#')
->addSigil('boards-dropdown-menu')
->addSigil('trigger-preview')
->setMetadata(
array(
'items' => hsprintf('%s', $trigger_menu),
'columnPHID' => $column->getPHID(),
));
return $trigger_button;
}
/**
* Add current state parameters (like order and the visibility of hidden
* columns) to a URI.
*
* This allows actions which toggle or adjust one piece of state to keep
* the rest of the board state persistent. If no URI is provided, this method
* starts with the request URI.
*
* @param string|null URI to add state parameters to.
* @param bool True to explicitly include all state.
* @return PhutilURI URI with state parameters.
*/
private function getURIWithState($base = null, $force = false) {
$project = $this->getProject();
if ($base === null) {
$base = $this->getRequest()->getPath();
}
$base = new PhutilURI($base);
if ($force || ($this->sortKey != $this->getDefaultSort($project))) {
if ($this->sortKey !== null) {
$base->replaceQueryParam('order', $this->sortKey);
} else {
$base->removeQueryParam('order');
}
} else {
$base->removeQueryParam('order');
}
if ($force || ($this->queryKey != $this->getDefaultFilter($project))) {
if ($this->queryKey !== null) {
$base->replaceQueryParam('filter', $this->queryKey);
} else {
$base->removeQueryParam('filter');
}
} else {
$base->removeQueryParam('filter');
}
if ($this->showHidden) {
$base->replaceQueryParam('hidden', 'true');
} else {
$base->removeQueryParam('hidden');
}
return $base;
}
private function buildInitializeContent(PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$type = $request->getStr('initialize-type');
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
$board_uri = $this->getApplicationURI("board/{$id}/");
$import_uri = $this->getApplicationURI("board/{$id}/import/");
$set_default = $request->getBool('default');
if ($set_default) {
$this
->getProfileMenuEngine()
->adjustDefault(PhabricatorProject::ITEM_WORKBOARD);
}
if ($request->isFormPost()) {
if ($type == 'backlog-only') {
$column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', true)
->setProjectPHID($project->getPHID())
->save();
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
->setNewValue(1);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($project, $xactions);
return id(new AphrontRedirectResponse())
->setURI($board_uri);
} else {
return id(new AphrontRedirectResponse())
->setURI($import_uri);
}
}
// TODO: Tailor this UI if the project is already a parent project. We
// should not offer options for creating a parent project workboard, since
// they can't have their own columns.
$new_selector = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Columns'))
->setName('initialize-type')
->setValue('backlog-only')
->addButton(
'backlog-only',
pht('New Empty Board'),
pht('Create a new board with just a backlog column.'))
->addButton(
'import',
pht('Import Columns'),
pht('Import board columns from another project.'));
$default_checkbox = id(new AphrontFormCheckboxControl())
->setLabel(pht('Make Default'))
->addCheckbox(
'default',
1,
pht('Make the workboard the default view for this project.'),
true);
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('initialize', 1)
->appendRemarkupInstructions(
pht('The workboard for this project has not been created yet.'))
->appendControl($new_selector)
->appendControl($default_checkbox)
->appendControl(
id(new AphrontFormSubmitControl())
->addCancelButton($profile_uri)
->setValue(pht('Create Workboard')));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Create Workboard'))
->setForm($form);
return $box;
}
private function buildNoAccessContent(PhabricatorProject $project) {
$viewer = $this->getViewer();
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
return $this->newDialog()
->setTitle(pht('Unable to Create Workboard'))
->appendParagraph(
pht(
'The workboard for this project has not been created yet, '.
'but you do not have permission to create it. Only users '.
'who can edit this project can create a workboard for it.'))
->addCancelButton($profile_uri);
}
private function buildEnableContent(PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
$board_uri = $this->getApplicationURI("board/{$id}/");
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
->setNewValue(1);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($project, $xactions);
return id(new AphrontRedirectResponse())
->setURI($board_uri);
}
return $this->newDialog()
->setTitle(pht('Workboard Disabled'))
->addHiddenInput('initialize', 1)
->appendParagraph(
pht(
'This workboard has been disabled, but can be restored to its '.
'former glory.'))
->addCancelButton($profile_uri)
->addSubmitButton(pht('Enable Workboard'));
}
private function buildDisabledContent(PhabricatorProject $project) {
$viewer = $this->getViewer();
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
return $this->newDialog()
->setTitle(pht('Workboard Disabled'))
->appendParagraph(
pht(
'This workboard has been disabled, and you do not have permission '.
'to enable it. Only users who can edit this project can restore '.
'the workboard.'))
->addCancelButton($profile_uri);
}
+ private function newWorkboardProfileMenu() {
+ $default_item = id(new PhabricatorProfileMenuItemConfiguration())
+ ->setBuiltinKey(PhabricatorProject::ITEM_WORKBOARD);
+
+ $menu = parent::getProfileMenu($default_item);
+
+ $menu->addClass('project-board-nav');
+
+ return $menu;
+ }
+
}
diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php
index 63494bf442..eb4ab70711 100644
--- a/src/applications/project/controller/PhabricatorProjectController.php
+++ b/src/applications/project/controller/PhabricatorProjectController.php
@@ -1,210 +1,210 @@
<?php
abstract class PhabricatorProjectController extends PhabricatorController {
private $project;
private $profileMenu;
private $profileMenuEngine;
protected function setProject(PhabricatorProject $project) {
$this->project = $project;
return $this;
}
protected function getProject() {
return $this->project;
}
protected function loadProject() {
$viewer = $this->getViewer();
$request = $this->getRequest();
$id = nonempty(
$request->getURIData('projectID'),
$request->getURIData('id'));
$slug = $request->getURIData('slug');
if ($slug) {
$normal_slug = PhabricatorSlug::normalizeProjectSlug($slug);
$is_abnormal = ($slug !== $normal_slug);
$normal_uri = "/tag/{$normal_slug}/";
} else {
$is_abnormal = false;
}
$query = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers(true)
->needWatchers(true)
->needImages(true)
->needSlugs(true);
if ($slug) {
$query->withSlugs(array($slug));
} else {
$query->withIDs(array($id));
}
$policy_exception = null;
try {
$project = $query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$policy_exception = $ex;
$project = null;
}
if (!$project) {
// This project legitimately does not exist, so just 404 the user.
if (!$policy_exception) {
return new Aphront404Response();
}
// Here, the project exists but the user can't see it. If they are
// using a non-canonical slug to view the project, redirect to the
// canonical slug. If they're already using the canonical slug, rethrow
// the exception to give them the policy error.
if ($is_abnormal) {
return id(new AphrontRedirectResponse())->setURI($normal_uri);
} else {
throw $policy_exception;
}
}
// The user can view the project, but is using a noncanonical slug.
// Redirect to the canonical slug.
$primary_slug = $project->getPrimarySlug();
if ($slug && ($slug !== $primary_slug)) {
$primary_uri = "/tag/{$primary_slug}/";
return id(new AphrontRedirectResponse())->setURI($primary_uri);
}
$this->setProject($project);
return null;
}
public function buildApplicationMenu() {
$menu = $this->newApplicationMenu();
$profile_menu = $this->getProfileMenu();
if ($profile_menu) {
$menu->setProfileMenu($profile_menu);
}
$menu->setSearchEngine(new PhabricatorProjectSearchEngine());
return $menu;
}
- protected function getProfileMenu() {
+ protected function getProfileMenu($default_item = null) {
if (!$this->profileMenu) {
$engine = $this->getProfileMenuEngine();
if ($engine) {
- $this->profileMenu = $engine->buildNavigation();
+ $this->profileMenu = $engine->buildNavigation($default_item);
}
}
return $this->profileMenu;
}
protected function buildApplicationCrumbs() {
return $this->newApplicationCrumbs('profile');
}
protected function newWorkboardCrumbs() {
return $this->newApplicationCrumbs('workboard');
}
private function newApplicationCrumbs($mode) {
$crumbs = parent::buildApplicationCrumbs();
$project = $this->getProject();
if ($project) {
$ancestors = $project->getAncestorProjects();
$ancestors = array_reverse($ancestors);
$ancestors[] = $project;
foreach ($ancestors as $ancestor) {
if ($ancestor->getPHID() === $project->getPHID()) {
// Link the current project's crumb to its profile no matter what,
// since we're already on the right context page for it and linking
// to the current page isn't helpful.
$crumb_uri = $ancestor->getProfileURI();
} else {
switch ($mode) {
case 'workboard':
$crumb_uri = $ancestor->getWorkboardURI();
break;
case 'profile':
default:
$crumb_uri = $ancestor->getProfileURI();
break;
}
}
$crumbs->addTextCrumb($ancestor->getName(), $crumb_uri);
}
}
return $crumbs;
}
protected function getProfileMenuEngine() {
if (!$this->profileMenuEngine) {
$viewer = $this->getViewer();
$project = $this->getProject();
if ($project) {
$engine = id(new PhabricatorProjectProfileMenuEngine())
->setViewer($viewer)
->setController($this)
->setProfileObject($project);
$this->profileMenuEngine = $engine;
}
}
return $this->profileMenuEngine;
}
protected function setProfileMenuEngine(
PhabricatorProjectProfileMenuEngine $engine) {
$this->profileMenuEngine = $engine;
return $this;
}
protected function newCardResponse(
$board_phid,
$object_phid,
PhabricatorProjectColumnOrder $ordering = null,
$sounds = array()) {
$viewer = $this->getViewer();
$request = $this->getRequest();
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
$engine = id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
->setVisiblePHIDs($visible_phids)
->setSounds($sounds);
if ($ordering) {
$engine->setOrdering($ordering);
}
return $engine->buildResponse();
}
public function renderHashtags(array $tags) {
$result = array();
foreach ($tags as $key => $tag) {
$result[] = '#'.$tag;
}
return implode(', ', $result);
}
}
diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php
index f4fde5fdff..e2439a341b 100644
--- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php
+++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php
@@ -1,1370 +1,1469 @@
<?php
abstract class PhabricatorProfileMenuEngine extends Phobject {
private $viewer;
private $profileObject;
private $customPHID;
private $items;
private $controller;
private $navigation;
private $showNavigation = true;
private $editMode;
private $pageClasses = array();
private $showContentCrumbs = true;
const ITEM_CUSTOM_DIVIDER = 'engine.divider';
const ITEM_MANAGE = 'item.configure';
const MODE_COMBINED = 'combined';
const MODE_GLOBAL = 'global';
const MODE_CUSTOM = 'custom';
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setProfileObject($profile_object) {
$this->profileObject = $profile_object;
return $this;
}
public function getProfileObject() {
return $this->profileObject;
}
public function setCustomPHID($custom_phid) {
$this->customPHID = $custom_phid;
return $this;
}
public function getCustomPHID() {
return $this->customPHID;
}
private function getEditModeCustomPHID() {
$mode = $this->getEditMode();
switch ($mode) {
case self::MODE_CUSTOM:
$custom_phid = $this->getCustomPHID();
break;
case self::MODE_GLOBAL:
$custom_phid = null;
break;
}
return $custom_phid;
}
public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
private function setDefaultItem(
PhabricatorProfileMenuItemConfiguration $default_item) {
$this->defaultItem = $default_item;
return $this;
}
public function getDefaultItem() {
return $this->pickDefaultItem($this->getItems());
}
public function setShowNavigation($show) {
$this->showNavigation = $show;
return $this;
}
public function getShowNavigation() {
return $this->showNavigation;
}
public function addContentPageClass($class) {
$this->pageClasses[] = $class;
return $this;
}
public function setShowContentCrumbs($show_content_crumbs) {
$this->showContentCrumbs = $show_content_crumbs;
return $this;
}
public function getShowContentCrumbs() {
return $this->showContentCrumbs;
}
abstract public function getItemURI($path);
abstract protected function isMenuEngineConfigurable();
abstract protected function getBuiltinProfileItems($object);
protected function getBuiltinCustomProfileItems(
$object,
$custom_phid) {
return array();
}
protected function getEditMode() {
return $this->editMode;
}
public function buildResponse() {
$controller = $this->getController();
$viewer = $controller->getViewer();
$this->setViewer($viewer);
$request = $controller->getRequest();
$item_action = $request->getURIData('itemAction');
if (!$item_action) {
$item_action = 'view';
}
$is_view = ($item_action == 'view');
// If the engine is not configurable, don't respond to any of the editing
// or configuration routes.
if (!$this->isMenuEngineConfigurable()) {
if (!$is_view) {
return new Aphront404Response();
}
}
$item_id = $request->getURIData('itemID');
// If we miss on the MenuEngine route, try the EditEngine route. This will
// be populated while editing items.
if (!$item_id) {
$item_id = $request->getURIData('id');
}
$item_list = $this->getItems();
$selected_item = $this->pickSelectedItem(
$item_list,
$item_id,
$is_view);
switch ($item_action) {
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 'hide':
case 'default':
case 'builtin':
if (!$selected_item) {
return new Aphront404Response();
}
break;
case 'edit':
if (!$request->getURIData('id')) {
// If we continue along the "edit" pathway without an ID, we hit an
// unrelated exception because we can not build a new menu item out
// of thin air. For menus, new items are created via the "new"
// action. Just catch this case and 404 early since there's currently
// no clean way to make EditEngine aware of this.
return new Aphront404Response();
}
break;
}
- $navigation = $this->buildNavigation();
+ $navigation = $this->buildNavigation($selected_item);
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if (!$is_view) {
$navigation->selectFilter(self::ITEM_MANAGE);
if ($selected_item) {
if ($selected_item->getCustomPHID()) {
$edit_mode = 'custom';
} else {
$edit_mode = 'global';
}
} else {
$edit_mode = $request->getURIData('itemEditMode');
}
$available_modes = $this->getViewerEditModes();
if ($available_modes) {
$available_modes = array_fuse($available_modes);
if (isset($available_modes[$edit_mode])) {
$this->editMode = $edit_mode;
} else {
if ($item_action != 'configure') {
return new Aphront404Response();
}
}
}
$page_title = pht('Configure Menu');
} else {
if ($selected_item) {
$page_title = $selected_item->getDisplayName();
} else {
$page_title = pht('Empty');
}
}
switch ($item_action) {
case 'view':
if ($selected_item) {
- $navigation->selectFilter($selected_item->getDefaultMenuItemKey());
-
try {
$content = $this->buildItemViewContent($selected_item);
} catch (Exception $ex) {
$content = id(new PHUIInfoView())
->setTitle(pht('Unable to Render Dashboard'))
->setErrors(array($ex->getMessage()));
}
$crumbs->addTextCrumb($selected_item->getDisplayName());
} else {
$content = $this->newNoMenuItemsView();
}
if (!$content) {
$content = $this->newEmptyView(
pht('Empty'),
pht('There is nothing here.'));
}
break;
case 'configure':
$mode = $this->getEditMode();
if (!$mode) {
$crumbs->addTextCrumb(pht('Configure Menu'));
$content = $this->buildMenuEditModeContent();
} else {
if (count($available_modes) > 1) {
$crumbs->addTextCrumb(
pht('Configure Menu'),
$this->getItemURI('configure/'));
switch ($mode) {
case self::MODE_CUSTOM:
$crumbs->addTextCrumb(pht('Personal'));
break;
case self::MODE_GLOBAL:
$crumbs->addTextCrumb(pht('Global'));
break;
}
} else {
$crumbs->addTextCrumb(pht('Configure Menu'));
}
$edit_list = $this->loadItems($mode);
$content = $this->buildItemConfigureContent($edit_list);
}
break;
case 'reorder':
$mode = $this->getEditMode();
$edit_list = $this->loadItems($mode);
$content = $this->buildItemReorderContent($edit_list);
break;
case 'new':
$item_key = $request->getURIData('itemKey');
$mode = $this->getEditMode();
$content = $this->buildItemNewContent($item_key, $mode);
break;
case 'builtin':
$content = $this->buildItemBuiltinContent($selected_item);
break;
case 'hide':
$content = $this->buildItemHideContent($selected_item);
break;
case 'default':
if (!$this->isMenuEnginePinnable()) {
return new Aphront404Response();
}
$content = $this->buildItemDefaultContent(
$selected_item,
$item_list);
break;
case 'edit':
$content = $this->buildItemEditContent();
break;
default:
throw new Exception(
pht(
'Unsupported item action "%s".',
$item_action));
}
if ($content instanceof AphrontResponse) {
return $content;
}
if ($content instanceof AphrontResponseProducerInterface) {
return $content;
}
$crumbs->setBorder(true);
$page = $controller->newPage()
->setTitle($page_title)
->appendChild($content);
if (!$is_view || $this->getShowContentCrumbs()) {
$page->setCrumbs($crumbs);
}
if ($this->getShowNavigation()) {
$page->setNavigation($navigation);
}
if ($is_view) {
foreach ($this->pageClasses as $class) {
$page->addClass($class);
}
}
return $page;
}
- public function buildNavigation() {
+ public function buildNavigation(
+ PhabricatorProfileMenuItemConfiguration $selected_item = null) {
+
if ($this->navigation) {
return $this->navigation;
}
$nav = id(new AphrontSideNavFilterView())
->setIsProfileMenu(true)
->setBaseURI(new PhutilURI($this->getItemURI('')));
$menu_items = $this->getItems();
$filtered_items = array();
foreach ($menu_items as $menu_item) {
if ($menu_item->isDisabled()) {
continue;
}
$filtered_items[] = $menu_item;
}
$filtered_groups = mgroup($filtered_items, 'getMenuItemKey');
foreach ($filtered_groups as $group) {
$first_item = head($group);
$first_item->willBuildNavigationItems($group);
}
$has_items = false;
foreach ($menu_items as $menu_item) {
if ($menu_item->isDisabled()) {
continue;
}
$items = $menu_item->buildNavigationMenuItems();
foreach ($items as $item) {
$this->validateNavigationMenuItem($item);
}
// If the item produced only a single item which does not otherwise
// have a key, try to automatically assign it a reasonable key. This
// makes selecting the correct item simpler.
if (count($items) == 1) {
$item = head($items);
if ($item->getKey() === null) {
$default_key = $menu_item->getDefaultMenuItemKey();
$item->setKey($default_key);
}
}
foreach ($items as $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);
+ $navigation_items = $nav->getMenu()->getItems();
+ $select_key = $this->pickHighlightedMenuItem(
+ $navigation_items,
+ $selected_item);
+ $nav->selectFilter($select_key);
+
$this->navigation = $nav;
return $this->navigation;
}
private function getItems() {
if ($this->items === null) {
$this->items = $this->loadItems(self::MODE_COMBINED);
}
return $this->items;
}
private function loadItems($mode) {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
$items = $this->loadBuiltinProfileItems($mode);
$query = id(new PhabricatorProfileMenuItemConfigurationQuery())
->setViewer($viewer)
->withProfilePHIDs(array($object->getPHID()));
switch ($mode) {
case self::MODE_GLOBAL:
$query->withCustomPHIDs(array(), true);
break;
case self::MODE_CUSTOM:
$query->withCustomPHIDs(array($this->getCustomPHID()), false);
break;
case self::MODE_COMBINED:
$query->withCustomPHIDs(array($this->getCustomPHID()), true);
break;
}
$stored_items = $query->execute();
foreach ($stored_items as $stored_item) {
$impl = $stored_item->getMenuItem();
$impl->setViewer($viewer);
$impl->setEngine($this);
}
// Merge the stored items into the builtin items. If a builtin item has
// a stored version, replace the defaults with the stored changes.
foreach ($stored_items as $stored_item) {
if (!$stored_item->shouldEnableForObject($object)) {
continue;
}
$builtin_key = $stored_item->getBuiltinKey();
if ($builtin_key !== null) {
// If this builtin actually exists, replace the builtin with the
// stored configuration. Otherwise, we're just going to drop the
// stored config: it corresponds to an out-of-date or uninstalled
// item.
if (isset($items[$builtin_key])) {
$items[$builtin_key] = $stored_item;
} else {
continue;
}
} else {
$items[] = $stored_item;
}
}
return $this->arrangeItems($items, $mode);
}
private function loadBuiltinProfileItems($mode) {
$object = $this->getProfileObject();
switch ($mode) {
case self::MODE_GLOBAL:
$builtins = $this->getBuiltinProfileItems($object);
break;
case self::MODE_CUSTOM:
$builtins = $this->getBuiltinCustomProfileItems(
$object,
$this->getCustomPHID());
break;
case self::MODE_COMBINED:
$builtins = array();
$builtins[] = $this->getBuiltinCustomProfileItems(
$object,
$this->getCustomPHID());
$builtins[] = $this->getBuiltinProfileItems($object);
$builtins = array_mergev($builtins);
break;
}
$items = PhabricatorProfileMenuItem::getAllMenuItems();
$viewer = $this->getViewer();
$order = 1;
$map = array();
foreach ($builtins as $builtin) {
$builtin_key = $builtin->getBuiltinKey();
if (!$builtin_key) {
throw new Exception(
pht(
'Object produced a builtin item with no builtin item key! '.
'Builtin items must have a unique key.'));
}
if (isset($map[$builtin_key])) {
throw new Exception(
pht(
'Object produced two items with the same builtin key ("%s"). '.
'Each item must have a unique builtin key.',
$builtin_key));
}
$item_key = $builtin->getMenuItemKey();
$item = idx($items, $item_key);
if (!$item) {
throw new Exception(
pht(
'Builtin item ("%s") specifies a bad item key ("%s"); there '.
'is no corresponding item implementation available.',
$builtin_key,
$item_key));
}
$item = clone $item;
$item->setViewer($viewer);
$item->setEngine($this);
$builtin
->setProfilePHID($object->getPHID())
->attachMenuItem($item)
->attachProfileObject($object)
->setMenuItemOrder($order);
if (!$builtin->shouldEnableForObject($object)) {
continue;
}
$map[$builtin_key] = $builtin;
$order++;
}
return $map;
}
private function validateNavigationMenuItem($item) {
if (!($item instanceof PHUIListItemView)) {
throw new Exception(
pht(
'Expected buildNavigationMenuItems() to return a list of '.
'PHUIListItemView objects, but got a surprise.'));
}
}
public function getConfigureURI() {
$mode = $this->getEditMode();
switch ($mode) {
case self::MODE_CUSTOM:
return $this->getItemURI('configure/custom/');
case self::MODE_GLOBAL:
return $this->getItemURI('configure/global/');
}
return $this->getItemURI('configure/');
}
private function buildItemReorderContent(array $items) {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
// If you're reordering global items, you need to be able to edit the
// object the menu appears on. If you're reordering custom items, you only
// need to be able to edit the custom object. Currently, the custom object
// is always the viewing user's own user object.
$custom_phid = $this->getEditModeCustomPHID();
if (!$custom_phid) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
} else {
$policy_object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($custom_phid))
->executeOne();
if (!$policy_object) {
throw new Exception(
pht(
'Failed to load custom PHID "%s"!',
$custom_phid));
}
PhabricatorPolicyFilter::requireCapability(
$viewer,
$policy_object,
PhabricatorPolicyCapability::CAN_EDIT);
}
$controller = $this->getController();
$request = $controller->getRequest();
$request->validateCSRF();
$order = $request->getStrList('order');
$by_builtin = array();
$by_id = array();
foreach ($items as $key => $item) {
$id = $item->getID();
if ($id) {
$by_id[$id] = $key;
continue;
}
$builtin_key = $item->getBuiltinKey();
if ($builtin_key) {
$by_builtin[$builtin_key] = $key;
continue;
}
}
$key_order = array();
foreach ($order as $order_item) {
if (isset($by_id[$order_item])) {
$key_order[] = $by_id[$order_item];
continue;
}
if (isset($by_builtin[$order_item])) {
$key_order[] = $by_builtin[$order_item];
continue;
}
}
$items = array_select_keys($items, $key_order) + $items;
$type_order =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_ORDER;
$order = 1;
foreach ($items as $item) {
$xactions = array();
$xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setTransactionType($type_order)
->setNewValue($order);
$editor = id(new PhabricatorProfileMenuEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($item, $xactions);
$order++;
}
return id(new AphrontRedirectResponse())
->setURI($this->getConfigureURI());
}
protected function buildItemViewContent(
PhabricatorProfileMenuItemConfiguration $item) {
return $item->newPageContent();
}
private function getViewerEditModes() {
$modes = array();
$viewer = $this->getViewer();
if ($viewer->isLoggedIn() && $this->isMenuEnginePersonalizable()) {
$modes[] = self::MODE_CUSTOM;
}
$object = $this->getProfileObject();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$modes[] = self::MODE_GLOBAL;
}
return $modes;
}
protected function isMenuEnginePersonalizable() {
return true;
}
/**
* Does this engine support pinning items?
*
* Personalizable menus disable pinning by default since it creates a number
* of weird edge cases without providing many benefits for current menus.
*
* @return bool True if items may be pinned as default items.
*/
protected function isMenuEnginePinnable() {
return !$this->isMenuEnginePersonalizable();
}
private function buildMenuEditModeContent() {
$viewer = $this->getViewer();
$modes = $this->getViewerEditModes();
if (!$modes) {
return new Aphront404Response();
}
if (count($modes) == 1) {
$mode = head($modes);
return id(new AphrontRedirectResponse())
->setURI($this->getItemURI("configure/{$mode}/"));
}
$menu = id(new PHUIObjectItemListView())
->setUser($viewer);
$modes = array_fuse($modes);
if (isset($modes['custom'])) {
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Personal Menu Items'))
->setHref($this->getItemURI('configure/custom/'))
->setImageURI($viewer->getProfileImageURI())
->addAttribute(pht('Edit the menu for your personal account.')));
}
if (isset($modes['global'])) {
$icon = id(new PHUIIconView())
->setIcon('fa-globe')
->setBackground('bg-blue');
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Global Menu Items'))
->setHref($this->getItemURI('configure/global/'))
->setImageIcon($icon)
->addAttribute(pht('Edit the global default menu for all users.')));
}
$box = id(new PHUIObjectBoxView())
->setObjectList($menu);
$header = id(new PHUIHeaderView())
->setHeader(pht('Manage Menu'))
->setHeaderIcon('fa-list');
return id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
}
private function buildItemConfigureContent(array $items) {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
$filtered_groups = mgroup($items, 'getMenuItemKey');
foreach ($filtered_groups as $group) {
$first_item = head($group);
$first_item->willBuildNavigationItems($group);
}
// Users only need to be able to edit the object which this menu appears
// on if they're editing global menu items. For example, users do not need
// to be able to edit the Favorites application to add new items to the
// Favorites menu.
if (!$this->getCustomPHID()) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
$list_id = celerity_generate_unique_node_id();
$mode = $this->getEditMode();
Javelin::initBehavior(
'reorder-profile-menu-items',
array(
'listID' => $list_id,
'orderURI' => $this->getItemURI("reorder/{$mode}/"),
));
$list = id(new PHUIObjectItemListView())
->setID($list_id)
->setNoDataString(pht('This menu currently has no items.'));
foreach ($items as $item) {
$id = $item->getID();
$builtin_key = $item->getBuiltinKey();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$item,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PHUIObjectItemView());
$name = $item->getDisplayName();
$type = $item->getMenuItemTypeName();
if (!strlen(trim($name))) {
$name = pht('Untitled "%s" Item', $type);
}
$view->setHeader($name);
$view->addAttribute($type);
if ($can_edit) {
$view
->setGrippable(true)
->addSigil('profile-menu-item')
->setMetadata(
array(
'key' => nonempty($id, $builtin_key),
));
if ($id) {
$default_uri = $this->getItemURI("default/{$id}/");
} else {
$default_uri = $this->getItemURI("default/{$builtin_key}/");
}
$default_text = null;
if ($this->isMenuEnginePinnable()) {
if ($item->isDefault()) {
$default_icon = 'fa-thumb-tack green';
$default_text = pht('Current Default');
} else if ($item->canMakeDefault()) {
$default_icon = 'fa-thumb-tack';
$default_text = pht('Make Default');
}
}
if ($default_text !== null) {
$view->addAction(
id(new PHUIListItemView())
->setHref($default_uri)
->setWorkflow(true)
->setName($default_text)
->setIcon($default_icon));
}
if ($id) {
$view->setHref($this->getItemURI("edit/{$id}/"));
$hide_uri = $this->getItemURI("hide/{$id}/");
} else {
$view->setHref($this->getItemURI("builtin/{$builtin_key}/"));
$hide_uri = $this->getItemURI("hide/{$builtin_key}/");
}
if ($item->isDisabled()) {
$hide_icon = 'fa-plus';
$hide_text = pht('Enable');
} else if ($item->getBuiltinKey() !== null) {
$hide_icon = 'fa-times';
$hide_text = pht('Disable');
} else {
$hide_icon = 'fa-times';
$hide_text = pht('Delete');
}
$can_disable = $item->canHideMenuItem();
$view->addAction(
id(new PHUIListItemView())
->setHref($hide_uri)
->setWorkflow(true)
->setDisabled(!$can_disable)
->setName($hide_text)
->setIcon($hide_icon));
}
if ($item->isDisabled()) {
$view->setDisabled(true);
}
$list->addItem($view);
}
$item_types = PhabricatorProfileMenuItem::getAllMenuItems();
$object = $this->getProfileObject();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
// See T12167. This makes the "Actions" dropdown button show up in the
// page header.
$action_list->setID(celerity_generate_unique_node_id());
$action_list->addAction(
id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Add New Menu Item...')));
foreach ($item_types as $item_type) {
if (!$item_type->canAddToObject($object)) {
continue;
}
$item_key = $item_type->getMenuItemKey();
$edit_mode = $this->getEditMode();
$action_list->addAction(
id(new PhabricatorActionView())
->setIcon($item_type->getMenuItemTypeIcon())
->setName($item_type->getMenuItemTypeName())
->setHref($this->getItemURI("new/{$edit_mode}/{$item_key}/"))
->setWorkflow(true));
}
$action_list->addAction(
id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation')));
$doc_link = PhabricatorEnv::getDoclink('Profile Menu User Guide');
$doc_name = pht('Profile Menu User Guide');
$action_list->addAction(
id(new PhabricatorActionView())
->setIcon('fa-book')
->setHref($doc_link)
->setName($doc_name));
$header = id(new PHUIHeaderView())
->setHeader(pht('Menu Items'))
->setHeaderIcon('fa-list');
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Current Menu Items'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
$curtain = id(new PHUICurtainView())
->setViewer($viewer)
->setActionList($action_list);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$box,
));
return $view;
}
private function buildItemNewContent($item_key, $mode) {
$item_types = PhabricatorProfileMenuItem::getAllMenuItems();
$item_type = idx($item_types, $item_key);
if (!$item_type) {
return new Aphront404Response();
}
$object = $this->getProfileObject();
if (!$item_type->canAddToObject($object)) {
return new Aphront404Response();
}
$custom_phid = $this->getEditModeCustomPHID();
$configuration = PhabricatorProfileMenuItemConfiguration::initializeNewItem(
$object,
$item_type,
$custom_phid);
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
$controller = $this->getController();
return id(new PhabricatorProfileMenuEditEngine())
->setMenuEngine($this)
->setProfileObject($object)
->setNewMenuItemConfiguration($configuration)
->setCustomPHID($custom_phid)
->setController($controller)
->buildResponse();
}
private function buildItemEditContent() {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
$controller = $this->getController();
$custom_phid = $this->getEditModeCustomPHID();
return id(new PhabricatorProfileMenuEditEngine())
->setMenuEngine($this)
->setProfileObject($object)
->setController($controller)
->setCustomPHID($custom_phid)
->buildResponse();
}
private function buildItemBuiltinContent(
PhabricatorProfileMenuItemConfiguration $configuration) {
// If this builtin item has already been persisted, redirect to the
// edit page.
$id = $configuration->getID();
if ($id) {
return id(new AphrontRedirectResponse())
->setURI($this->getItemURI("edit/{$id}/"));
}
// Otherwise, act like we're creating a new item, we're just starting
// with the builtin template.
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
$object = $this->getProfileObject();
$controller = $this->getController();
$custom_phid = $this->getEditModeCustomPHID();
return id(new PhabricatorProfileMenuEditEngine())
->setIsBuiltin(true)
->setMenuEngine($this)
->setProfileObject($object)
->setNewMenuItemConfiguration($configuration)
->setController($controller)
->setCustomPHID($custom_phid)
->buildResponse();
}
private function buildItemHideContent(
PhabricatorProfileMenuItemConfiguration $configuration) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$configuration->canHideMenuItem()) {
return $controller->newDialog()
->setTitle(pht('Mandatory Item'))
->appendParagraph(
pht('This menu item is very important, and can not be disabled.'))
->addCancelButton($this->getConfigureURI());
}
if ($configuration->getBuiltinKey() === null) {
$new_value = null;
$title = pht('Delete Menu Item');
$body = pht('Delete this menu item?');
$button = pht('Delete Menu Item');
} else if ($configuration->isDisabled()) {
$new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE;
$title = pht('Enable Menu Item');
$body = pht(
'Enable this menu item? It will appear in the menu again.');
$button = pht('Enable Menu Item');
} else {
$new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DISABLED;
$title = pht('Disable Menu Item');
$body = pht(
'Disable this menu item? It will no longer appear in the menu, but '.
'you can re-enable it later.');
$button = pht('Disable Menu Item');
}
$v_visibility = $configuration->getVisibility();
if ($request->isFormPost()) {
if ($new_value === null) {
$configuration->delete();
} else {
$type_visibility =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY;
$xactions = array();
$xactions[] =
id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setTransactionType($type_visibility)
->setNewValue($new_value);
$editor = id(new PhabricatorProfileMenuEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($configuration, $xactions);
}
return id(new AphrontRedirectResponse())
->setURI($this->getConfigureURI());
}
return $controller->newDialog()
->setTitle($title)
->appendParagraph($body)
->addCancelButton($this->getConfigureURI())
->addSubmitButton($button);
}
private function buildItemDefaultContent(
PhabricatorProfileMenuItemConfiguration $configuration,
array $items) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
$done_uri = $this->getConfigureURI();
if (!$configuration->canMakeDefault()) {
return $controller->newDialog()
->setTitle(pht('Not Defaultable'))
->appendParagraph(
pht(
'This item can not be set as the default item. This is usually '.
'because the item has no page of its own, or links to an '.
'external page.'))
->addCancelButton($done_uri);
}
if ($configuration->isDefault()) {
return $controller->newDialog()
->setTitle(pht('Already Default'))
->appendParagraph(
pht(
'This item is already set as the default item for this menu.'))
->addCancelButton($done_uri);
}
if ($request->isFormPost()) {
$key = $configuration->getID();
if (!$key) {
$key = $configuration->getBuiltinKey();
}
$this->adjustDefault($key);
return id(new AphrontRedirectResponse())
->setURI($done_uri);
}
return $controller->newDialog()
->setTitle(pht('Make Default'))
->appendParagraph(
pht(
'Set this item as the default for this menu? Users arriving on '.
'this page will be shown the content of this item by default.'))
->addCancelButton($done_uri)
->addSubmitButton(pht('Make Default'));
}
protected function newItem() {
return PhabricatorProfileMenuItemConfiguration::initializeNewBuiltin();
}
protected function newManageItem() {
return $this->newItem()
->setBuiltinKey(self::ITEM_MANAGE)
->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY);
}
public function adjustDefault($key) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $request->getViewer();
$items = $this->loadItems(self::MODE_COMBINED);
// To adjust the default item, we first change any existing items that
// are marked as defaults to "visible", then make the new default item
// the default.
$default = array();
$visible = array();
foreach ($items as $item) {
$builtin_key = $item->getBuiltinKey();
$id = $item->getID();
$is_target =
(($builtin_key !== null) && ($builtin_key === $key)) ||
(($id !== null) && ((int)$id === (int)$key));
if ($is_target) {
if (!$item->isDefault()) {
$default[] = $item;
}
} else {
if ($item->isDefault()) {
$visible[] = $item;
}
}
}
$type_visibility =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY;
$v_visible = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE;
$v_default = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT;
$apply = array(
array($v_visible, $visible),
array($v_default, $default),
);
foreach ($apply as $group) {
list($value, $items) = $group;
foreach ($items as $item) {
$xactions = array();
$xactions[] =
id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setTransactionType($type_visibility)
->setNewValue($value);
$editor = id(new PhabricatorProfileMenuEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($item, $xactions);
}
}
return $this;
}
private function arrangeItems(array $items, $mode) {
// Sort the items.
$items = msortv($items, 'getSortVector');
$object = $this->getProfileObject();
// If we have some global items and some custom items and are in "combined"
// mode, put a hard-coded divider item between them.
if ($mode == self::MODE_COMBINED) {
$list = array();
$seen_custom = false;
$seen_global = false;
foreach ($items as $item) {
if ($item->getCustomPHID()) {
$seen_custom = true;
} else {
if ($seen_custom && !$seen_global) {
$list[] = $this->newItem()
->setBuiltinKey(self::ITEM_CUSTOM_DIVIDER)
->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY)
->attachProfileObject($object)
->attachMenuItem(
new PhabricatorDividerProfileMenuItem());
}
$seen_global = true;
}
$list[] = $item;
}
$items = $list;
}
// Normalize keys since callers shouldn't rely on this array being
// partially keyed.
$items = array_values($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.'));
}
private function pickDefaultItem(array $items) {
// Remove all the items which can not be the default item.
foreach ($items as $key => $item) {
if (!$item->canMakeDefault()) {
unset($items[$key]);
continue;
}
if ($item->isDisabled()) {
unset($items[$key]);
continue;
}
}
// If this engine supports pinning items and a valid item is pinned,
// pick that item as the default.
if ($this->isMenuEnginePinnable()) {
foreach ($items as $key => $item) {
if ($item->isDefault()) {
return $item;
}
}
}
// If we have some other valid items, pick the first one as the default.
if ($items) {
return head($items);
}
return null;
}
private function pickSelectedItem(array $items, $item_id, $is_view) {
if (strlen($item_id)) {
$item_id_int = (int)$item_id;
foreach ($items as $item) {
if ($item_id_int) {
if ((int)$item->getID() === $item_id_int) {
return $item;
}
}
$builtin_key = $item->getBuiltinKey();
if ($builtin_key === (string)$item_id) {
return $item;
}
}
// Nothing matches the selected item ID, so we don't have a valid
// selection.
return null;
}
if ($is_view) {
return $this->pickDefaultItem($items);
}
return null;
}
+ private function pickHighlightedMenuItem(
+ array $items,
+ PhabricatorProfileMenuItemConfiguration $selected_item = null) {
+
+ assert_instances_of($items, 'PHUIListItemView');
+
+ $default_key = null;
+ if ($selected_item) {
+ $default_key = $selected_item->getDefaultMenuItemKey();
+ }
+
+ $controller = $this->getController();
+
+ // In some rare cases, when like building the "Favorites" menu on a
+ // 404 page, we may not have a controller. Just accept whatever default
+ // behavior we'd otherwise end up with.
+ if (!$controller) {
+ return $default_key;
+ }
+
+ $request = $controller->getRequest();
+
+ // See T12949. If one of the menu items is a link to the same URI that
+ // the page was accessed with, we want to highlight that item. For example,
+ // this allows you to add links to a menu that apply filters to a
+ // workboard.
+
+ $matches = array();
+ foreach ($items as $item) {
+ $href = $item->getHref();
+ if ($this->isMatchForRequestURI($request, $href)) {
+ $matches[] = $item;
+ }
+ }
+
+ foreach ($matches as $match) {
+ if ($match->getKey() === $default_key) {
+ return $default_key;
+ }
+ }
+
+ if ($matches) {
+ return head($matches)->getKey();
+ }
+
+ return $default_key;
+ }
+
+ private function isMatchForRequestURI(AphrontRequest $request, $item_uri) {
+ $request_uri = $request->getAbsoluteRequestURI();
+ $item_uri = new PhutilURI($item_uri);
+
+ // If the request URI and item URI don't have matching paths, they
+ // do not match.
+ if ($request_uri->getPath() !== $item_uri->getPath()) {
+ return false;
+ }
+
+ // If the request URI and item URI don't have matching parameters, they
+ // also do not match. We're specifically trying to let "?filter=X" work
+ // on Workboards, among other use cases, so this is important.
+ $request_params = $request_uri->getQueryParamsAsPairList();
+ $item_params = $item_uri->getQueryParamsAsPairList();
+ if ($request_params !== $item_params) {
+ return false;
+ }
+
+ // If the paths and parameters match, the item domain must be: empty; or
+ // match the request domain; or match the production domain.
+
+ $request_domain = $request_uri->getDomain();
+
+ $production_uri = PhabricatorEnv::getProductionURI('/');
+ $production_domain = id(new PhutilURI($production_uri))
+ ->getDomain();
+
+ $allowed_domains = array(
+ '',
+ $request_domain,
+ $production_domain,
+ );
+ $allowed_domains = array_fuse($allowed_domains);
+
+ $item_domain = $item_uri->getDomain();
+ $item_domain = (string)$item_domain;
+
+ if (isset($allowed_domains[$item_domain])) {
+ return true;
+ }
+
+ return false;
+ }
+
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Mar 17, 1:12 AM (19 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
963997
Default Alt Text
(100 KB)

Event Timeline