Differential D15171 Diff 36626 src/applications/project/controller/PhabricatorProjectBoardViewController.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/project/controller/PhabricatorProjectBoardViewController.php
Show First 20 Lines • Show All 124 Lines • ▼ Show 20 Lines | if ($request->getURIData('filter')) { | ||||
->appendChild($filter_form->buildLayoutView()) | ->appendChild($filter_form->buildLayoutView()) | ||||
->setSubmitURI($board_uri) | ->setSubmitURI($board_uri) | ||||
->addSubmitButton(pht('Apply Filter')) | ->addSubmitButton(pht('Apply Filter')) | ||||
->addCancelButton($board_uri); | ->addCancelButton($board_uri); | ||||
} | } | ||||
$task_query = $engine->buildQueryFromSavedQuery($saved); | $task_query = $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 | $tasks = $task_query | ||||
->withEdgeLogicPHIDs( | ->withEdgeLogicPHIDs( | ||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, | PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, | ||||
PhabricatorQueryConstraint::OPERATOR_AND, | PhabricatorQueryConstraint::OPERATOR_ANCESTOR, | ||||
array($project->getPHID())) | array($select_phids)) | ||||
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) | ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) | ||||
->setViewer($viewer) | ->setViewer($viewer) | ||||
->execute(); | ->execute(); | ||||
$tasks = mpull($tasks, null, 'getPHID'); | $tasks = mpull($tasks, null, 'getPHID'); | ||||
if ($tasks) { | $task_map = $this->loadColumnMap($project, $tasks, $columns); | ||||
$positions = id(new PhabricatorProjectColumnPositionQuery()) | |||||
->setViewer($viewer) | |||||
->withObjectPHIDs(mpull($tasks, 'getPHID')) | |||||
->withColumns($columns) | |||||
->execute(); | |||||
$positions = mpull($positions, null, 'getObjectPHID'); | |||||
} else { | |||||
$positions = array(); | |||||
} | |||||
$task_map = array(); | |||||
foreach ($tasks as $task) { | |||||
$task_phid = $task->getPHID(); | |||||
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; | |||||
} | |||||
$position = $positions[$task_phid]; | |||||
$task_map[$position->getColumnPHID()][] = $task_phid; | |||||
} | |||||
// 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); | |||||
} | |||||
} | |||||
$task_can_edit_map = id(new PhabricatorPolicyFilter()) | $task_can_edit_map = id(new PhabricatorPolicyFilter()) | ||||
->setViewer($viewer) | ->setViewer($viewer) | ||||
->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) | ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) | ||||
->apply($tasks); | ->apply($tasks); | ||||
// If this is a batch edit, select the editable tasks in the chosen column | // If this is a batch edit, select the editable tasks in the chosen column | ||||
// and ship the user into the batch editor. | // and ship the user into the batch editor. | ||||
▲ Show 20 Lines • Show All 54 Lines • ▼ Show 20 Lines | public function handleRequest(AphrontRequest $request) { | ||||
); | ); | ||||
$this->initBehavior( | $this->initBehavior( | ||||
'project-boards', | 'project-boards', | ||||
$behavior_config); | $behavior_config); | ||||
$this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); | $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); | ||||
foreach ($columns as $column) { | foreach ($columns as $column) { | ||||
if (!$this->showHidden && $column->isHidden()) { | |||||
continue; | |||||
} | |||||
$task_phids = idx($task_map, $column->getPHID(), array()); | $task_phids = idx($task_map, $column->getPHID(), array()); | ||||
$column_tasks = array_select_keys($tasks, $task_phids); | $column_tasks = array_select_keys($tasks, $task_phids); | ||||
$panel = id(new PHUIWorkpanelView()) | $panel = id(new PHUIWorkpanelView()) | ||||
->setHeader($column->getDisplayName()) | ->setHeader($column->getDisplayName()) | ||||
->setSubHeader($column->getDisplayType()) | ->setSubHeader($column->getDisplayType()) | ||||
->addSigil('workpanel'); | ->addSigil('workpanel'); | ||||
▲ Show 20 Lines • Show All 123 Lines • ▼ Show 20 Lines | final class PhabricatorProjectBoardViewController | ||||
private function loadColumns(PhabricatorProject $project) { | private function loadColumns(PhabricatorProject $project) { | ||||
$viewer = $this->getViewer(); | $viewer = $this->getViewer(); | ||||
$column_query = id(new PhabricatorProjectColumnQuery()) | $column_query = id(new PhabricatorProjectColumnQuery()) | ||||
->setViewer($viewer) | ->setViewer($viewer) | ||||
->withProjectPHIDs(array($project->getPHID())); | ->withProjectPHIDs(array($project->getPHID())); | ||||
if (!$this->showHidden) { | $columns = $column_query->execute(); | ||||
$column_query->withStatuses( | $columns = msort($columns, 'getSequence'); | ||||
array(PhabricatorProjectColumn::STATUS_ACTIVE)); | |||||
// If this project has no real columns, consider the workboard empty and | |||||
// return nothing. We don't want to create proxy columns if there's no | |||||
// board at all yet. | |||||
if (!$columns) { | |||||
return array(); | |||||
} | } | ||||
$columns = $column_query->execute(); | // If this project has subprojects or milestones, remove all columns | ||||
$columns = mpull($columns, null, 'getSequence'); | // except the backlog, then create any missing columns. | ||||
ksort($columns); | if ($project->getHasSubprojects() || $project->getHasMilestones()) { | ||||
foreach ($columns as $key => $column) { | |||||
if (!$column->getProxyPHID() && !$column->isDefaultColumn()) { | |||||
unset($columns[$key]); | |||||
} | |||||
} | |||||
$child_projects = id(new PhabricatorProjectQuery()) | |||||
->setViewer($viewer) | |||||
->withParentProjectPHIDs(array($project->getPHID())) | |||||
->withIsMilestone(true) | |||||
->execute(); | |||||
$child_projects = mpull($child_projects, null, 'getPHID'); | |||||
$next_sequence = last($columns)->getSequence() + 1; | |||||
$proxy_columns = mpull($columns, null, 'getProxyPHID'); | |||||
foreach ($child_projects as $phid => $child) { | |||||
if (isset($proxy_columns[$phid])) { | |||||
continue; | |||||
} | |||||
$new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) | |||||
->attachProject($project) | |||||
->attachProxy($child) | |||||
->setSequence($next_sequence++) | |||||
->setProjectPHID($project->getPHID()) | |||||
->setProxyPHID($phid); | |||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | |||||
$new_column->save(); | |||||
unset($unguarded); | |||||
$columns[] = $new_column; | |||||
} | |||||
} | |||||
return $columns; | return $columns; | ||||
} | } | ||||
private function buildSortMenu( | private function buildSortMenu( | ||||
PhabricatorUser $viewer, | PhabricatorUser $viewer, | ||||
$sort_key) { | $sort_key) { | ||||
▲ Show 20 Lines • Show All 204 Lines • ▼ Show 20 Lines | private function buildColumnMenu( | ||||
$can_edit = PhabricatorPolicyFilter::hasCapability( | $can_edit = PhabricatorPolicyFilter::hasCapability( | ||||
$viewer, | $viewer, | ||||
$project, | $project, | ||||
PhabricatorPolicyCapability::CAN_EDIT); | PhabricatorPolicyCapability::CAN_EDIT); | ||||
$column_items = array(); | $column_items = array(); | ||||
if ($column->getProxyPHID()) { | |||||
$default_phid = $column->getProxyPHID(); | |||||
} else { | |||||
$default_phid = $column->getProjectPHID(); | |||||
} | |||||
$column_items[] = id(new PhabricatorActionView()) | $column_items[] = id(new PhabricatorActionView()) | ||||
->setIcon('fa-plus') | ->setIcon('fa-plus') | ||||
->setName(pht('Create Task...')) | ->setName(pht('Create Task...')) | ||||
->setHref($this->getCreateURI()) | ->setHref($this->getCreateURI()) | ||||
->addSigil('column-add-task') | ->addSigil('column-add-task') | ||||
->setMetadata( | ->setMetadata( | ||||
array( | array( | ||||
'columnPHID' => $column->getPHID(), | 'columnPHID' => $column->getPHID(), | ||||
'projectPHID' => $default_phid, | |||||
)); | )); | ||||
$batch_edit_uri = $request->getRequestURI(); | $batch_edit_uri = $request->getRequestURI(); | ||||
$batch_edit_uri->setQueryParam('batch', $column->getID()); | $batch_edit_uri->setQueryParam('batch', $column->getID()); | ||||
$can_batch_edit = PhabricatorPolicyFilter::hasCapability( | $can_batch_edit = PhabricatorPolicyFilter::hasCapability( | ||||
$viewer, | $viewer, | ||||
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), | PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), | ||||
ManiphestBulkEditCapability::CAPABILITY); | ManiphestBulkEditCapability::CAPABILITY); | ||||
▲ Show 20 Lines • Show All 132 Lines • ▼ Show 20 Lines | if ($request->isFormPost()) { | ||||
return id(new AphrontRedirectResponse()) | return id(new AphrontRedirectResponse()) | ||||
->setURI($board_uri); | ->setURI($board_uri); | ||||
} else { | } else { | ||||
return id(new AphrontRedirectResponse()) | return id(new AphrontRedirectResponse()) | ||||
->setURI($import_uri); | ->setURI($import_uri); | ||||
} | } | ||||
} | } | ||||
// TODO: Tailor this UI if the project is already a parent project. We | |||||
Lint: TODO Comment: This comment has a TODO. | |||||
// should not offer options for creating a parent project workboard, since | |||||
// they can't have their own columns. | |||||
$new_selector = id(new AphrontFormRadioButtonControl()) | $new_selector = id(new AphrontFormRadioButtonControl()) | ||||
->setLabel(pht('Columns')) | ->setLabel(pht('Columns')) | ||||
->setName('initialize-type') | ->setName('initialize-type') | ||||
->setValue('backlog-only') | ->setValue('backlog-only') | ||||
->addButton( | ->addButton( | ||||
'backlog-only', | 'backlog-only', | ||||
pht('New Empty Board'), | pht('New Empty Board'), | ||||
pht('Create a new board with just a backlog column.')) | pht('Create a new board with just a backlog column.')) | ||||
Show All 40 Lines | return $this->newDialog() | ||||
->appendParagraph( | ->appendParagraph( | ||||
pht( | pht( | ||||
'The workboard for this project has not been created yet, '. | 'The workboard for this project has not been created yet, '. | ||||
'but you do not have permission to create it. Only users '. | 'but you do not have permission to create it. Only users '. | ||||
'who can edit this project can create a workboard for it.')) | 'who can edit this project can create a workboard for it.')) | ||||
->addCancelButton($profile_uri); | ->addCancelButton($profile_uri); | ||||
} | } | ||||
private function loadColumnMap( | |||||
PhabricatorProject $board_project, | |||||
array $tasks, | |||||
array $columns) { | |||||
if (!$tasks) { | |||||
return array(); | |||||
} | |||||
$viewer = $this->getViewer(); | |||||
$positions = id(new PhabricatorProjectColumnPositionQuery()) | |||||
->setViewer($viewer) | |||||
->withObjectPHIDs(mpull($tasks, 'getPHID')) | |||||
->withColumns($columns) | |||||
->execute(); | |||||
$positions = mpull($positions, null, 'getObjectPHID'); | |||||
// First, put the tasks in the correct global order so we don't have to | |||||
// worry about it later. | |||||
if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { | |||||
$sort_map = array(); | |||||
foreach ($tasks as $phid => $task) { | |||||
$position = idx($positions, $phid); | |||||
if ($position) { | |||||
$sort_map[$phid] = $position->getOrderingKey(); | |||||
} else { | |||||
$sort_map[$phid] = 0; | |||||
} | |||||
} | |||||
asort($sort_map); | |||||
$tasks = array_select_keys($tasks, array_keys($sort_map)); | |||||
} | |||||
// Find all the columns which are proxies for other objects. | |||||
$proxy_map = array(); | |||||
foreach ($columns as $column) { | |||||
$proxy_phid = $column->getProxyPHID(); | |||||
if ($proxy_phid) { | |||||
$proxy_map[$proxy_phid] = $column->getPHID(); | |||||
} | |||||
} | |||||
// If we have proxies, we need to force cards into the correct proxy | |||||
// columns. | |||||
if ($proxy_map) { | |||||
$all_projects = array(); | |||||
foreach ($tasks as $task) { | |||||
foreach ($task->getProjectPHIDs() as $project_phid) { | |||||
$all_projects[$project_phid] = $project_phid; | |||||
} | |||||
} | |||||
if ($all_projects) { | |||||
$projects = id(new PhabricatorProjectQuery()) | |||||
->setViewer($viewer) | |||||
->withPHIDs($all_projects) | |||||
->execute(); | |||||
$projects = mpull($projects, null, 'getPHID'); | |||||
} else { | |||||
$projects = array(); | |||||
} | |||||
// Build a map from every project that any task is tagged with to the | |||||
// ancestor project which has a column on this board, if one exists. | |||||
$ancestor_map = array(); | |||||
foreach ($projects as $phid => $project) { | |||||
if (isset($proxy_map[$phid])) { | |||||
$ancestor_map[$phid] = $proxy_map[$phid]; | |||||
} else { | |||||
$seen = array($phid); | |||||
foreach ($project->getAncestorProjects() as $ancestor) { | |||||
$ancestor_phid = $ancestor->getPHID(); | |||||
$seen[] = $ancestor_phid; | |||||
if (isset($proxy_map[$ancestor_phid])) { | |||||
foreach ($seen as $project_phid) { | |||||
$ancestor_map[$project_phid] = $proxy_map[$ancestor_phid]; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
$column_map = mpull($columns, null, 'getPHID'); | |||||
foreach ($columns as $column) { | |||||
if ($column->isDefaultColumn()) { | |||||
$default_phid = $column->getPHID(); | |||||
break; | |||||
} | |||||
} | |||||
$rem_positions = array(); | |||||
$add_positions = array(); | |||||
$task_map = array(); | |||||
foreach ($tasks as $task_phid => $task) { | |||||
// First, check for a tags that have corresponding proxy columns, | |||||
if ($proxy_map) { | |||||
$proxy_hits = array(); | |||||
foreach ($task->getProjectPHIDs() as $project_phid) { | |||||
if (isset($ancestor_map[$project_phid])) { | |||||
$task_map[$ancestor_map[$project_phid]][] = $task_phid; | |||||
$proxy_hits[] = $ancestor_map[$project_phid]; | |||||
// NOTE: For now, only permit tasks to appear in one column. We | |||||
// may change this soon. | |||||
break; | |||||
} | |||||
} | |||||
if ($proxy_hits) { | |||||
$proxy_hits = array_fuse($proxy_hits); | |||||
$task_positions = array(); | |||||
$position = idx($positions, $task_phid); | |||||
if ($position) { | |||||
$task_positions[] = $position; | |||||
} | |||||
foreach ($task_positions as $task_position) { | |||||
$column_phid = $task_position->getColumnPHID(); | |||||
if (isset($proxy_hits[$column_phid])) { | |||||
unset($proxy_hits[$column_phid]); | |||||
continue; | |||||
} | |||||
$rem_positions[] = $task_position; | |||||
} | |||||
foreach ($proxy_hits as $proxy_column_phid) { | |||||
$add_positions[] = id(new PhabricatorProjectColumnPosition()) | |||||
->setBoardPHID($board_project->getPHID()) | |||||
->setColumnPHID($proxy_column_phid) | |||||
->setObjectPHID($task_phid) | |||||
->setSequence(0); | |||||
} | |||||
continue; | |||||
} | |||||
} | |||||
// Next, check for existing positions on the board. | |||||
$position = idx($positions, $task_phid); | |||||
if ($position) { | |||||
$column_phid = $position->getColumnPHID(); | |||||
// We only use an exiting position if the column is valid. For example, | |||||
// if a task was in a column but the project was converted into a | |||||
// parent project, the task gets kicked back to the backlog. | |||||
if (isset($column_map[$column_phid])) { | |||||
$task_map[$column_phid][] = $task_phid; | |||||
continue; | |||||
} else { | |||||
$rem_positions[] = $position; | |||||
} | |||||
} | |||||
// If we made it here, we didn't find a valid position, so we're putting | |||||
// it in the backlog and writing a new position row. | |||||
$task_map[$default_phid][] = $task_phid; | |||||
$add_positions[] = id(new PhabricatorProjectColumnPosition()) | |||||
->setBoardPHID($board_project->getPHID()) | |||||
->setColumnPHID($default_phid) | |||||
->setObjectPHID($task_phid) | |||||
->setSequence(0); | |||||
} | |||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | |||||
foreach ($rem_positions as $position) { | |||||
$position->delete(); | |||||
} | |||||
foreach ($add_positions as $position) { | |||||
$position->save(); | |||||
} | |||||
unset($unguarded); | |||||
return $task_map; | |||||
} | |||||
} | } |
This comment has a TODO.