Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14860195
D18806.id45121.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
37 KB
Referenced Files
None
Subscribers
None
D18806.id45121.diff
View Options
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' => 'c25f844b',
+ 'core.pkg.css' => '99eb9d1d',
'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',
),
@@ -1806,6 +1800,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
@@ -1474,8 +1474,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',
@@ -1534,6 +1534,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',
@@ -2184,6 +2185,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',
@@ -6645,8 +6647,8 @@
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
- 'ManiphestBatchEditController' => 'ManiphestController',
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
+ 'ManiphestBulkEditController' => 'ManiphestController',
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
@@ -6728,6 +6730,7 @@
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
+ 'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine',
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
@@ -7453,6 +7456,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
Details
Attached
Mime Type
text/plain
Expires
Sat, Feb 8, 1:27 AM (21 h, 5 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7099641
Default Alt Text
D18806.id45121.diff (37 KB)
Attached To
Mode
D18806: Rebuild the bulk editor on SearchEngine
Attached
Detach File
Event Timeline
Log In to Comment