diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4172,6 +4172,7 @@ 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnAuthorOrder' => 'applications/project/order/PhabricatorProjectColumnAuthorOrder.php', + 'PhabricatorProjectColumnBulkEditController' => 'applications/project/controller/PhabricatorProjectColumnBulkEditController.php', 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', @@ -10445,6 +10446,7 @@ 'PhabricatorConduitResultInterface', ), 'PhabricatorProjectColumnAuthorOrder' => 'PhabricatorProjectColumnOrder', + 'PhabricatorProjectColumnBulkEditController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -81,6 +81,8 @@ => 'PhabricatorProjectColumnDetailController', 'viewquery/(?P\d+)/' => 'PhabricatorProjectColumnViewQueryController', + 'bulk/(?P\d+)/' + => 'PhabricatorProjectColumnBulkEditController', 'import/' => 'PhabricatorProjectBoardImportController', 'reorder/' diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -3,8 +3,6 @@ final class PhabricatorProjectBoardViewController extends PhabricatorProjectBoardController { - const BATCH_EDIT_ALL = 'all'; - public function shouldAllowPublic() { return true; } @@ -34,42 +32,9 @@ $custom_query = null; } - $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'); + $layout_engine = $state->getLayoutEngine(); $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; @@ -122,67 +87,13 @@ ->appendChild($content); } + $tasks = $state->getObjects(); + $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 = $state->newWorkboardURI(); - 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', $project->getID()); - - return id(new AphrontRedirectResponse()) - ->setURI($bulk_uri); - } - $move_id = $request->getStr('move'); if (strlen($move_id)) { $column_id_map = mpull($columns, null, 'getID'); @@ -426,11 +337,13 @@ } } + $container_phids = $state->getBoardContainerPHIDs(); + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) ->setObjects(array_select_keys($tasks, $visible_phids)) ->setEditMap($task_can_edit_map) - ->setExcludedProjectPHIDs($select_phids); + ->setExcludedProjectPHIDs($container_phids); $templates = array(); $all_tasks = array(); @@ -912,13 +825,6 @@ ->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) { @@ -1002,9 +908,12 @@ $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( + $bulk_edit_uri = $state->newWorkboardURI( + urisprintf( + 'bulk/%d/', + $column->getID())); + + $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); @@ -1012,8 +921,8 @@ $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-list-ul') ->setName(pht('Bulk Edit Tasks...')) - ->setHref($batch_edit_uri) - ->setDisabled(!$can_batch_edit); + ->setHref($bulk_edit_uri) + ->setDisabled(!$can_bulk_edit); $batch_move_uri = $request->getRequestURI(); $batch_move_uri->replaceQueryParam('move', $column->getID()); diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $bulk_column = idx($columns, $column_id); + if (!$bulk_column) { + return new Aphront404Response(); + } + + $bulk_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $bulk_column->getPHID()); + + $tasks = $state->getObjects(); + + $bulk_tasks = array_select_keys($tasks, $bulk_task_phids); + + $bulk_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($bulk_tasks); + + if (!$bulk_tasks) { + 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($bulk_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', $project->getID()); + + return id(new AphrontRedirectResponse()) + ->setURI($bulk_uri); + } + +} diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php --- a/src/applications/project/state/PhabricatorWorkboardViewState.php +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -8,6 +8,8 @@ private $requestState = array(); private $savedQuery; private $searchEngine; + private $layoutEngine; + private $objects; public function setProject(PhabricatorProject $project) { $this->project = $project; @@ -212,4 +214,78 @@ return $this->requestState; } + public function getLayoutEngine() { + if ($this->layoutEngine === null) { + $this->layoutEngine = $this->newLayoutEngine(); + } + return $this->layoutEngine; + } + + private function newLayoutEngine() { + $project = $this->getProject(); + $viewer = $this->getViewer(); + + $board_phid = $project->getPHID(); + $objects = $this->getObjects(); + + // Regardless of display order, pass tasks to the layout engine in ID order + // so layout is consistent. + $objects = msort($objects, 'getID'); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setObjectPHIDs(array_keys($objects)) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + return $layout_engine; + } + + public function getBoardContainerPHIDs() { + $project = $this->getProject(); + $viewer = $this->getViewer(); + + $container_phids = array($project->getPHID()); + if ($project->getHasSubprojects() || $project->getHasMilestones()) { + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($container_phids) + ->execute(); + foreach ($descendants as $descendant) { + $container_phids[] = $descendant->getPHID(); + } + } + + return $container_phids; + } + + public function getObjects() { + if ($this->objects === null) { + $this->objects = $this->newObjects(); + } + + return $this->objects; + } + + private function newObjects() { + $viewer = $this->getViewer(); + $saved_query = $this->getSavedQuery(); + $search_engine = $this->getSearchEngine(); + + $container_phids = $this->getBoardContainerPHIDs(); + + $task_query = $search_engine->buildQueryFromSavedQuery($saved_query) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($container_phids)); + + $tasks = $task_query->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + return $tasks; + } + }