mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 14:52:41 +01:00
Rebuild the bulk editor on SearchEngine
Summary: Depends on D18805. Ref T13025. Fixes T10268. Instead of using a list of IDs for the bulk editor, power it with SearchEngine queries. This gives us the full power of SearchEngine and lets us use a query key instead of a list of 20,000 IDs to avoid issues with URL lengths. Also, split it into a base `BulkEngine` and per-application subclasses. This moves us toward T10005 and universal support for bulk operations. Also: - Renames most of "batch" to "bulk": we're curently inconsitent about this, I like "bulk" better since I think it's more clear if you don't regularly interact with `.bat` files, and newer stuff mostly uses "bulk". - When objects in the result set can't be edited because you don't have permission, show the status more clearly. This probably breaks some stuff a bit since I refactored so heavily, but it seems mostly OK from poking around. I'll clean up anything I missed in followups to deal with remaining items on T13025. Test Plan: {F5302300} - Bulk edited from Maniphest. - Bulk edited from a workboard (no more giant `?ids=....` in the URL). - Hit most of the error conditions, I think? - Clicked the "Cancel" button. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13025, T10268 Differential Revision: https://secure.phabricator.com/D18806
This commit is contained in:
parent
ad659627b3
commit
7f91c8c4ac
13 changed files with 612 additions and 293 deletions
|
@ -9,7 +9,7 @@ return array(
|
||||||
'names' => array(
|
'names' => array(
|
||||||
'conpherence.pkg.css' => 'e68cf1fa',
|
'conpherence.pkg.css' => 'e68cf1fa',
|
||||||
'conpherence.pkg.js' => '15191c65',
|
'conpherence.pkg.js' => '15191c65',
|
||||||
'core.pkg.css' => '5be8063f',
|
'core.pkg.css' => '075f9867',
|
||||||
'core.pkg.js' => '4c79d74f',
|
'core.pkg.js' => '4c79d74f',
|
||||||
'darkconsole.pkg.js' => '1f9a31bc',
|
'darkconsole.pkg.js' => '1f9a31bc',
|
||||||
'differential.pkg.css' => '45951e9e',
|
'differential.pkg.css' => '45951e9e',
|
||||||
|
@ -18,7 +18,7 @@ return array(
|
||||||
'diffusion.pkg.js' => '6134c5a1',
|
'diffusion.pkg.js' => '6134c5a1',
|
||||||
'favicon.ico' => '30672e08',
|
'favicon.ico' => '30672e08',
|
||||||
'maniphest.pkg.css' => '4845691a',
|
'maniphest.pkg.css' => '4845691a',
|
||||||
'maniphest.pkg.js' => '5ab2753f',
|
'maniphest.pkg.js' => '4d7e79c8',
|
||||||
'rsrc/audio/basic/alert.mp3' => '98461568',
|
'rsrc/audio/basic/alert.mp3' => '98461568',
|
||||||
'rsrc/audio/basic/bing.mp3' => 'ab8603a5',
|
'rsrc/audio/basic/bing.mp3' => 'ab8603a5',
|
||||||
'rsrc/audio/basic/pock.mp3' => '0cc772f5',
|
'rsrc/audio/basic/pock.mp3' => '0cc772f5',
|
||||||
|
@ -135,7 +135,7 @@ return array(
|
||||||
'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77',
|
'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-drag-ui.css' => '08f4ccc3',
|
||||||
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6',
|
'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/object-item/phui-oi-simple-ui.css' => 'a8beebea',
|
||||||
'rsrc/css/phui/phui-action-list.css' => 'f7f61a34',
|
'rsrc/css/phui/phui-action-list.css' => 'f7f61a34',
|
||||||
'rsrc/css/phui/phui-action-panel.css' => 'b4798122',
|
'rsrc/css/phui/phui-action-panel.css' => 'b4798122',
|
||||||
|
@ -420,7 +420,7 @@ return array(
|
||||||
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
|
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
|
||||||
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
|
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
|
||||||
'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7',
|
'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-line-chart.js' => 'e4232876',
|
||||||
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
|
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
|
||||||
'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763',
|
'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763',
|
||||||
|
@ -643,7 +643,7 @@ return array(
|
||||||
'javelin-behavior-line-chart' => 'e4232876',
|
'javelin-behavior-line-chart' => 'e4232876',
|
||||||
'javelin-behavior-load-blame' => '42126667',
|
'javelin-behavior-load-blame' => '42126667',
|
||||||
'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
|
'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-list-editor' => 'a9f88de2',
|
||||||
'javelin-behavior-maniphest-subpriority-editor' => '71237763',
|
'javelin-behavior-maniphest-subpriority-editor' => '71237763',
|
||||||
'javelin-behavior-owners-path-editor' => '7a68dda3',
|
'javelin-behavior-owners-path-editor' => '7a68dda3',
|
||||||
|
@ -862,7 +862,7 @@ return array(
|
||||||
'phui-oi-color-css' => 'cd2b9b77',
|
'phui-oi-color-css' => 'cd2b9b77',
|
||||||
'phui-oi-drag-ui-css' => '08f4ccc3',
|
'phui-oi-drag-ui-css' => '08f4ccc3',
|
||||||
'phui-oi-flush-ui-css' => '9d9685d6',
|
'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-oi-simple-ui-css' => 'a8beebea',
|
||||||
'phui-pager-css' => 'edcbc226',
|
'phui-pager-css' => 'edcbc226',
|
||||||
'phui-pinboard-view-css' => '2495140e',
|
'phui-pinboard-view-css' => '2495140e',
|
||||||
|
@ -960,12 +960,6 @@ return array(
|
||||||
'javelin-stratcom',
|
'javelin-stratcom',
|
||||||
'javelin-workflow',
|
'javelin-workflow',
|
||||||
),
|
),
|
||||||
'0825c27a' => array(
|
|
||||||
'javelin-behavior',
|
|
||||||
'javelin-dom',
|
|
||||||
'javelin-stratcom',
|
|
||||||
'javelin-util',
|
|
||||||
),
|
|
||||||
'08f4ccc3' => array(
|
'08f4ccc3' => array(
|
||||||
'phui-oi-list-view-css',
|
'phui-oi-list-view-css',
|
||||||
),
|
),
|
||||||
|
@ -1815,6 +1809,12 @@ return array(
|
||||||
'phuix-autocomplete',
|
'phuix-autocomplete',
|
||||||
'javelin-mask',
|
'javelin-mask',
|
||||||
),
|
),
|
||||||
|
'ad54037e' => array(
|
||||||
|
'javelin-behavior',
|
||||||
|
'javelin-dom',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-util',
|
||||||
|
),
|
||||||
'b003d4fb' => array(
|
'b003d4fb' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-stratcom',
|
'javelin-stratcom',
|
||||||
|
|
|
@ -1487,8 +1487,8 @@ phutil_register_library_map(array(
|
||||||
'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
|
'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
|
||||||
'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
|
'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
|
||||||
'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
|
'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
|
||||||
'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php',
|
|
||||||
'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
|
'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
|
||||||
|
'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php',
|
||||||
'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
|
'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
|
||||||
'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
|
'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
|
||||||
'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
|
'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
|
||||||
|
@ -1547,6 +1547,7 @@ phutil_register_library_map(array(
|
||||||
'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php',
|
'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php',
|
||||||
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
|
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
|
||||||
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
|
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
|
||||||
|
'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php',
|
||||||
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
|
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
|
||||||
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
|
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
|
||||||
'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php',
|
'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php',
|
||||||
|
@ -2198,6 +2199,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
|
'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
|
||||||
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
|
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
|
||||||
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
|
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
|
||||||
|
'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
|
||||||
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
|
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
|
||||||
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
|
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
|
||||||
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
|
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
|
||||||
|
@ -6678,8 +6680,8 @@ phutil_register_library_map(array(
|
||||||
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
|
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
|
||||||
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
|
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
|
||||||
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||||
'ManiphestBatchEditController' => 'ManiphestController',
|
|
||||||
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
|
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
|
||||||
|
'ManiphestBulkEditController' => 'ManiphestController',
|
||||||
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
|
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
|
||||||
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
|
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
|
||||||
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
|
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
|
@ -6761,6 +6763,7 @@ phutil_register_library_map(array(
|
||||||
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
|
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
|
||||||
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
|
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
|
||||||
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
|
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
|
||||||
|
'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine',
|
||||||
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
|
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
|
||||||
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
|
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||||
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
|
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
|
||||||
|
@ -7487,6 +7490,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
|
'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
|
||||||
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
|
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
|
||||||
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
|
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
|
||||||
|
'PhabricatorBulkEngine' => 'Phobject',
|
||||||
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
|
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
|
||||||
'PhabricatorCacheEngine' => 'Phobject',
|
'PhabricatorCacheEngine' => 'Phobject',
|
||||||
'PhabricatorCacheEngineExtension' => 'Phobject',
|
'PhabricatorCacheEngineExtension' => 'Phobject',
|
||||||
|
|
|
@ -618,6 +618,10 @@ abstract class PhabricatorApplication
|
||||||
')?';
|
')?';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getBulkRoutePattern($base = null) {
|
||||||
|
return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
|
||||||
|
}
|
||||||
|
|
||||||
protected function getQueryRoutePattern($base = null) {
|
protected function getQueryRoutePattern($base = null) {
|
||||||
return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
|
return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication {
|
||||||
'/maniphest/' => array(
|
'/maniphest/' => array(
|
||||||
'(?:query/(?P<queryKey>[^/]+)/)?' => 'ManiphestTaskListController',
|
'(?:query/(?P<queryKey>[^/]+)/)?' => 'ManiphestTaskListController',
|
||||||
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
|
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
|
||||||
'batch/' => 'ManiphestBatchEditController',
|
$this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController',
|
||||||
'task/' => array(
|
'task/' => array(
|
||||||
$this->getEditRoutePattern('edit/')
|
$this->getEditRoutePattern('edit/')
|
||||||
=> 'ManiphestTaskEditController',
|
=> 'ManiphestTaskEditController',
|
||||||
|
|
50
src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php
Normal file
50
src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -255,7 +255,7 @@ final class ManiphestTaskResultListView extends ManiphestView {
|
||||||
$user,
|
$user,
|
||||||
array(
|
array(
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'action' => '/maniphest/batch/',
|
'action' => '/maniphest/bulk/',
|
||||||
'id' => 'batch-select-form',
|
'id' => 'batch-select-form',
|
||||||
),
|
),
|
||||||
$editor);
|
$editor);
|
||||||
|
|
|
@ -230,14 +230,23 @@ final class PhabricatorProjectBoardViewController
|
||||||
->addCancelButton($board_uri);
|
->addCancelButton($board_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
$batch_ids = mpull($batch_tasks, 'getID');
|
// Create a saved query to hold the working set. This allows us to get
|
||||||
$batch_ids = implode(',', $batch_ids);
|
// 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())
|
return id(new AphrontRedirectResponse())
|
||||||
->setURI($batch_uri);
|
->setURI($bulk_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
$move_id = $request->getStr('move');
|
$move_id = $request->getStr('move');
|
||||||
|
@ -1048,7 +1057,7 @@ final class PhabricatorProjectBoardViewController
|
||||||
|
|
||||||
$column_items[] = id(new PhabricatorActionView())
|
$column_items[] = id(new PhabricatorActionView())
|
||||||
->setIcon('fa-list-ul')
|
->setIcon('fa-list-ul')
|
||||||
->setName(pht('Batch Edit Tasks...'))
|
->setName(pht('Bulk Edit Tasks...'))
|
||||||
->setHref($batch_edit_uri)
|
->setHref($batch_edit_uri)
|
||||||
->setDisabled(!$can_batch_edit);
|
->setDisabled(!$can_batch_edit);
|
||||||
|
|
||||||
|
|
454
src/applications/transactions/bulk/PhabricatorBulkEngine.php
Normal file
454
src/applications/transactions/bulk/PhabricatorBulkEngine.php
Normal file
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ final class PHUIObjectItemView extends AphrontTagView {
|
||||||
private $selectableName;
|
private $selectableName;
|
||||||
private $selectableValue;
|
private $selectableValue;
|
||||||
private $isSelected;
|
private $isSelected;
|
||||||
|
private $isForbidden;
|
||||||
|
|
||||||
public function setDisabled($disabled) {
|
public function setDisabled($disabled) {
|
||||||
$this->disabled = $disabled;
|
$this->disabled = $disabled;
|
||||||
|
@ -164,10 +165,17 @@ final class PHUIObjectItemView extends AphrontTagView {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setSelectable($name, $value, $is_selected) {
|
public function setSelectable(
|
||||||
|
$name,
|
||||||
|
$value,
|
||||||
|
$is_selected,
|
||||||
|
$is_forbidden = false) {
|
||||||
|
|
||||||
$this->selectableName = $name;
|
$this->selectableName = $name;
|
||||||
$this->selectableValue = $value;
|
$this->selectableValue = $value;
|
||||||
$this->isSelected = $is_selected;
|
$this->isSelected = $is_selected;
|
||||||
|
$this->isForbidden = $is_forbidden;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,11 +307,13 @@ final class PHUIObjectItemView extends AphrontTagView {
|
||||||
throw new Exception(pht('Invalid effect!'));
|
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';
|
$item_classes[] = 'phui-oi-selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->selectableName !== null) {
|
if ($this->selectableName !== null && !$this->isForbidden) {
|
||||||
$item_classes[] = 'phui-oi-selectable';
|
$item_classes[] = 'phui-oi-selectable';
|
||||||
$sigils[] = 'phui-oi-selectable';
|
$sigils[] = 'phui-oi-selectable';
|
||||||
|
|
||||||
|
@ -654,14 +664,18 @@ final class PHUIObjectItemView extends AphrontTagView {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->selectableName !== null) {
|
if ($this->selectableName !== null) {
|
||||||
$checkbox = phutil_tag(
|
if (!$this->isForbidden) {
|
||||||
'input',
|
$checkbox = phutil_tag(
|
||||||
array(
|
'input',
|
||||||
'type' => 'checkbox',
|
array(
|
||||||
'name' => $this->selectableName,
|
'type' => 'checkbox',
|
||||||
'value' => $this->selectableValue,
|
'name' => $this->selectableName,
|
||||||
'checked' => ($this->isSelected ? 'checked' : null),
|
'value' => $this->selectableValue,
|
||||||
));
|
'checked' => ($this->isSelected ? 'checked' : null),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$checkbox = null;
|
||||||
|
}
|
||||||
|
|
||||||
$column0 = phutil_tag(
|
$column0 = phutil_tag(
|
||||||
'div',
|
'div',
|
||||||
|
|
|
@ -455,6 +455,10 @@ ul.phui-oi-list-view .phui-oi-selected
|
||||||
border-color: {$sh-blueborder};
|
border-color: {$sh-blueborder};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phui-oi-forbidden {
|
||||||
|
background: {$sh-redbackground};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* - Handle Icons --------------------------------------------------------------
|
/* - Handle Icons --------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -157,12 +157,15 @@ JX.behavior('maniphest-batch-selector', function(config) {
|
||||||
'submit',
|
'submit',
|
||||||
null,
|
null,
|
||||||
function() {
|
function() {
|
||||||
var inputs = [];
|
var ids = [];
|
||||||
for (var k in selected) {
|
for (var k in selected) {
|
||||||
inputs.push(
|
ids.push(k);
|
||||||
JX.$N('input', {type: 'hidden', name: 'batch[]', value: 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();
|
update();
|
||||||
|
|
Loading…
Reference in a new issue