2014-03-26 22:40:47 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
final class PhabricatorProjectBoardViewController
|
|
|
|
extends PhabricatorProjectBoardController {
|
|
|
|
|
Add a "Batch Edit Tasks..." action to workboard columns
Summary:
Ref T5523. Adds a new workflow to make some kinds of bulk workboard operations easier.
New dropdown action:
{F376848}
This brings you into the existing bulk edit flow:
{F376849}
When you save an edit, you're taken back to the board:
{F376850}
If you try to edit a column with nothing in it, you get an error:
{F376851}
Note that the selected workboard filter is applied before choosing tasks, so if your filter is set to "open tasks" we only batch edit the open (i.e., currently visible) tasks in the column. I think this is more powerful (it lets you use filtering to select task subsets) but might not be completely obvious in all cases (although I do think it's more obvious than the alternative rule -- just an issue of neither rule being completely obvious).
Test Plan:
- Batch edited tasks in a column.
- Used "Batch Edit Tasks..." to move tasks to a different workboard by removing + adding a project.
- Batch edited a column with filtered-out tasks, verified only visible tasks were edited.
- Batch edited a column with no visible tasks, received error.
- Used the batch editor normally (Maniphest -> Maniphest, no boards).
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: johnny-bit, cburroughs, epriestley
Projects: #prioritized
Maniphest Tasks: T5523
Differential Revision: https://secure.phabricator.com/D12475
2015-04-20 19:05:08 +02:00
|
|
|
const BATCH_EDIT_ALL = 'all';
|
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
private $id;
|
2014-06-13 23:09:21 +02:00
|
|
|
private $slug;
|
2014-03-26 22:40:47 +01:00
|
|
|
private $handles;
|
2014-05-20 20:42:05 +02:00
|
|
|
private $queryKey;
|
|
|
|
private $filter;
|
2014-08-08 17:10:29 +02:00
|
|
|
private $sortKey;
|
|
|
|
private $showHidden;
|
2014-03-26 22:40:47 +01:00
|
|
|
|
|
|
|
public function shouldAllowPublic() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-01-12 19:04:01 +01:00
|
|
|
public function handleRequest(AphrontRequest $request) {
|
2014-03-26 22:40:47 +01:00
|
|
|
$viewer = $request->getUser();
|
2015-01-12 19:04:01 +01:00
|
|
|
$id = $request->getURIData('id');
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-06-25 21:30:20 +02:00
|
|
|
$show_hidden = $request->getBool('hidden');
|
2014-08-08 17:10:29 +02:00
|
|
|
$this->showHidden = $show_hidden;
|
2014-06-25 21:30:20 +02:00
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
$project = id(new PhabricatorProjectQuery())
|
|
|
|
->setViewer($viewer)
|
2014-06-13 23:09:21 +02:00
|
|
|
->needImages(true);
|
2015-01-12 19:04:01 +01:00
|
|
|
$id = $request->getURIData('id');
|
|
|
|
$slug = $request->getURIData('slug');
|
|
|
|
if ($slug) {
|
|
|
|
$project->withSlugs(array($slug));
|
2014-06-13 23:09:21 +02:00
|
|
|
} else {
|
2015-01-12 19:04:01 +01:00
|
|
|
$project->withIDs(array($id));
|
2014-06-13 23:09:21 +02:00
|
|
|
}
|
|
|
|
$project = $project->executeOne();
|
2014-03-26 22:40:47 +01:00
|
|
|
if (!$project) {
|
|
|
|
return new Aphront404Response();
|
|
|
|
}
|
2014-06-13 23:09:21 +02:00
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
$this->setProject($project);
|
2014-08-01 20:06:42 +02:00
|
|
|
$this->id = $project->getID();
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-08-08 17:10:29 +02:00
|
|
|
$sort_key = $request->getStr('order');
|
|
|
|
switch ($sort_key) {
|
|
|
|
case PhabricatorProjectColumn::ORDER_NATURAL:
|
|
|
|
case PhabricatorProjectColumn::ORDER_PRIORITY:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$sort_key = PhabricatorProjectColumn::DEFAULT_ORDER;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
$this->sortKey = $sort_key;
|
|
|
|
|
2014-06-25 21:30:20 +02:00
|
|
|
$column_query = id(new PhabricatorProjectColumnQuery())
|
2014-03-26 22:40:47 +01:00
|
|
|
->setViewer($viewer)
|
2014-06-25 21:30:20 +02:00
|
|
|
->withProjectPHIDs(array($project->getPHID()));
|
|
|
|
if (!$show_hidden) {
|
|
|
|
$column_query->withStatuses(
|
|
|
|
array(PhabricatorProjectColumn::STATUS_ACTIVE));
|
|
|
|
}
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-06-25 21:30:20 +02:00
|
|
|
$columns = $column_query->execute();
|
2014-03-26 22:40:47 +01:00
|
|
|
$columns = mpull($columns, null, 'getSequence');
|
|
|
|
|
2014-10-07 23:49:22 +02:00
|
|
|
// TODO: Expand the checks here if we add the ability
|
|
|
|
// to hide the Backlog column
|
|
|
|
if (!$columns) {
|
2014-08-05 22:40:41 +02:00
|
|
|
switch ($request->getStr('initialize-type')) {
|
|
|
|
case 'backlog-only':
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$column = PhabricatorProjectColumn::initializeNewColumn($viewer)
|
|
|
|
->setSequence(0)
|
2014-08-09 00:50:36 +02:00
|
|
|
->setProperty('isDefault', true)
|
2014-08-05 22:40:41 +02:00
|
|
|
->setProjectPHID($project->getPHID())
|
|
|
|
->save();
|
|
|
|
$column->attachProject($project);
|
|
|
|
$columns[0] = $column;
|
|
|
|
unset($unguarded);
|
|
|
|
break;
|
|
|
|
case 'import':
|
|
|
|
return id(new AphrontRedirectResponse())
|
|
|
|
->setURI(
|
|
|
|
$this->getApplicationURI('board/'.$project->getID().'/import/'));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return $this->initializeWorkboardDialog($project);
|
|
|
|
break;
|
|
|
|
}
|
2014-03-26 22:40:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ksort($columns);
|
|
|
|
|
2014-05-20 20:42:05 +02:00
|
|
|
$board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
|
|
|
|
|
|
|
|
$engine = id(new ManiphestTaskSearchEngine())
|
2014-03-26 22:40:47 +01:00
|
|
|
->setViewer($viewer)
|
2014-05-20 20:42:05 +02:00
|
|
|
->setBaseURI($board_uri)
|
|
|
|
->setIsBoardView(true);
|
|
|
|
|
|
|
|
if ($request->isFormPost()) {
|
|
|
|
$saved = $engine->buildSavedQueryFromRequest($request);
|
|
|
|
$engine->saveQuery($saved);
|
2015-02-17 20:06:21 +01:00
|
|
|
$filter_form = id(new AphrontFormView())
|
|
|
|
->setUser($viewer);
|
|
|
|
$engine->buildSearchForm($filter_form, $saved);
|
|
|
|
if ($engine->getErrors()) {
|
|
|
|
return $this->newDialog()
|
|
|
|
->setWidth(AphrontDialogView::WIDTH_FULL)
|
|
|
|
->setTitle(pht('Advanced Filter'))
|
|
|
|
->appendChild($filter_form->buildLayoutView())
|
|
|
|
->setErrors($engine->getErrors())
|
|
|
|
->setSubmitURI($board_uri)
|
|
|
|
->addSubmitButton(pht('Apply Filter'))
|
|
|
|
->addCancelButton($board_uri);
|
|
|
|
}
|
2014-05-20 20:42:05 +02:00
|
|
|
return id(new AphrontRedirectResponse())->setURI(
|
2014-08-08 17:10:29 +02:00
|
|
|
$this->getURIWithState(
|
|
|
|
$engine->getQueryResultsPageURI($saved->getQueryKey())));
|
2014-05-20 20:42:05 +02:00
|
|
|
}
|
|
|
|
|
2015-01-12 19:04:01 +01:00
|
|
|
$query_key = $request->getURIData('queryKey');
|
2014-05-20 20:42:05 +02:00
|
|
|
if (!$query_key) {
|
|
|
|
$query_key = 'open';
|
|
|
|
}
|
2014-08-08 17:10:29 +02:00
|
|
|
$this->queryKey = $query_key;
|
2014-05-20 20:42:05 +02:00
|
|
|
|
|
|
|
$custom_query = null;
|
|
|
|
if ($engine->isBuiltinQuery($query_key)) {
|
|
|
|
$saved = $engine->buildSavedQueryFromBuiltin($query_key);
|
|
|
|
} else {
|
|
|
|
$saved = id(new PhabricatorSavedQueryQuery())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->withQueryKeys(array($query_key))
|
|
|
|
->executeOne();
|
|
|
|
|
|
|
|
if (!$saved) {
|
|
|
|
return new Aphront404Response();
|
|
|
|
}
|
|
|
|
|
|
|
|
$custom_query = $saved;
|
|
|
|
}
|
|
|
|
|
2015-01-12 19:04:01 +01:00
|
|
|
if ($request->getURIData('filter')) {
|
2014-05-20 20:42:05 +02:00
|
|
|
$filter_form = id(new AphrontFormView())
|
|
|
|
->setUser($viewer);
|
|
|
|
$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 = $engine->buildQueryFromSavedQuery($saved);
|
|
|
|
|
|
|
|
$tasks = $task_query
|
2015-04-23 13:10:39 +02:00
|
|
|
->withEdgeLogicPHIDs(
|
|
|
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
|
|
|
PhabricatorQueryConstraint::OPERATOR_AND,
|
|
|
|
array($project->getPHID()))
|
2014-03-26 22:40:47 +01:00
|
|
|
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
|
2014-05-20 20:42:05 +02:00
|
|
|
->setViewer($viewer)
|
2014-03-26 22:40:47 +01:00
|
|
|
->execute();
|
|
|
|
$tasks = mpull($tasks, null, 'getPHID');
|
2014-08-07 00:09:09 +02:00
|
|
|
|
|
|
|
if ($tasks) {
|
|
|
|
$positions = id(new PhabricatorProjectColumnPositionQuery())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->withObjectPHIDs(mpull($tasks, 'getPHID'))
|
|
|
|
->withColumns($columns)
|
|
|
|
->execute();
|
|
|
|
$positions = mpull($positions, null, 'getObjectPHID');
|
|
|
|
} else {
|
|
|
|
$positions = array();
|
2014-03-26 22:40:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$task_map = array();
|
|
|
|
foreach ($tasks as $task) {
|
|
|
|
$task_phid = $task->getPHID();
|
2014-08-09 01:22:11 +02:00
|
|
|
if (empty($positions[$task_phid])) {
|
|
|
|
// This shouldn't normally be possible because we create positions on
|
|
|
|
// demand, but we might have raced as an object was removed from the
|
|
|
|
// board. Just drop the task if we don't have a position for it.
|
|
|
|
continue;
|
2014-08-07 00:09:09 +02:00
|
|
|
}
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-08-09 01:22:11 +02:00
|
|
|
$position = $positions[$task_phid];
|
|
|
|
$task_map[$position->getColumnPHID()][] = $task_phid;
|
2014-03-26 22:40:47 +01:00
|
|
|
}
|
|
|
|
|
2014-08-08 17:11:00 +02:00
|
|
|
// If we're showing the board in "natural" order, sort columns by their
|
|
|
|
// column positions.
|
|
|
|
if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) {
|
|
|
|
foreach ($task_map as $column_phid => $task_phids) {
|
|
|
|
$order = array();
|
|
|
|
foreach ($task_phids as $task_phid) {
|
|
|
|
if (isset($positions[$task_phid])) {
|
|
|
|
$order[$task_phid] = $positions[$task_phid]->getOrderingKey();
|
|
|
|
} else {
|
|
|
|
$order[$task_phid] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
asort($order);
|
|
|
|
$task_map[$column_phid] = array_keys($order);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
$task_can_edit_map = id(new PhabricatorPolicyFilter())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
|
|
|
|
->apply($tasks);
|
|
|
|
|
Add a "Batch Edit Tasks..." action to workboard columns
Summary:
Ref T5523. Adds a new workflow to make some kinds of bulk workboard operations easier.
New dropdown action:
{F376848}
This brings you into the existing bulk edit flow:
{F376849}
When you save an edit, you're taken back to the board:
{F376850}
If you try to edit a column with nothing in it, you get an error:
{F376851}
Note that the selected workboard filter is applied before choosing tasks, so if your filter is set to "open tasks" we only batch edit the open (i.e., currently visible) tasks in the column. I think this is more powerful (it lets you use filtering to select task subsets) but might not be completely obvious in all cases (although I do think it's more obvious than the alternative rule -- just an issue of neither rule being completely obvious).
Test Plan:
- Batch edited tasks in a column.
- Used "Batch Edit Tasks..." to move tasks to a different workboard by removing + adding a project.
- Batch edited a column with filtered-out tasks, verified only visible tasks were edited.
- Batch edited a column with no visible tasks, received error.
- Used the batch editor normally (Maniphest -> Maniphest, no boards).
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: johnny-bit, cburroughs, epriestley
Projects: #prioritized
Maniphest Tasks: T5523
Differential Revision: https://secure.phabricator.com/D12475
2015-04-20 19:05:08 +02:00
|
|
|
// 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 = idx($task_map, $batch_column->getPHID(), array());
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
$batch_ids = mpull($batch_tasks, 'getID');
|
|
|
|
$batch_ids = implode(',', $batch_ids);
|
|
|
|
|
|
|
|
$batch_uri = new PhutilURI('/maniphest/batch/');
|
|
|
|
$batch_uri->setQueryParam('board', $this->id);
|
|
|
|
$batch_uri->setQueryParam('batch', $batch_ids);
|
|
|
|
return id(new AphrontRedirectResponse())
|
|
|
|
->setURI($batch_uri);
|
|
|
|
}
|
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
$board_id = celerity_generate_unique_node_id();
|
|
|
|
|
|
|
|
$board = id(new PHUIWorkboardView())
|
|
|
|
->setUser($viewer)
|
|
|
|
->setID($board_id);
|
|
|
|
|
|
|
|
$this->initBehavior(
|
|
|
|
'project-boards',
|
|
|
|
array(
|
|
|
|
'boardID' => $board_id,
|
|
|
|
'projectPHID' => $project->getPHID(),
|
|
|
|
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
|
|
|
|
'createURI' => '/maniphest/task/create/',
|
2014-08-08 17:10:29 +02:00
|
|
|
'order' => $this->sortKey,
|
2014-03-26 22:40:47 +01:00
|
|
|
));
|
|
|
|
|
|
|
|
$this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
|
|
|
|
|
|
|
|
foreach ($columns as $column) {
|
2014-08-15 18:28:08 +02:00
|
|
|
$task_phids = idx($task_map, $column->getPHID(), array());
|
|
|
|
$column_tasks = array_select_keys($tasks, $task_phids);
|
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
$panel = id(new PHUIWorkpanelView())
|
|
|
|
->setHeader($column->getDisplayName())
|
2014-11-04 20:11:15 +01:00
|
|
|
->setSubHeader($column->getDisplayType())
|
Allow columns to have a point limit
Summary:
Fixes T5885. This implements optional soft point limits for workboard columns, per traditional Kanban.
- Allow columns to have a point limit set.
- When a column has a point limit, show it in the header.
- If a column has too many points in it, show the column and point count in red.
@chad, this could probably use some design tweaks. In particular:
- I changed the color of "hidden" columns to avoid confusion with "overfull" columns. We might be able to find a better color.
- UI hints for overfull columns might need adjustment.
(After T4427, we'll let you sum some custom field instead of total number of tasks, which is why this is called "points" rather than "number of tasks".)
Test Plan:
{F190914}
Note that:
- "Pre-planning" has a limit, so it shows "4/12".
- "Planning" has a limit and is overfull, so it shows "5 / 4".
- Other columns do not have limits.
- "Post-planning" is a hidden column. This might be too muted now.
Transactions:
{F190915}
Error messages / edit screen:
{F190916}
Reviewers: btrahan, chad
Reviewed By: btrahan
Subscribers: chad, epriestley
Maniphest Tasks: T5885
Differential Revision: https://secure.phabricator.com/D10276
2014-08-15 20:16:08 +02:00
|
|
|
->addSigil('workpanel');
|
|
|
|
|
|
|
|
$header_icon = $column->getHeaderIcon();
|
|
|
|
if ($header_icon) {
|
|
|
|
$panel->setHeaderIcon($header_icon);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($column->isHidden()) {
|
|
|
|
$panel->addClass('project-panel-hidden');
|
|
|
|
}
|
2014-06-25 21:30:20 +02:00
|
|
|
|
2014-08-08 19:35:51 +02:00
|
|
|
$column_menu = $this->buildColumnMenu($project, $column);
|
|
|
|
$panel->addHeaderAction($column_menu);
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-08-15 18:28:08 +02:00
|
|
|
$tag_id = celerity_generate_unique_node_id();
|
|
|
|
$tag_content_id = celerity_generate_unique_node_id();
|
|
|
|
|
|
|
|
$count_tag = id(new PHUITagView())
|
|
|
|
->setType(PHUITagView::TYPE_SHADE)
|
|
|
|
->setShade(PHUITagView::COLOR_BLUE)
|
|
|
|
->setID($tag_id)
|
|
|
|
->setName(phutil_tag('span', array('id' => $tag_content_id), '-'))
|
|
|
|
->setStyle('display: none');
|
|
|
|
|
|
|
|
$panel->setHeaderTag($count_tag);
|
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
$cards = id(new PHUIObjectItemListView())
|
|
|
|
->setUser($viewer)
|
|
|
|
->setFlush(true)
|
|
|
|
->setAllowEmptyList(true)
|
|
|
|
->addSigil('project-column')
|
|
|
|
->setMetadata(
|
|
|
|
array(
|
|
|
|
'columnPHID' => $column->getPHID(),
|
2014-08-15 18:28:08 +02:00
|
|
|
'countTagID' => $tag_id,
|
|
|
|
'countTagContentID' => $tag_content_id,
|
Allow columns to have a point limit
Summary:
Fixes T5885. This implements optional soft point limits for workboard columns, per traditional Kanban.
- Allow columns to have a point limit set.
- When a column has a point limit, show it in the header.
- If a column has too many points in it, show the column and point count in red.
@chad, this could probably use some design tweaks. In particular:
- I changed the color of "hidden" columns to avoid confusion with "overfull" columns. We might be able to find a better color.
- UI hints for overfull columns might need adjustment.
(After T4427, we'll let you sum some custom field instead of total number of tasks, which is why this is called "points" rather than "number of tasks".)
Test Plan:
{F190914}
Note that:
- "Pre-planning" has a limit, so it shows "4/12".
- "Planning" has a limit and is overfull, so it shows "5 / 4".
- Other columns do not have limits.
- "Post-planning" is a hidden column. This might be too muted now.
Transactions:
{F190915}
Error messages / edit screen:
{F190916}
Reviewers: btrahan, chad
Reviewed By: btrahan
Subscribers: chad, epriestley
Maniphest Tasks: T5885
Differential Revision: https://secure.phabricator.com/D10276
2014-08-15 20:16:08 +02:00
|
|
|
'pointLimit' => $column->getPointLimit(),
|
2014-03-26 22:40:47 +01:00
|
|
|
));
|
2014-06-25 21:30:20 +02:00
|
|
|
|
2014-08-15 18:28:08 +02:00
|
|
|
foreach ($column_tasks as $task) {
|
2014-03-26 22:40:47 +01:00
|
|
|
$owner = null;
|
|
|
|
if ($task->getOwnerPHID()) {
|
|
|
|
$owner = $this->handles[$task->getOwnerPHID()];
|
|
|
|
}
|
|
|
|
$can_edit = idx($task_can_edit_map, $task->getPHID(), false);
|
|
|
|
$cards->addItem(id(new ProjectBoardTaskCard())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->setTask($task)
|
|
|
|
->setOwner($owner)
|
|
|
|
->setCanEdit($can_edit)
|
|
|
|
->getItem());
|
|
|
|
}
|
|
|
|
$panel->setCards($cards);
|
|
|
|
$board->addPanel($panel);
|
|
|
|
}
|
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
Javelin::initBehavior(
|
|
|
|
'boards-dropdown',
|
|
|
|
array());
|
|
|
|
|
2014-08-08 17:10:29 +02:00
|
|
|
$sort_menu = $this->buildSortMenu(
|
|
|
|
$viewer,
|
|
|
|
$sort_key);
|
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$filter_menu = $this->buildFilterMenu(
|
2014-03-26 22:40:47 +01:00
|
|
|
$viewer,
|
2014-06-25 21:30:53 +02:00
|
|
|
$custom_query,
|
|
|
|
$engine,
|
|
|
|
$query_key);
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$manage_menu = $this->buildManageMenu($project, $show_hidden);
|
2014-05-08 23:21:32 +02:00
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$header_link = phutil_tag(
|
|
|
|
'a',
|
|
|
|
array(
|
2015-01-12 19:04:01 +01:00
|
|
|
'href' => $this->getApplicationURI('profile/'.$project->getID().'/'),
|
2014-06-25 21:30:53 +02:00
|
|
|
),
|
|
|
|
$project->getName());
|
2014-05-08 23:21:32 +02:00
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$header = id(new PHUIHeaderView())
|
2015-01-12 23:48:29 +01:00
|
|
|
->setHeader(pht('%s Workboard', $header_link))
|
2014-06-25 21:30:53 +02:00
|
|
|
->setUser($viewer)
|
|
|
|
->setNoBackground(true)
|
2014-08-08 17:10:29 +02:00
|
|
|
->addActionLink($sort_menu)
|
2014-06-25 21:30:53 +02:00
|
|
|
->addActionLink($filter_menu)
|
|
|
|
->addActionLink($manage_menu)
|
|
|
|
->setPolicyObject($project);
|
|
|
|
|
|
|
|
$board_box = id(new PHUIBoxView())
|
|
|
|
->appendChild($board)
|
|
|
|
->addClass('project-board-wrapper');
|
|
|
|
|
2015-01-12 19:04:01 +01:00
|
|
|
$nav = $this->buildIconNavView($project);
|
|
|
|
$nav->appendChild($header);
|
|
|
|
$nav->appendChild($board_box);
|
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
return $this->buildApplicationPage(
|
2015-02-26 19:27:28 +01:00
|
|
|
$nav,
|
2014-05-20 20:42:05 +02:00
|
|
|
array(
|
2014-06-25 21:30:53 +02:00
|
|
|
'title' => pht('%s Board', $project->getName()),
|
2014-09-10 23:44:34 +02:00
|
|
|
'showFooter' => false,
|
2014-05-20 20:42:05 +02:00
|
|
|
));
|
2014-06-25 21:30:53 +02:00
|
|
|
}
|
|
|
|
|
2014-08-08 17:10:29 +02:00
|
|
|
private function buildSortMenu(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
$sort_key) {
|
|
|
|
|
|
|
|
$sort_icon = id(new PHUIIconView())
|
|
|
|
->setIconFont('fa-sort-amount-asc bluegrey');
|
|
|
|
|
|
|
|
$named = array(
|
|
|
|
PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'),
|
|
|
|
PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'),
|
|
|
|
);
|
|
|
|
|
|
|
|
$base_uri = $this->getURIWithState();
|
|
|
|
|
|
|
|
$items = array();
|
|
|
|
foreach ($named as $key => $name) {
|
|
|
|
$is_selected = ($key == $sort_key);
|
|
|
|
if ($is_selected) {
|
|
|
|
$active_order = $name;
|
|
|
|
}
|
|
|
|
|
|
|
|
$item = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-sort-amount-asc')
|
|
|
|
->setSelected($is_selected)
|
|
|
|
->setName($name);
|
|
|
|
|
|
|
|
$uri = $base_uri->alter('order', $key);
|
|
|
|
$item->setHref($uri);
|
|
|
|
|
|
|
|
$items[] = $item;
|
|
|
|
}
|
|
|
|
|
|
|
|
$sort_menu = id(new PhabricatorActionListView())
|
|
|
|
->setUser($viewer);
|
|
|
|
foreach ($items as $item) {
|
|
|
|
$sort_menu->addAction($item);
|
|
|
|
}
|
|
|
|
|
|
|
|
$sort_button = id(new PHUIButtonView())
|
|
|
|
->setText(pht('Sort: %s', $active_order))
|
|
|
|
->setIcon($sort_icon)
|
|
|
|
->setTag('a')
|
|
|
|
->setHref('#')
|
|
|
|
->addSigil('boards-dropdown-menu')
|
|
|
|
->setMetadata(
|
|
|
|
array(
|
|
|
|
'items' => hsprintf('%s', $sort_menu),
|
|
|
|
));
|
|
|
|
|
|
|
|
return $sort_button;
|
|
|
|
}
|
2014-06-25 21:30:53 +02:00
|
|
|
private function buildFilterMenu(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
$custom_query,
|
|
|
|
PhabricatorApplicationSearchEngine $engine,
|
|
|
|
$query_key) {
|
2014-05-20 20:42:05 +02:00
|
|
|
|
|
|
|
$filter_icon = id(new PHUIIconView())
|
|
|
|
->setIconFont('fa-search-plus bluegrey');
|
|
|
|
|
|
|
|
$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) {
|
2014-08-08 17:10:29 +02:00
|
|
|
$uri = $this->getApplicationURI(
|
|
|
|
'board/'.$this->id.'/filter/query/'.$key.'/');
|
2014-05-20 20:42:05 +02:00
|
|
|
$item->setWorkflow(true);
|
|
|
|
} else {
|
2014-08-08 17:10:29 +02:00
|
|
|
$uri = $engine->getQueryResultsPageURI($key);
|
2014-05-20 20:42:05 +02:00
|
|
|
}
|
|
|
|
|
2014-08-08 17:10:29 +02:00
|
|
|
$uri = $this->getURIWithState($uri);
|
|
|
|
$item->setHref($uri);
|
|
|
|
|
2014-05-20 20:42:05 +02:00
|
|
|
$items[] = $item;
|
|
|
|
}
|
|
|
|
|
|
|
|
$items[] = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-cog')
|
|
|
|
->setHref($this->getApplicationURI('board/'.$this->id.'/filter/'))
|
|
|
|
->setWorkflow(true)
|
|
|
|
->setName(pht('Advanced Filter...'));
|
|
|
|
|
|
|
|
$filter_menu = id(new PhabricatorActionListView())
|
|
|
|
->setUser($viewer);
|
|
|
|
foreach ($items as $item) {
|
|
|
|
$filter_menu->addAction($item);
|
|
|
|
}
|
|
|
|
|
|
|
|
$filter_button = id(new PHUIButtonView())
|
|
|
|
->setText(pht('Filter: %s', $active_filter))
|
|
|
|
->setIcon($filter_icon)
|
|
|
|
->setTag('a')
|
|
|
|
->setHref('#')
|
2014-06-25 21:30:53 +02:00
|
|
|
->addSigil('boards-dropdown-menu')
|
2014-05-20 20:42:05 +02:00
|
|
|
->setMetadata(
|
|
|
|
array(
|
|
|
|
'items' => hsprintf('%s', $filter_menu),
|
|
|
|
));
|
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
return $filter_button;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function buildManageMenu(
|
|
|
|
PhabricatorProject $project,
|
|
|
|
$show_hidden) {
|
|
|
|
|
|
|
|
$request = $this->getRequest();
|
|
|
|
$viewer = $request->getUser();
|
|
|
|
|
|
|
|
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
|
|
|
$viewer,
|
|
|
|
$project,
|
|
|
|
PhabricatorPolicyCapability::CAN_EDIT);
|
|
|
|
|
|
|
|
$manage_icon = id(new PHUIIconView())
|
|
|
|
->setIconFont('fa-cog bluegrey');
|
|
|
|
|
|
|
|
$manage_items = array();
|
|
|
|
|
|
|
|
$manage_items[] = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-plus')
|
|
|
|
->setName(pht('Add Column'))
|
2014-07-12 04:27:07 +02:00
|
|
|
->setHref($this->getApplicationURI('board/'.$this->id.'/edit/'))
|
|
|
|
->setDisabled(!$can_edit)
|
|
|
|
->setWorkflow(!$can_edit);
|
|
|
|
|
|
|
|
$manage_items[] = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-exchange')
|
|
|
|
->setName(pht('Reorder Columns'))
|
|
|
|
->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/'))
|
|
|
|
->setDisabled(!$can_edit)
|
|
|
|
->setWorkflow(true);
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-06-25 21:30:20 +02:00
|
|
|
if ($show_hidden) {
|
2014-08-08 17:10:29 +02:00
|
|
|
$hidden_uri = $this->getURIWithState()
|
2014-06-25 21:30:20 +02:00
|
|
|
->setQueryParam('hidden', null);
|
2014-06-25 21:30:53 +02:00
|
|
|
$hidden_icon = 'fa-eye-slash';
|
2014-06-25 21:30:20 +02:00
|
|
|
$hidden_text = pht('Hide Hidden Columns');
|
|
|
|
} else {
|
2014-08-08 17:10:29 +02:00
|
|
|
$hidden_uri = $this->getURIWithState()
|
2014-06-25 21:30:20 +02:00
|
|
|
->setQueryParam('hidden', 'true');
|
2014-06-25 21:30:53 +02:00
|
|
|
$hidden_icon = 'fa-eye';
|
2014-06-25 21:30:20 +02:00
|
|
|
$hidden_text = pht('Show Hidden Columns');
|
|
|
|
}
|
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$manage_items[] = id(new PhabricatorActionView())
|
2014-06-25 21:30:20 +02:00
|
|
|
->setIcon($hidden_icon)
|
2014-06-25 21:30:53 +02:00
|
|
|
->setName($hidden_text)
|
2014-06-25 21:30:20 +02:00
|
|
|
->setHref($hidden_uri);
|
|
|
|
|
Add a "Batch Edit Tasks..." action to workboard columns
Summary:
Ref T5523. Adds a new workflow to make some kinds of bulk workboard operations easier.
New dropdown action:
{F376848}
This brings you into the existing bulk edit flow:
{F376849}
When you save an edit, you're taken back to the board:
{F376850}
If you try to edit a column with nothing in it, you get an error:
{F376851}
Note that the selected workboard filter is applied before choosing tasks, so if your filter is set to "open tasks" we only batch edit the open (i.e., currently visible) tasks in the column. I think this is more powerful (it lets you use filtering to select task subsets) but might not be completely obvious in all cases (although I do think it's more obvious than the alternative rule -- just an issue of neither rule being completely obvious).
Test Plan:
- Batch edited tasks in a column.
- Used "Batch Edit Tasks..." to move tasks to a different workboard by removing + adding a project.
- Batch edited a column with filtered-out tasks, verified only visible tasks were edited.
- Batch edited a column with no visible tasks, received error.
- Used the batch editor normally (Maniphest -> Maniphest, no boards).
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: johnny-bit, cburroughs, epriestley
Projects: #prioritized
Maniphest Tasks: T5523
Differential Revision: https://secure.phabricator.com/D12475
2015-04-20 19:05:08 +02:00
|
|
|
$batch_edit_uri = $request->getRequestURI();
|
|
|
|
$batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL);
|
|
|
|
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
|
|
|
|
$viewer,
|
|
|
|
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
|
|
|
|
ManiphestBulkEditCapability::CAPABILITY);
|
|
|
|
|
|
|
|
$manage_items[] = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-list-ul')
|
|
|
|
->setName(pht('Batch Edit Visible Tasks...'))
|
|
|
|
->setHref($batch_edit_uri)
|
|
|
|
->setDisabled(!$can_batch_edit);
|
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$manage_menu = id(new PhabricatorActionListView())
|
|
|
|
->setUser($viewer);
|
|
|
|
foreach ($manage_items as $item) {
|
|
|
|
$manage_menu->addAction($item);
|
|
|
|
}
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
$manage_button = id(new PHUIButtonView())
|
|
|
|
->setText(pht('Manage Board'))
|
|
|
|
->setIcon($manage_icon)
|
|
|
|
->setTag('a')
|
|
|
|
->setHref('#')
|
|
|
|
->addSigil('boards-dropdown-menu')
|
|
|
|
->setMetadata(
|
|
|
|
array(
|
|
|
|
'items' => hsprintf('%s', $manage_menu),
|
|
|
|
));
|
2014-03-26 22:40:47 +01:00
|
|
|
|
2014-06-25 21:30:53 +02:00
|
|
|
return $manage_button;
|
2014-03-26 22:40:47 +01:00
|
|
|
}
|
|
|
|
|
2014-08-08 19:35:51 +02:00
|
|
|
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();
|
|
|
|
|
|
|
|
$column_items[] = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-plus')
|
|
|
|
->setName(pht('Create Task...'))
|
|
|
|
->setHref('/maniphest/task/create/')
|
|
|
|
->addSigil('column-add-task')
|
|
|
|
->setMetadata(
|
|
|
|
array(
|
|
|
|
'columnPHID' => $column->getPHID(),
|
|
|
|
))
|
|
|
|
->setDisabled(!$can_edit);
|
|
|
|
|
Add a "Batch Edit Tasks..." action to workboard columns
Summary:
Ref T5523. Adds a new workflow to make some kinds of bulk workboard operations easier.
New dropdown action:
{F376848}
This brings you into the existing bulk edit flow:
{F376849}
When you save an edit, you're taken back to the board:
{F376850}
If you try to edit a column with nothing in it, you get an error:
{F376851}
Note that the selected workboard filter is applied before choosing tasks, so if your filter is set to "open tasks" we only batch edit the open (i.e., currently visible) tasks in the column. I think this is more powerful (it lets you use filtering to select task subsets) but might not be completely obvious in all cases (although I do think it's more obvious than the alternative rule -- just an issue of neither rule being completely obvious).
Test Plan:
- Batch edited tasks in a column.
- Used "Batch Edit Tasks..." to move tasks to a different workboard by removing + adding a project.
- Batch edited a column with filtered-out tasks, verified only visible tasks were edited.
- Batch edited a column with no visible tasks, received error.
- Used the batch editor normally (Maniphest -> Maniphest, no boards).
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: johnny-bit, cburroughs, epriestley
Projects: #prioritized
Maniphest Tasks: T5523
Differential Revision: https://secure.phabricator.com/D12475
2015-04-20 19:05:08 +02:00
|
|
|
$batch_edit_uri = $request->getRequestURI();
|
|
|
|
$batch_edit_uri->setQueryParam('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('Batch Edit Tasks...'))
|
|
|
|
->setHref($batch_edit_uri)
|
|
|
|
->setDisabled(!$can_batch_edit);
|
|
|
|
|
2014-08-08 19:35:51 +02:00
|
|
|
$edit_uri = $this->getApplicationURI(
|
|
|
|
'board/'.$this->id.'/column/'.$column->getID().'/');
|
|
|
|
|
|
|
|
$column_items[] = id(new PhabricatorActionView())
|
|
|
|
->setIcon('fa-pencil')
|
|
|
|
->setName(pht('Edit Column'))
|
|
|
|
->setHref($edit_uri)
|
|
|
|
->setDisabled(!$can_edit)
|
|
|
|
->setWorkflow(!$can_edit);
|
|
|
|
|
2014-09-04 21:47:32 +02:00
|
|
|
$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);
|
|
|
|
}
|
|
|
|
|
2014-08-08 19:35:51 +02:00
|
|
|
$column_menu = id(new PhabricatorActionListView())
|
|
|
|
->setUser($viewer);
|
|
|
|
foreach ($column_items as $item) {
|
|
|
|
$column_menu->addAction($item);
|
|
|
|
}
|
|
|
|
|
|
|
|
$column_button = id(new PHUIIconView())
|
|
|
|
->setIconFont('fa-caret-down')
|
|
|
|
->setHref('#')
|
|
|
|
->addSigil('boards-dropdown-menu')
|
|
|
|
->setMetadata(
|
|
|
|
array(
|
|
|
|
'items' => hsprintf('%s', $column_menu),
|
|
|
|
));
|
|
|
|
|
|
|
|
return $column_button;
|
|
|
|
}
|
|
|
|
|
2014-08-05 22:40:41 +02:00
|
|
|
private function initializeWorkboardDialog(PhabricatorProject $project) {
|
|
|
|
|
|
|
|
$instructions = pht('This workboard has not been setup yet.');
|
|
|
|
$new_selector = id(new AphrontFormRadioButtonControl())
|
|
|
|
->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.'));
|
|
|
|
|
|
|
|
$dialog = id(new AphrontDialogView())
|
|
|
|
->setUser($this->getRequest()->getUser())
|
|
|
|
->setTitle(pht('New Workboard'))
|
|
|
|
->addSubmitButton('Continue')
|
|
|
|
->addCancelButton($this->getApplicationURI('view/'.$project->getID().'/'))
|
|
|
|
->appendParagraph($instructions)
|
|
|
|
->appendChild($new_selector);
|
|
|
|
|
|
|
|
return id(new AphrontDialogResponse())
|
|
|
|
->setDialog($dialog);
|
|
|
|
}
|
2014-06-25 21:30:53 +02:00
|
|
|
|
2014-08-08 17:10:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* @return PhutilURI URI with state parameters.
|
|
|
|
*/
|
|
|
|
private function getURIWithState($base = null) {
|
|
|
|
if ($base === null) {
|
|
|
|
$base = $this->getRequest()->getRequestURI();
|
|
|
|
}
|
|
|
|
|
|
|
|
$base = new PhutilURI($base);
|
|
|
|
|
|
|
|
if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) {
|
|
|
|
$base->setQueryParam('order', $this->sortKey);
|
|
|
|
} else {
|
|
|
|
$base->setQueryParam('order', null);
|
|
|
|
}
|
|
|
|
|
|
|
|
$base->setQueryParam('hidden', $this->showHidden ? 'true' : null);
|
|
|
|
|
|
|
|
return $base;
|
|
|
|
}
|
|
|
|
|
2014-03-26 22:40:47 +01:00
|
|
|
}
|