Page MenuHomePhabricator

D18806.diff
No OneTemporary

D18806.diff

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -9,7 +9,7 @@
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
- 'core.pkg.css' => '5be8063f',
+ 'core.pkg.css' => '075f9867',
'core.pkg.js' => '4c79d74f',
'darkconsole.pkg.js' => '1f9a31bc',
'differential.pkg.css' => '45951e9e',
@@ -18,7 +18,7 @@
'diffusion.pkg.js' => '6134c5a1',
'favicon.ico' => '30672e08',
'maniphest.pkg.css' => '4845691a',
- 'maniphest.pkg.js' => '5ab2753f',
+ 'maniphest.pkg.js' => '4d7e79c8',
'rsrc/audio/basic/alert.mp3' => '98461568',
'rsrc/audio/basic/bing.mp3' => 'ab8603a5',
'rsrc/audio/basic/pock.mp3' => '0cc772f5',
@@ -135,7 +135,7 @@
'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77',
'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3',
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6',
- 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '73c5f5c4',
+ 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '6ae18df0',
'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea',
'rsrc/css/phui/phui-action-list.css' => 'f7f61a34',
'rsrc/css/phui/phui-action-panel.css' => 'b4798122',
@@ -420,7 +420,7 @@
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7',
- 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '0825c27a',
+ 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ad54037e',
'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876',
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763',
@@ -643,7 +643,7 @@
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
- 'javelin-behavior-maniphest-batch-selector' => '0825c27a',
+ 'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
'javelin-behavior-maniphest-subpriority-editor' => '71237763',
'javelin-behavior-owners-path-editor' => '7a68dda3',
@@ -862,7 +862,7 @@
'phui-oi-color-css' => 'cd2b9b77',
'phui-oi-drag-ui-css' => '08f4ccc3',
'phui-oi-flush-ui-css' => '9d9685d6',
- 'phui-oi-list-view-css' => '73c5f5c4',
+ 'phui-oi-list-view-css' => '6ae18df0',
'phui-oi-simple-ui-css' => 'a8beebea',
'phui-pager-css' => 'edcbc226',
'phui-pinboard-view-css' => '2495140e',
@@ -960,12 +960,6 @@
'javelin-stratcom',
'javelin-workflow',
),
- '0825c27a' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
- ),
'08f4ccc3' => array(
'phui-oi-list-view-css',
),
@@ -1815,6 +1809,12 @@
'phuix-autocomplete',
'javelin-mask',
),
+ 'ad54037e' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-util',
+ ),
'b003d4fb' => array(
'javelin-behavior',
'javelin-stratcom',
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
@@ -1487,8 +1487,8 @@
'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
- 'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php',
'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
+ 'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php',
'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
@@ -1547,6 +1547,7 @@
'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php',
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
+ 'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php',
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php',
@@ -2198,6 +2199,7 @@
'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
+ 'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
@@ -6678,8 +6680,8 @@
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
- 'ManiphestBatchEditController' => 'ManiphestController',
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
+ 'ManiphestBulkEditController' => 'ManiphestController',
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
@@ -6761,6 +6763,7 @@
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
+ 'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine',
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
@@ -7487,6 +7490,7 @@
'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
+ 'PhabricatorBulkEngine' => 'Phobject',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
'PhabricatorCacheEngine' => 'Phobject',
'PhabricatorCacheEngineExtension' => 'Phobject',
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -618,6 +618,10 @@
')?';
}
+ protected function getBulkRoutePattern($base = null) {
+ return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
+ }
+
protected function getQueryRoutePattern($base = null) {
return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
}
diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
--- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php
+++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
@@ -52,7 +52,7 @@
'/maniphest/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
- 'batch/' => 'ManiphestBatchEditController',
+ $this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController',
'task/' => array(
$this->getEditRoutePattern('edit/')
=> 'ManiphestTaskEditController',
diff --git a/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php
@@ -0,0 +1,50 @@
+<?php
+
+final class ManiphestTaskBulkEngine
+ extends PhabricatorBulkEngine {
+
+ private $workboard;
+
+ public function setWorkboard(PhabricatorProject $workboard) {
+ $this->workboard = $workboard;
+ return $this;
+ }
+
+ public function getWorkboard() {
+ return $this->workboard;
+ }
+
+ public function newSearchEngine() {
+ return new ManiphestTaskSearchEngine();
+ }
+
+ public function getDoneURI() {
+ $board_uri = $this->getBoardURI();
+ if ($board_uri) {
+ return $board_uri;
+ }
+
+ return parent::getDoneURI();
+ }
+
+ public function getCancelURI() {
+ $board_uri = $this->getBoardURI();
+ if ($board_uri) {
+ return $board_uri;
+ }
+
+ return parent::getCancelURI();
+ }
+
+ private function getBoardURI() {
+ $workboard = $this->getWorkboard();
+
+ if ($workboard) {
+ $project_id = $workboard->getID();
+ return "/project/board/{$project_id}/";
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
deleted file mode 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ /dev/null
@@ -1,255 +0,0 @@
-<?php
-
-final class ManiphestBatchEditController extends ManiphestController {
-
- public function handleRequest(AphrontRequest $request) {
- $viewer = $this->getViewer();
-
- $this->requireApplicationCapability(
- ManiphestBulkEditCapability::CAPABILITY);
-
- $project = null;
- $board_id = $request->getInt('board');
- if ($board_id) {
- $project = id(new PhabricatorProjectQuery())
- ->setViewer($viewer)
- ->withIDs(array($board_id))
- ->executeOne();
- if (!$project) {
- return new Aphront404Response();
- }
- }
-
- $task_ids = $request->getArr('batch');
- if (!$task_ids) {
- $task_ids = $request->getStrList('batch');
- }
-
- if (!$task_ids) {
- throw new Exception(
- pht(
- 'No tasks are selected.'));
- }
-
- $tasks = id(new ManiphestTaskQuery())
- ->setViewer($viewer)
- ->withIDs($task_ids)
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->needSubscriberPHIDs(true)
- ->needProjectPHIDs(true)
- ->execute();
-
- if (!$tasks) {
- throw new Exception(
- pht("You don't have permission to edit any of the selected tasks."));
- }
-
- if ($project) {
- $cancel_uri = '/project/board/'.$project->getID().'/';
- $redirect_uri = $cancel_uri;
- } else {
- $cancel_uri = '/maniphest/';
- $redirect_uri = '/maniphest/?ids='.implode(',', mpull($tasks, 'getID'));
- }
-
- $actions = $request->getStr('actions');
- if ($actions) {
- $actions = phutil_json_decode($actions);
- }
-
- if ($request->isFormPost() && $actions) {
- $job = PhabricatorWorkerBulkJob::initializeNewJob(
- $viewer,
- new ManiphestTaskEditBulkJobType(),
- array(
- 'taskPHIDs' => mpull($tasks, 'getPHID'),
- 'actions' => $actions,
- 'cancelURI' => $cancel_uri,
- 'doneURI' => $redirect_uri,
- ));
-
- $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
-
- $xactions = array();
- $xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
- ->setTransactionType($type_status)
- ->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM);
-
- $editor = id(new PhabricatorWorkerBulkJobEditor())
- ->setActor($viewer)
- ->setContentSourceFromRequest($request)
- ->setContinueOnMissingFields(true)
- ->applyTransactions($job, $xactions);
-
- return id(new AphrontRedirectResponse())
- ->setURI($job->getMonitorURI());
- }
-
- $list = $this->newBulkObjectList($tasks);
-
- $template = new AphrontTokenizerTemplateView();
- $template = $template->render();
-
- $projects_source = new PhabricatorProjectDatasource();
- $mailable_source = new PhabricatorMetaMTAMailableDatasource();
- $mailable_source->setViewer($viewer);
- $owner_source = new ManiphestAssigneeDatasource();
- $owner_source->setViewer($viewer);
- $spaces_source = id(new PhabricatorSpacesNamespaceDatasource())
- ->setViewer($viewer);
-
- require_celerity_resource('maniphest-batch-editor');
- Javelin::initBehavior(
- 'maniphest-batch-editor',
- array(
- 'root' => 'maniphest-batch-edit-form',
- 'tokenizerTemplate' => $template,
- 'sources' => array(
- 'project' => array(
- 'src' => $projects_source->getDatasourceURI(),
- 'placeholder' => $projects_source->getPlaceholderText(),
- 'browseURI' => $projects_source->getBrowseURI(),
- ),
- 'owner' => array(
- 'src' => $owner_source->getDatasourceURI(),
- 'placeholder' => $owner_source->getPlaceholderText(),
- 'browseURI' => $owner_source->getBrowseURI(),
- 'limit' => 1,
- ),
- 'cc' => array(
- 'src' => $mailable_source->getDatasourceURI(),
- 'placeholder' => $mailable_source->getPlaceholderText(),
- 'browseURI' => $mailable_source->getBrowseURI(),
- ),
- 'spaces' => array(
- 'src' => $spaces_source->getDatasourceURI(),
- 'placeholder' => $spaces_source->getPlaceholderText(),
- 'browseURI' => $spaces_source->getBrowseURI(),
- 'limit' => 1,
- ),
- ),
- 'input' => 'batch-form-actions',
- 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
- 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
- ));
-
- $form = id(new PHUIFormLayoutView())
- ->setUser($viewer);
-
- $form->appendChild(
- phutil_tag(
- 'input',
- array(
- 'type' => 'hidden',
- 'name' => 'actions',
- 'id' => 'batch-form-actions',
- )));
-
- $form->appendChild(
- id(new PHUIFormInsetView())
- ->setTitle(pht('Actions'))
- ->setRightButton(javelin_tag(
- 'a',
- array(
- 'href' => '#',
- 'class' => 'button button-green',
- 'sigil' => 'add-action',
- 'mustcapture' => true,
- ),
- pht('Add Another Action')))
- ->setContent(javelin_tag(
- 'table',
- array(
- 'sigil' => 'maniphest-batch-actions',
- 'class' => 'maniphest-batch-actions-table',
- ),
- '')))
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->setValue(pht('Update Tasks'))
- ->addCancelButton($cancel_uri));
-
- $title = pht('Batch Editor');
-
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb($title);
- $crumbs->setBorder(true);
-
- $header = id(new PHUIHeaderView())
- ->setHeader(pht('Batch Editor'))
- ->setHeaderIcon('fa-pencil-square-o');
-
- $task_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Selected Tasks'))
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setObjectList($list);
-
- $form_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Actions'))
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setForm($form);
-
-
- $complete_form = phabricator_form(
- $viewer,
- array(
- 'action' => $request->getRequestURI(),
- 'method' => 'POST',
- 'id' => 'maniphest-batch-edit-form',
- ),
- array(
- phutil_tag(
- 'input',
- array(
- 'type' => 'hidden',
- 'name' => 'board',
- 'value' => $board_id,
- )),
- $task_box,
- $form_box,
- ));
-
- $view = id(new PHUITwoColumnView())
- ->setHeader($header)
- ->setFooter($complete_form);
-
- return $this->newPage()
- ->setTitle($title)
- ->setCrumbs($crumbs)
- ->appendChild($view);
- }
-
- private function newBulkObjectList(array $objects) {
- $viewer = $this->getViewer();
- $objects = mpull($objects, null, 'getPHID');
-
- $handles = $viewer->loadHandles(array_keys($objects));
-
- $status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
-
- $list = id(new PHUIObjectItemListView())
- ->setViewer($viewer)
- ->setFlush(true);
-
- foreach ($objects as $phid => $object) {
- $handle = $handles[$phid];
-
- $is_closed = ($handle->getStatus() === $status_closed);
-
- $item = id(new PHUIObjectItemView())
- ->setHeader($handle->getFullName())
- ->setHref($handle->getURI())
- ->setDisabled($is_closed)
- ->setSelectable('batch[]', $object->getID(), true);
-
- $list->addItem($item);
- }
-
- return $list;
- }
-
-}
diff --git a/src/applications/maniphest/controller/ManiphestBulkEditController.php b/src/applications/maniphest/controller/ManiphestBulkEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/maniphest/controller/ManiphestBulkEditController.php
@@ -0,0 +1,32 @@
+<?php
+
+final class ManiphestBulkEditController extends ManiphestController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $this->requireApplicationCapability(
+ ManiphestBulkEditCapability::CAPABILITY);
+
+ $bulk_engine = id(new ManiphestTaskBulkEngine())
+ ->setViewer($viewer)
+ ->setController($this)
+ ->addContextParameter('board');
+
+ $board_id = $request->getInt('board');
+ if ($board_id) {
+ $project = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($board_id))
+ ->executeOne();
+ if (!$project) {
+ return new Aphront404Response();
+ }
+
+ $bulk_engine->setWorkboard($project);
+ }
+
+ return $bulk_engine->buildResponse();
+ }
+
+}
diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php
--- a/src/applications/maniphest/view/ManiphestTaskResultListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php
@@ -255,7 +255,7 @@
$user,
array(
'method' => 'POST',
- 'action' => '/maniphest/batch/',
+ 'action' => '/maniphest/bulk/',
'id' => 'batch-select-form',
),
$editor);
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
@@ -230,14 +230,23 @@
->addCancelButton($board_uri);
}
- $batch_ids = mpull($batch_tasks, 'getID');
- $batch_ids = implode(',', $batch_ids);
+ // 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->setQueryParam('board', $this->id);
- $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);
+ ->setURI($bulk_uri);
}
$move_id = $request->getStr('move');
@@ -1048,7 +1057,7 @@
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-list-ul')
- ->setName(pht('Batch Edit Tasks...'))
+ ->setName(pht('Bulk Edit Tasks...'))
->setHref($batch_edit_uri)
->setDisabled(!$can_batch_edit);
diff --git a/src/applications/transactions/bulk/PhabricatorBulkEngine.php b/src/applications/transactions/bulk/PhabricatorBulkEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/bulk/PhabricatorBulkEngine.php
@@ -0,0 +1,454 @@
+<?php
+
+abstract class PhabricatorBulkEngine extends Phobject {
+
+ private $viewer;
+ private $controller;
+ private $context = array();
+ private $objectList;
+ private $savedQuery;
+ private $editableList;
+ private $targetList;
+
+ abstract public function newSearchEngine();
+
+ public function getCancelURI() {
+ $saved_query = $this->savedQuery;
+ if ($saved_query) {
+ $path = '/query/'.$saved_query->getQueryKey().'/';
+ } else {
+ $path = '/';
+ }
+
+ return $this->getQueryURI($path);
+ }
+
+ public function getDoneURI() {
+ if ($this->objectList !== null) {
+ $ids = mpull($this->objectList, 'getID');
+ $path = '/?ids='.implode(',', $ids);
+ } else {
+ $path = '/';
+ }
+
+ return $this->getQueryURI($path);
+ }
+
+ protected function getQueryURI($path = '/') {
+ $viewer = $this->getViewer();
+
+ $engine = id($this->newSearchEngine())
+ ->setViewer($viewer);
+
+ return $engine->getQueryBaseURI().ltrim($path, '/');
+ }
+
+ protected function getBulkURI() {
+ $saved_query = $this->savedQuery;
+ if ($saved_query) {
+ $path = '/query/'.$saved_query->getQueryKey().'/';
+ } else {
+ $path = '/';
+ }
+
+ return $this->getBulkBaseURI($path);
+ }
+
+ protected function getBulkBaseURI($path) {
+ return $this->getQueryURI('bulk/'.ltrim($path, '/'));
+ }
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setController(PhabricatorController $controller) {
+ $this->controller = $controller;
+ return $this;
+ }
+
+ final public function getController() {
+ return $this->controller;
+ }
+
+ final public function addContextParameter($key) {
+ $this->context[$key] = true;
+ return $this;
+ }
+
+ final public function buildResponse() {
+ $viewer = $this->getViewer();
+ $controller = $this->getController();
+ $request = $controller->getRequest();
+
+ $response = $this->loadObjectList();
+ if ($response) {
+ return $response;
+ }
+
+ if ($request->isFormPost() && $request->getBool('bulkEngine')) {
+ return $this->buildEditResponse();
+ }
+
+ $list_view = $this->newBulkObjectList();
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Bulk Editor'))
+ ->setHeaderIcon('fa-pencil-square-o');
+
+ $list_box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Working Set'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setObjectList($list_view);
+
+ $form_view = $this->newBulkActionForm();
+
+ $form_box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Actions'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setForm($form_view);
+
+ $complete_form = phabricator_form(
+ $viewer,
+ array(
+ 'action' => $this->getBulkURI(),
+ 'method' => 'POST',
+ 'id' => 'maniphest-batch-edit-form',
+ ),
+ array(
+ $this->newContextInputs(),
+ $list_box,
+ $form_box,
+ ));
+
+ $column_view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter($complete_form);
+
+ // TODO: This is a bit hacky and inflexible.
+ $crumbs = $controller->buildApplicationCrumbsForEditEngine();
+ $crumbs->addTextCrumb(pht('Query'), $this->getCancelURI());
+ $crumbs->addTextCrumb(pht('Bulk Editor'));
+
+ return $controller->newPage()
+ ->setTitle(pht('Bulk Edit'))
+ ->setCrumbs($crumbs)
+ ->appendChild($column_view);
+ }
+
+ private function loadObjectList() {
+ $viewer = $this->getViewer();
+ $controller = $this->getController();
+ $request = $controller->getRequest();
+
+ $search_engine = id($this->newSearchEngine())
+ ->setViewer($viewer);
+
+ $query_key = $request->getURIData('queryKey');
+ if (strlen($query_key)) {
+ 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();
+ }
+ }
+ } else {
+ // TODO: For now, since we don't deal gracefully with queries which
+ // match a huge result set, just bail if we don't have any query
+ // parameters instead of querying for a trillion tasks and timing out.
+ $request_data = $request->getPassthroughRequestData();
+ if (!$request_data) {
+ throw new Exception(
+ pht(
+ 'Expected a query key or a set of query constraints.'));
+ }
+
+ $saved = $search_engine->buildSavedQueryFromRequest($request);
+ $search_engine->saveQuery($saved);
+ }
+
+ $object_query = $search_engine->buildQueryFromSavedQuery($saved)
+ ->setViewer($viewer);
+ $object_list = $object_query->execute();
+ $object_list = mpull($object_list, null, 'getPHID');
+
+ // If the user has submitted the bulk edit form, select only the objects
+ // they checked.
+ if ($request->getBool('bulkEngine')) {
+ $target_phids = $request->getArr('bulkTargetPHIDs');
+
+ // NOTE: It's possible that the underlying query result set has changed
+ // between the time we ran the query initially and now: for example, the
+ // query was for "Open Tasks" and some tasks were closed while the user
+ // was making action selections.
+
+ // This could result in some objects getting dropped from the working set
+ // here: we'll have target PHIDs for them, but they will no longer be
+ // part of the object list. For now, just go with this since it doesn't
+ // seem like a big problem and may even be desirable.
+
+ $this->targetList = array_select_keys($object_list, $target_phids);
+ } else {
+ $this->targetList = $object_list;
+ }
+
+ $this->objectList = $object_list;
+ $this->savedQuery = $saved;
+
+ // Filter just the editable objects. We show all the objects which the
+ // query matches whether they're editable or not, but indicate which ones
+ // can not be edited to the user.
+
+ $editable_list = id(new PhabricatorPolicyFilter())
+ ->setViewer($viewer)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->apply($object_list);
+ $this->editableList = mpull($editable_list, null, 'getPHID');
+
+ return null;
+ }
+
+ private function newBulkObjectList() {
+ $viewer = $this->getViewer();
+
+ $objects = $this->objectList;
+ $objects = mpull($objects, null, 'getPHID');
+
+ $handles = $viewer->loadHandles(array_keys($objects));
+
+ $status_closed = PhabricatorObjectHandle::STATUS_CLOSED;
+
+ $list = id(new PHUIObjectItemListView())
+ ->setViewer($viewer)
+ ->setFlush(true);
+
+ foreach ($objects as $phid => $object) {
+ $handle = $handles[$phid];
+
+ $is_closed = ($handle->getStatus() === $status_closed);
+ $can_edit = isset($this->editableList[$phid]);
+ $is_disabled = ($is_closed || !$can_edit);
+ $is_selected = isset($this->targetList[$phid]);
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($handle->getFullName())
+ ->setHref($handle->getURI())
+ ->setDisabled($is_disabled)
+ ->setSelectable('bulkTargetPHIDs[]', $phid, $is_selected, !$can_edit);
+
+ if (!$can_edit) {
+ $item->addIcon('fa-pencil red', pht('Not Editable'));
+ }
+
+ $list->addItem($item);
+ }
+
+ return $list;
+ }
+
+ private function newContextInputs() {
+ $viewer = $this->getViewer();
+ $controller = $this->getController();
+ $request = $controller->getRequest();
+
+ $parameters = array();
+ foreach ($this->context as $key => $value) {
+ $parameters[$key] = $request->getStr($key);
+ }
+
+ $parameters = array(
+ 'bulkEngine' => 1,
+ ) + $parameters;
+
+ $result = array();
+ foreach ($parameters as $key => $value) {
+ $result[] = phutil_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => $key,
+ 'value' => $value,
+ ));
+ }
+
+ return $result;
+ }
+
+ private function newBulkActionForm() {
+ $viewer = $this->getViewer();
+
+ $cancel_uri = $this->getCancelURI();
+
+ $template = new AphrontTokenizerTemplateView();
+ $template = $template->render();
+
+ $projects_source = new PhabricatorProjectDatasource();
+ $mailable_source = new PhabricatorMetaMTAMailableDatasource();
+ $mailable_source->setViewer($viewer);
+ $owner_source = new ManiphestAssigneeDatasource();
+ $owner_source->setViewer($viewer);
+ $spaces_source = id(new PhabricatorSpacesNamespaceDatasource())
+ ->setViewer($viewer);
+
+ require_celerity_resource('maniphest-batch-editor');
+
+ Javelin::initBehavior(
+ 'maniphest-batch-editor',
+ array(
+ 'root' => 'maniphest-batch-edit-form',
+ 'tokenizerTemplate' => $template,
+ 'sources' => array(
+ 'project' => array(
+ 'src' => $projects_source->getDatasourceURI(),
+ 'placeholder' => $projects_source->getPlaceholderText(),
+ 'browseURI' => $projects_source->getBrowseURI(),
+ ),
+ 'owner' => array(
+ 'src' => $owner_source->getDatasourceURI(),
+ 'placeholder' => $owner_source->getPlaceholderText(),
+ 'browseURI' => $owner_source->getBrowseURI(),
+ 'limit' => 1,
+ ),
+ 'cc' => array(
+ 'src' => $mailable_source->getDatasourceURI(),
+ 'placeholder' => $mailable_source->getPlaceholderText(),
+ 'browseURI' => $mailable_source->getBrowseURI(),
+ ),
+ 'spaces' => array(
+ 'src' => $spaces_source->getDatasourceURI(),
+ 'placeholder' => $spaces_source->getPlaceholderText(),
+ 'browseURI' => $spaces_source->getBrowseURI(),
+ 'limit' => 1,
+ ),
+ ),
+ 'input' => 'batch-form-actions',
+ 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
+ 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
+ ));
+
+ $form = id(new PHUIFormLayoutView())
+ ->setUser($viewer);
+
+ $form->appendChild(
+ phutil_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => 'actions',
+ 'id' => 'batch-form-actions',
+ )));
+
+ $form->appendChild(
+ id(new PHUIFormInsetView())
+ ->setTitle(pht('Bulk Edit Actions'))
+ ->setRightButton(
+ javelin_tag(
+ 'a',
+ array(
+ 'href' => '#',
+ 'class' => 'button button-green',
+ 'sigil' => 'add-action',
+ 'mustcapture' => true,
+ ),
+ pht('Add Another Action')))
+ ->setContent(
+ javelin_tag(
+ 'table',
+ array(
+ 'sigil' => 'maniphest-batch-actions',
+ 'class' => 'maniphest-batch-actions-table',
+ ),
+ '')))
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue(pht('Apply Bulk Edit'))
+ ->addCancelButton($cancel_uri));
+
+ return $form;
+ }
+
+ private function buildEditResponse() {
+ $viewer = $this->getViewer();
+ $controller = $this->getController();
+ $request = $controller->getRequest();
+
+ if (!$this->objectList) {
+ throw new Exception(pht('Query does not match any objects.'));
+ }
+
+ if (!$this->editableList) {
+ throw new Exception(
+ pht(
+ 'Query does not match any objects you have permission to edit.'));
+ }
+
+ // Restrict the selection set to objects the user can actually edit.
+ $objects = array_intersect_key($this->editableList, $this->targetList);
+
+ if (!$objects) {
+ throw new Exception(
+ pht(
+ 'You have not selected any objects to edit.'));
+ }
+
+ $raw_actions = $request->getStr('actions');
+ if ($raw_actions) {
+ $actions = phutil_json_decode($raw_actions);
+ } else {
+ $actions = array();
+ }
+
+ if (!$actions) {
+ throw new Exception(
+ pht(
+ 'You have not chosen any edits to apply.'));
+ }
+
+ $cancel_uri = $this->getCancelURI();
+ $done_uri = $this->getDoneURI();
+
+ $job = PhabricatorWorkerBulkJob::initializeNewJob(
+ $viewer,
+ // TODO: This is a Maniphest-specific job type for now, but will become
+ // a generic one so it gets to live here for now instead of in the task
+ // specific BulkEngine subclass.
+ new ManiphestTaskEditBulkJobType(),
+ array(
+ 'taskPHIDs' => mpull($objects, 'getPHID'),
+ 'actions' => $actions,
+ 'cancelURI' => $cancel_uri,
+ 'doneURI' => $done_uri,
+ ));
+
+ $type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
+
+ $xactions = array();
+ $xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
+ ->setTransactionType($type_status)
+ ->setNewValue(PhabricatorWorkerBulkJob::STATUS_CONFIRM);
+
+ $editor = id(new PhabricatorWorkerBulkJobEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnMissingFields(true)
+ ->applyTransactions($job, $xactions);
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($job->getMonitorURI());
+ }
+
+}
diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php
--- a/src/view/phui/PHUIObjectItemView.php
+++ b/src/view/phui/PHUIObjectItemView.php
@@ -32,6 +32,7 @@
private $selectableName;
private $selectableValue;
private $isSelected;
+ private $isForbidden;
public function setDisabled($disabled) {
$this->disabled = $disabled;
@@ -164,10 +165,17 @@
return $this;
}
- public function setSelectable($name, $value, $is_selected) {
+ public function setSelectable(
+ $name,
+ $value,
+ $is_selected,
+ $is_forbidden = false) {
+
$this->selectableName = $name;
$this->selectableValue = $value;
$this->isSelected = $is_selected;
+ $this->isForbidden = $is_forbidden;
+
return $this;
}
@@ -299,11 +307,13 @@
throw new Exception(pht('Invalid effect!'));
}
- if ($this->isSelected) {
+ if ($this->isForbidden) {
+ $item_classes[] = 'phui-oi-forbidden';
+ } else if ($this->isSelected) {
$item_classes[] = 'phui-oi-selected';
}
- if ($this->selectableName !== null) {
+ if ($this->selectableName !== null && !$this->isForbidden) {
$item_classes[] = 'phui-oi-selectable';
$sigils[] = 'phui-oi-selectable';
@@ -654,14 +664,18 @@
}
if ($this->selectableName !== null) {
- $checkbox = phutil_tag(
- 'input',
- array(
- 'type' => 'checkbox',
- 'name' => $this->selectableName,
- 'value' => $this->selectableValue,
- 'checked' => ($this->isSelected ? 'checked' : null),
- ));
+ if (!$this->isForbidden) {
+ $checkbox = phutil_tag(
+ 'input',
+ array(
+ 'type' => 'checkbox',
+ 'name' => $this->selectableName,
+ 'value' => $this->selectableValue,
+ 'checked' => ($this->isSelected ? 'checked' : null),
+ ));
+ } else {
+ $checkbox = null;
+ }
$column0 = phutil_tag(
'div',
diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
--- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
+++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
@@ -455,6 +455,10 @@
border-color: {$sh-blueborder};
}
+.phui-oi-forbidden {
+ background: {$sh-redbackground};
+}
+
/* - Handle Icons --------------------------------------------------------------
diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
--- a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
@@ -157,12 +157,15 @@
'submit',
null,
function() {
- var inputs = [];
+ var ids = [];
for (var k in selected) {
- inputs.push(
- JX.$N('input', {type: 'hidden', name: 'batch[]', value: k}));
+ ids.push(k);
}
- JX.DOM.setContent(JX.$(config.idContainer), inputs);
+ ids = ids.join(',');
+
+ var input = JX.$N('input', {type: 'hidden', name: 'ids', value: ids});
+
+ JX.DOM.setContent(JX.$(config.idContainer), input);
});
update();

File Metadata

Mime Type
text/plain
Expires
Oct 18 2024, 4:12 AM (4 w, 2 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/n6/jt/igqr54ncipt44si3
Default Alt Text
D18806.diff (37 KB)

Event Timeline