From ad659627b3e931256e295efda941775c4020b713 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Nov 2017 05:14:05 -0800 Subject: [PATCH 01/80] Make bulk editor working set editable and more homogenous Summary: Ref T13025. See PHI50. Fixes T11286. Ref T10005. Begin modernizing the bulk editor. For T10005 ("move the bulk editor to modern infrastructure"), rewrite the rendering of the editable set so that it is application-agnostic and can work with any kind of object. For T11286 ("let users de-select items in the working set"), make the working set editable. Test Plan: {F5302158} - Deselected some objects, applied an edit, saw the edit apply to only selected objects. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13025, T11286, T10005 Differential Revision: https://secure.phabricator.com/D18805 --- resources/celerity/map.php | 17 ++-- .../ManiphestBatchEditController.php | 77 +++++++++++++------ src/view/phui/PHUIObjectItemView.php | 43 +++++++++++ .../phui/object-item/phui-oi-list-view.css | 19 +++++ webroot/rsrc/css/phui/phui-box.css | 4 + .../js/phui/behavior-phui-selectable-list.js | 44 +++++++++++ 6 files changed, 175 insertions(+), 29 deletions(-) create mode 100644 webroot/rsrc/js/phui/behavior-phui-selectable-list.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index d937b91e4f..0fdf44344f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'fdb27ef9', + 'core.pkg.css' => '5be8063f', 'core.pkg.js' => '4c79d74f', 'darkconsole.pkg.js' => '1f9a31bc', 'differential.pkg.css' => '45951e9e', @@ -135,14 +135,14 @@ return array( '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' => 'bf094950', + 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '73c5f5c4', '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', 'rsrc/css/phui/phui-badge.css' => '22c0cf4f', 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3', 'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c', - 'rsrc/css/phui/phui-box.css' => '9f3745fb', + 'rsrc/css/phui/phui-box.css' => '4bd6cdb9', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-cms.css' => '504b4b23', 'rsrc/css/phui/phui-comment-form.css' => 'ac68149f', @@ -523,6 +523,7 @@ return array( 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', + 'rsrc/js/phui/behavior-phui-selectable-list.js' => '464259a2', 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', @@ -673,6 +674,7 @@ return array( 'javelin-behavior-phui-dropdown-menu' => 'b95d6f7d', 'javelin-behavior-phui-file-upload' => 'b003d4fb', 'javelin-behavior-phui-hovercards' => 'bcaccd64', + 'javelin-behavior-phui-selectable-list' => '464259a2', 'javelin-behavior-phui-submenu' => 'a6f7a73b', 'javelin-behavior-phui-tab-group' => '0a0b10e9', 'javelin-behavior-phuix-example' => '68af71ca', @@ -820,7 +822,7 @@ return array( 'phui-badge-view-css' => '22c0cf4f', 'phui-basic-nav-view-css' => '98c11ab3', 'phui-big-info-view-css' => 'acc3492c', - 'phui-box-css' => '9f3745fb', + 'phui-box-css' => '4bd6cdb9', 'phui-button-bar-css' => 'f1ff5494', 'phui-button-css' => '1863cc6e', 'phui-button-simple-css' => '8e1baf68', @@ -860,7 +862,7 @@ return array( 'phui-oi-color-css' => 'cd2b9b77', 'phui-oi-drag-ui-css' => '08f4ccc3', 'phui-oi-flush-ui-css' => '9d9685d6', - 'phui-oi-list-view-css' => 'bf094950', + 'phui-oi-list-view-css' => '73c5f5c4', 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', @@ -1226,6 +1228,11 @@ return array( 'javelin-behavior', 'javelin-dom', ), + '464259a2' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), '469c0d9e' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index f2ec4b433f..c3438a6cee 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -89,12 +89,7 @@ final class ManiphestBatchEditController extends ManiphestController { ->setURI($job->getMonitorURI()); } - $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); - - $list = new ManiphestTaskListView(); - $list->setTasks($tasks); - $list->setUser($viewer); - $list->setHandles($handles); + $list = $this->newBulkObjectList($tasks); $template = new AphrontTokenizerTemplateView(); $template = $template->render(); @@ -142,21 +137,8 @@ final class ManiphestBatchEditController extends ManiphestController { 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), )); - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->addHiddenInput('board', $board_id) - ->setID('maniphest-batch-edit-form'); - - foreach ($tasks as $task) { - $form->appendChild( - phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => 'batch[]', - 'value' => $task->getID(), - ))); - } + $form = id(new PHUIFormLayoutView()) + ->setUser($viewer); $form->appendChild( phutil_tag( @@ -166,6 +148,7 @@ final class ManiphestBatchEditController extends ManiphestController { 'name' => 'actions', 'id' => 'batch-form-actions', ))); + $form->appendChild( id(new PHUIFormInsetView()) ->setTitle(pht('Actions')) @@ -210,17 +193,63 @@ final class ManiphestBatchEditController extends ManiphestController { ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( + + $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/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 070d436cf6..4753561124 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -29,6 +29,10 @@ final class PHUIObjectItemView extends AphrontTagView { private $coverImage; private $description; + private $selectableName; + private $selectableValue; + private $isSelected; + public function setDisabled($disabled) { $this->disabled = $disabled; return $this; @@ -160,6 +164,13 @@ final class PHUIObjectItemView extends AphrontTagView { return $this; } + public function setSelectable($name, $value, $is_selected) { + $this->selectableName = $name; + $this->selectableValue = $value; + $this->isSelected = $is_selected; + return $this; + } + public function setEpoch($epoch) { $date = phabricator_datetime($epoch, $this->getUser()); $this->addIcon('none', $date); @@ -239,6 +250,8 @@ final class PHUIObjectItemView extends AphrontTagView { } protected function getTagAttributes() { + $sigils = array(); + $item_classes = array(); $item_classes[] = 'phui-oi'; @@ -286,6 +299,17 @@ final class PHUIObjectItemView extends AphrontTagView { throw new Exception(pht('Invalid effect!')); } + if ($this->isSelected) { + $item_classes[] = 'phui-oi-selected'; + } + + if ($this->selectableName !== null) { + $item_classes[] = 'phui-oi-selectable'; + $sigils[] = 'phui-oi-selectable'; + + Javelin::initBehavior('phui-selectable-list'); + } + if ($this->getGrippable()) { $item_classes[] = 'phui-oi-grippable'; } @@ -300,6 +324,7 @@ final class PHUIObjectItemView extends AphrontTagView { return array( 'class' => $item_classes, + 'sigil' => $sigils, ); } @@ -628,6 +653,24 @@ final class PHUIObjectItemView extends AphrontTagView { $countdown); } + if ($this->selectableName !== null) { + $checkbox = phutil_tag( + 'input', + array( + 'type' => 'checkbox', + 'name' => $this->selectableName, + 'value' => $this->selectableValue, + 'checked' => ($this->isSelected ? 'checked' : null), + )); + + $column0 = phutil_tag( + 'div', + array( + 'class' => 'phui-oi-col0 phui-oi-checkbox', + ), + $checkbox); + } + $column1 = phutil_tag( 'div', array( 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 index e356396451..4ee5d41b67 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css @@ -664,3 +664,22 @@ ul.phui-oi-list-view .phui-oi-selected padding: 0 8px 8px; text-align: left; } + +.phui-oi-col0.phui-oi-checkbox { + width: 28px; + text-align: center; +} + +.phui-oi-selectable { + cursor: pointer; + user-select: none; + -webkit-user-select: none; +} + +/* When the list selection state can be toggled on the client (as in the bulk + editor), keep the border color consistent to make the interaction feel more + robust. */ +ul.phui-oi-list-view .phui-oi-selectable + .phui-oi-frame { + border-color: {$blueborder}; +} diff --git a/webroot/rsrc/css/phui/phui-box.css b/webroot/rsrc/css/phui/phui-box.css index 278f1365e8..c5fd1d8d3b 100644 --- a/webroot/rsrc/css/phui/phui-box.css +++ b/webroot/rsrc/css/phui/phui-box.css @@ -103,6 +103,10 @@ body.device .phui-box-blue-property.phui-object-box.phui-object-box-collapsed padding: 2px 8px; } +.phui-box-blue-property .phui-oi-list-view.phui-oi-list-flush { + padding: 0; +} + body .phui-box-blue-property.phui-object-box.phui-object-box-collapsed { padding: 0; } diff --git a/webroot/rsrc/js/phui/behavior-phui-selectable-list.js b/webroot/rsrc/js/phui/behavior-phui-selectable-list.js new file mode 100644 index 0000000000..eaa33565d0 --- /dev/null +++ b/webroot/rsrc/js/phui/behavior-phui-selectable-list.js @@ -0,0 +1,44 @@ +/** + * @provides javelin-behavior-phui-selectable-list + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + */ + +JX.behavior('phui-selectable-list', function() { + + JX.Stratcom.listen('click', 'phui-oi-selectable', function(e) { + if (!e.isNormalClick()) { + return; + } + + // If the user clicked a link, ignore it. + if (e.getNode('tag:a')) { + return; + } + + var root = e.getNode('phui-oi-selectable'); + + // If the user did not click the checkbox, pretend they did. This makes + // the entire element a click target to make changing the selection set a + // bit easier. + if (!e.getNode('tag:input')) { + var checkbox = getCheckbox(root); + checkbox.checked = !checkbox.checked; + + e.kill(); + } + + setTimeout(JX.bind(null, redraw, root), 0); + }); + + function getCheckbox(root) { + return JX.DOM.find(root, 'input'); + } + + function redraw(root) { + var checkbox = getCheckbox(root); + JX.DOM.alterClass(root, 'phui-oi-selected', !!checkbox.checked); + } + +}); From 7f91c8c4acbb65ce7bd8a9fad217fd8a9db30112 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Nov 2017 05:57:39 -0800 Subject: [PATCH 02/80] 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 --- resources/celerity/map.php | 24 +- src/__phutil_library_map__.php | 8 +- .../base/PhabricatorApplication.php | 4 + .../PhabricatorManiphestApplication.php | 2 +- .../bulk/ManiphestTaskBulkEngine.php | 50 ++ .../ManiphestBatchEditController.php | 255 ---------- .../ManiphestBulkEditController.php | 32 ++ .../view/ManiphestTaskResultListView.php | 2 +- .../PhabricatorProjectBoardViewController.php | 23 +- .../bulk/PhabricatorBulkEngine.php | 454 ++++++++++++++++++ src/view/phui/PHUIObjectItemView.php | 36 +- .../phui/object-item/phui-oi-list-view.css | 4 + .../maniphest/behavior-batch-selector.js | 11 +- 13 files changed, 612 insertions(+), 293 deletions(-) create mode 100644 src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php delete mode 100644 src/applications/maniphest/controller/ManiphestBatchEditController.php create mode 100644 src/applications/maniphest/controller/ManiphestBulkEditController.php create mode 100644 src/applications/transactions/bulk/PhabricatorBulkEngine.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0fdf44344f..650d3f7efb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 index f4a410ad2e..3ff9ce4fd3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1487,8 +1487,8 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( 'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod', 'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand', 'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', - 'ManiphestBatchEditController' => 'ManiphestController', 'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability', + 'ManiphestBulkEditController' => 'ManiphestController', 'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand', 'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand', 'ManiphestConduitAPIMethod' => 'ConduitAPIMethod', @@ -6761,6 +6763,7 @@ phutil_register_library_map(array( 'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule', + 'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine', 'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType', @@ -7487,6 +7490,7 @@ phutil_register_library_map(array( '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 index c25612408c..0ae8e75354 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -618,6 +618,10 @@ abstract class PhabricatorApplication ')?'; } + protected function getBulkRoutePattern($base = null) { + return $base.'(?:query/(?P[^/]+)/)?'; + } + protected function getQueryRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/)?'; } diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 3076354599..6e4ac0a8f6 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -52,7 +52,7 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { '/maniphest/' => array( '(?:query/(?P[^/]+)/)?' => 'ManiphestTaskListController', 'report/(?:(?P\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 index 0000000000..8b62f627d8 --- /dev/null +++ b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php @@ -0,0 +1,50 @@ +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 index c3438a6cee..0000000000 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ /dev/null @@ -1,255 +0,0 @@ -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 index 0000000000..b698e54848 --- /dev/null +++ b/src/applications/maniphest/controller/ManiphestBulkEditController.php @@ -0,0 +1,32 @@ +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 index 0144df0e33..2203db8bfc 100644 --- a/src/applications/maniphest/view/ManiphestTaskResultListView.php +++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php @@ -255,7 +255,7 @@ final class ManiphestTaskResultListView extends ManiphestView { $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 index 793fe5603d..9396d1873e 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -230,14 +230,23 @@ final class PhabricatorProjectBoardViewController ->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 @@ final class PhabricatorProjectBoardViewController $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 index 0000000000..82dc7f2510 --- /dev/null +++ b/src/applications/transactions/bulk/PhabricatorBulkEngine.php @@ -0,0 +1,454 @@ +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 index 4753561124..c1e57632c4 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -32,6 +32,7 @@ final class PHUIObjectItemView extends AphrontTagView { private $selectableName; private $selectableValue; private $isSelected; + private $isForbidden; public function setDisabled($disabled) { $this->disabled = $disabled; @@ -164,10 +165,17 @@ final class PHUIObjectItemView extends AphrontTagView { 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 @@ final class PHUIObjectItemView extends AphrontTagView { 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 @@ final class PHUIObjectItemView extends AphrontTagView { } 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 index 4ee5d41b67..ff79a8d70b 100644 --- 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 @@ ul.phui-oi-list-view .phui-oi-selected 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 index b7728abb57..b62f40a589 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js +++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js @@ -157,12 +157,15 @@ JX.behavior('maniphest-batch-selector', function(config) { '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(); From 6ef45d82459336daeeaf2e08fd2c6b3c1faade44 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jan 2018 11:08:16 -0800 Subject: [PATCH 03/80] Provide a generic transaction-oriented bulk job worker Summary: Depends on D18806. Ref T13025. See PHI173. Currently, Maniphest bulk edits are processed by a Maniphest-specific worker. I want to replace this with a generic worker which can apply transactional edits to any object. This implements a generic worker, although it has no callers yet. Future changes give it callers, and later remove the Maniphest-specific worker. Test Plan: See next changes. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13025 Differential Revision: https://secure.phabricator.com/D18862 --- src/__phutil_library_map__.php | 2 + .../bulk/PhabricatorEditEngineBulkJobType.php | 82 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3ff9ce4fd3..b39d89a087 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2729,6 +2729,7 @@ phutil_register_library_map(array( 'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php', 'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php', 'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php', + 'PhabricatorEditEngineBulkJobType' => 'applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php', 'PhabricatorEditEngineCheckboxesCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php', 'PhabricatorEditEngineColumnsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineColumnsCommentAction.php', 'PhabricatorEditEngineCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php', @@ -8110,6 +8111,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod', + 'PhabricatorEditEngineBulkJobType' => 'PhabricatorWorkerBulkJobType', 'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineCommentAction' => 'Phobject', diff --git a/src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php b/src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php new file mode 100644 index 0000000000..1a529f753d --- /dev/null +++ b/src/applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php @@ -0,0 +1,82 @@ +getSize())); + } + + public function getJobSize(PhabricatorWorkerBulkJob $job) { + return count($job->getParameter('objectPHIDs', array())); + } + + public function getDoneURI(PhabricatorWorkerBulkJob $job) { + return $job->getParameter('doneURI'); + } + + public function createTasks(PhabricatorWorkerBulkJob $job) { + $tasks = array(); + + foreach ($job->getParameter('objectPHIDs', array()) as $phid) { + $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid); + } + + return $tasks; + } + + public function runTask( + PhabricatorUser $actor, + PhabricatorWorkerBulkJob $job, + PhabricatorWorkerBulkTask $task) { + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($actor) + ->withPHIDs(array($task->getObjectPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return; + } + + $raw_xactions = $job->getParameter('xactions'); + $xactions = $this->buildTransactions($object, $raw_xactions); + + $editor = $object->getApplicationTransactionEditor() + ->setActor($actor) + ->setContentSource($job->newContentSource()) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($object, $xactions); + } + + private function buildTransactions($object, array $raw_xactions) { + $xactions = array(); + + foreach ($raw_xactions as $raw_xaction) { + $xaction = $object->getApplicationTransactionTemplate() + ->setTransactionType($raw_xaction['type']) + ->setNewValue($raw_xaction['value']); + + $xactions[] = $xaction; + } + + return $xactions; + } + +} From 09e71a408235008e39cb5e82876511452a5a0d13 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jan 2018 11:23:55 -0800 Subject: [PATCH 04/80] Define bulk edits in terms of EditEngine, not hard-coded ad-hoc definitions Summary: Depends on D18862. See PHI173. Ref T13025. Fixes T10005. This redefines bulk edits in terms of EditEngine fields, rather than hard-coding the whole thing. Only text fields -- and, specifically, only the "Title" field -- are supported after this change. Followup changes will add more bulk edit parameter types and broader field support. However, the title field now works without any Maniphest-specific code, outside of the small amount of binding code in the `ManiphestBulkEditor` subclass. Test Plan: Used the bulk edit workflow to change the titles of tasks. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13025, T10005 Differential Revision: https://secure.phabricator.com/D18863 --- resources/celerity/map.php | 37 ++-- src/__phutil_library_map__.php | 4 + .../bulk/ManiphestTaskBulkEngine.php | 4 + .../maniphest/editor/ManiphestEditEngine.php | 1 + .../bulk/PhabricatorBulkEngine.php | 166 ++++++++---------- .../bulk/type/BulkParameterType.php | 24 +++ .../bulk/type/BulkStringParameterType.php | 10 ++ .../editengine/PhabricatorEditEngine.php | 65 +++++++ .../editfield/PhabricatorEditField.php | 61 ++++++- .../PhabricatorPHIDListEditField.php | 4 + .../editfield/PhabricatorTextEditField.php | 4 + .../edittype/PhabricatorEditType.php | 40 +++++ .../application/maniphest/batch-editor.css | 17 -- webroot/rsrc/css/phui/phui-bulk-editor.css | 22 +++ .../maniphest/behavior-batch-editor.js | 158 ----------------- webroot/rsrc/js/core/behavior-bulk-editor.js | 113 ++++++++++++ webroot/rsrc/js/phuix/PHUIXFormControl.js | 13 ++ 17 files changed, 455 insertions(+), 288 deletions(-) create mode 100644 src/applications/transactions/bulk/type/BulkParameterType.php create mode 100644 src/applications/transactions/bulk/type/BulkStringParameterType.php delete mode 100644 webroot/rsrc/css/application/maniphest/batch-editor.css create mode 100644 webroot/rsrc/css/phui/phui-bulk-editor.css delete mode 100644 webroot/rsrc/js/application/maniphest/behavior-batch-editor.js create mode 100644 webroot/rsrc/js/core/behavior-bulk-editor.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 650d3f7efb..a8eee7a3cb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -81,7 +81,6 @@ return array( 'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4', 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 'rsrc/css/application/herald/herald.css' => 'cd8d0134', - 'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5', 'rsrc/css/application/maniphest/report.css' => '9b9580b7', 'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b', 'rsrc/css/application/maniphest/task-summary.css' => '11cc5344', @@ -143,6 +142,7 @@ return array( 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3', 'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c', 'rsrc/css/phui/phui-box.css' => '4bd6cdb9', + 'rsrc/css/phui/phui-bulk-editor.css' => '1fe728a8', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-cms.css' => '504b4b23', 'rsrc/css/phui/phui-comment-form.css' => 'ac68149f', @@ -419,7 +419,6 @@ return array( 'rsrc/js/application/herald/HeraldRuleEditor.js' => '2dff5579', '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' => 'ad54037e', 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', @@ -477,6 +476,7 @@ return array( 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c', + 'rsrc/js/core/behavior-bulk-editor.js' => '5e178556', 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 'rsrc/js/core/behavior-copy.js' => 'b0b8f86d', 'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96', @@ -532,7 +532,7 @@ return array( 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca', - 'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671', + 'rsrc/js/phuix/PHUIXFormControl.js' => '68bb05aa', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( @@ -595,6 +595,7 @@ return array( 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-badge-view' => '8ff5e24c', + 'javelin-behavior-bulk-editor' => '5e178556', 'javelin-behavior-bulk-job-reload' => 'edf8a145', 'javelin-behavior-calendar-month-view' => 'fe33e256', 'javelin-behavior-choose-control' => '327a00d1', @@ -642,7 +643,6 @@ return array( 'javelin-behavior-lightbox-attachments' => '560f41da', 'javelin-behavior-line-chart' => 'e4232876', 'javelin-behavior-load-blame' => '42126667', - 'javelin-behavior-maniphest-batch-editor' => '782ab6e7', 'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '71237763', @@ -756,7 +756,6 @@ return array( 'javelin-workboard-column' => '758b4758', 'javelin-workboard-controller' => '26167537', 'javelin-workflow' => '1e911d0f', - 'maniphest-batch-editor' => 'b0f0b6d5', 'maniphest-report-css' => '9b9580b7', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', @@ -823,6 +822,7 @@ return array( 'phui-basic-nav-view-css' => '98c11ab3', 'phui-big-info-view-css' => 'acc3492c', 'phui-box-css' => '4bd6cdb9', + 'phui-bulk-editor-css' => '1fe728a8', 'phui-button-bar-css' => 'f1ff5494', 'phui-button-css' => '1863cc6e', 'phui-button-simple-css' => '8e1baf68', @@ -884,7 +884,7 @@ return array( 'phuix-autocomplete' => 'e0731603', 'phuix-button-view' => '8a91e1ac', 'phuix-dropdown-menu' => '04b2ae03', - 'phuix-form-control-view' => '83e03671', + 'phuix-form-control-view' => '68bb05aa', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', @@ -1387,6 +1387,15 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + '5e178556' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'phabricator-prefab', + 'multirow-row-manager', + 'javelin-json', + 'phuix-form-control-view', + ), '5e2634b9' => array( 'javelin-behavior', 'javelin-aphlict', @@ -1436,6 +1445,10 @@ return array( 'javelin-dom', 'phuix-button-view', ), + '68bb05aa' => array( + 'javelin-install', + 'javelin-dom', + ), '69adf288' => array( 'javelin-install', ), @@ -1524,14 +1537,6 @@ return array( 'javelin-request', 'javelin-util', ), - '782ab6e7' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'phabricator-prefab', - 'multirow-row-manager', - 'javelin-json', - ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', @@ -1570,10 +1575,6 @@ return array( 'javelin-behavior', 'javelin-scrollbar', ), - '83e03671' => array( - 'javelin-install', - 'javelin-dom', - ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b39d89a087..e9bb60fe22 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -222,6 +222,8 @@ phutil_register_library_map(array( 'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php', 'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php', 'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php', + 'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php', + 'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php', 'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php', 'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php', 'CelerityAPI' => 'applications/celerity/CelerityAPI.php', @@ -5242,6 +5244,8 @@ phutil_register_library_map(array( 'AuditConduitAPIMethod' => 'ConduitAPIMethod', 'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod', 'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability', + 'BulkParameterType' => 'Phobject', + 'BulkStringParameterType' => 'BulkParameterType', 'CalendarTimeUtil' => 'Phobject', 'CalendarTimeUtilTestCase' => 'PhabricatorTestCase', 'CelerityAPI' => 'Phobject', diff --git a/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php index 8b62f627d8..44f575eefe 100644 --- a/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php +++ b/src/applications/maniphest/bulk/ManiphestTaskBulkEngine.php @@ -18,6 +18,10 @@ final class ManiphestTaskBulkEngine return new ManiphestTaskSearchEngine(); } + public function newEditEngine() { + return new ManiphestEditEngine(); + } + public function getDoneURI() { $board_uri = $this->getBoardURI(); if ($board_uri) { diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index b4ca86a442..f74fae076f 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -178,6 +178,7 @@ EODOCS id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) + ->setBulkEditLabel(pht('Set title to')) ->setDescription(pht('Name of the task.')) ->setConduitDescription(pht('Rename the task.')) ->setConduitTypeDescription(pht('New task name.')) diff --git a/src/applications/transactions/bulk/PhabricatorBulkEngine.php b/src/applications/transactions/bulk/PhabricatorBulkEngine.php index 82dc7f2510..f3cbeac9e1 100644 --- a/src/applications/transactions/bulk/PhabricatorBulkEngine.php +++ b/src/applications/transactions/bulk/PhabricatorBulkEngine.php @@ -10,7 +10,10 @@ abstract class PhabricatorBulkEngine extends Phobject { private $editableList; private $targetList; + private $rootFormID; + abstract public function newSearchEngine(); + abstract public function newEditEngine(); public function getCancelURI() { $saved_query = $this->savedQuery; @@ -118,7 +121,7 @@ abstract class PhabricatorBulkEngine extends Phobject { array( 'action' => $this->getBulkURI(), 'method' => 'POST', - 'id' => 'maniphest-batch-edit-form', + 'id' => $this->getRootFormID(), ), array( $this->newContextInputs(), @@ -290,95 +293,60 @@ abstract class PhabricatorBulkEngine extends Phobject { private function newBulkActionForm() { $viewer = $this->getViewer(); + $input_id = celerity_generate_unique_node_id(); + + $edit_engine = id($this->newEditEngine()) + ->setViewer($viewer); + + $edit_map = $edit_engine->newBulkEditMap(); + + require_celerity_resource('phui-bulk-editor-css'); + + Javelin::initBehavior( + 'bulk-editor', + array( + 'rootNodeID' => $this->getRootFormID(), + 'inputNodeID' => $input_id, + 'edits' => $edit_map, + )); $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', - ), - ''))) + return id(new PHUIFormLayoutView()) + ->setViewer($viewer) + ->appendChild( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'xactions', + 'id' => $input_id, + ))) + ->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' => 'bulk-actions', + 'class' => 'bulk-edit-table', + ), + ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Apply Bulk Edit')) ->addCancelButton($cancel_uri)); - - return $form; } private function buildEditResponse() { @@ -405,31 +373,33 @@ abstract class PhabricatorBulkEngine extends Phobject { 'You have not selected any objects to edit.')); } - $raw_actions = $request->getStr('actions'); - if ($raw_actions) { - $actions = phutil_json_decode($raw_actions); + $raw_xactions = $request->getStr('xactions'); + if ($raw_xactions) { + $raw_xactions = phutil_json_decode($raw_xactions); } else { - $actions = array(); + $raw_xactions = array(); } - if (!$actions) { + if (!$raw_xactions) { throw new Exception( pht( 'You have not chosen any edits to apply.')); } + $edit_engine = id($this->newEditEngine()) + ->setViewer($viewer); + + $xactions = $edit_engine->newRawBulkTransactions($raw_xactions); + $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(), + new PhabricatorEditEngineBulkJobType(), array( - 'taskPHIDs' => mpull($objects, 'getPHID'), - 'actions' => $actions, + 'objectPHIDs' => mpull($objects, 'getPHID'), + 'xactions' => $xactions, 'cancelURI' => $cancel_uri, 'doneURI' => $done_uri, )); @@ -451,4 +421,12 @@ abstract class PhabricatorBulkEngine extends Phobject { ->setURI($job->getMonitorURI()); } + private function getRootFormID() { + if (!$this->rootFormID) { + $this->rootFormID = celerity_generate_unique_node_id(); + } + + return $this->rootFormID; + } + } diff --git a/src/applications/transactions/bulk/type/BulkParameterType.php b/src/applications/transactions/bulk/type/BulkParameterType.php new file mode 100644 index 0000000000..e5d1ad9312 --- /dev/null +++ b/src/applications/transactions/bulk/type/BulkParameterType.php @@ -0,0 +1,24 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + abstract public function getPHUIXControlType(); + + public function getPHUIXControlSpecification() { + return array( + 'value' => null, + ); + } + +} diff --git a/src/applications/transactions/bulk/type/BulkStringParameterType.php b/src/applications/transactions/bulk/type/BulkStringParameterType.php new file mode 100644 index 0000000000..906ff9e2de --- /dev/null +++ b/src/applications/transactions/bulk/type/BulkStringParameterType.php @@ -0,0 +1,10 @@ +loadDefaultConfiguration(); + if (!$config) { + throw new Exception( + pht('No default edit engine configuration for bulk edit.')); + } + + $object = $this->newEditableObject(); + $fields = $this->buildEditFields($object); + + $edit_types = $this->getBulkEditTypesFromFields($fields); + + $map = array(); + foreach ($edit_types as $key => $type) { + $bulk_type = $type->getBulkParameterType(); + if ($bulk_type === null) { + continue; + } + + $bulk_label = $type->getBulkEditLabel(); + if ($bulk_label === null) { + continue; + } + + $map[] = array( + 'label' => $bulk_label, + 'xaction' => $type->getTransactionType(), + 'control' => array( + 'type' => $bulk_type->getPHUIXControlType(), + 'spec' => (object)$bulk_type->getPHUIXControlSpecification(), + ), + ); + } + + return $map; + } + + + final public function newRawBulkTransactions(array $xactions) { + return $xactions; + } + + private function getBulkEditTypesFromFields(array $fields) { + $types = array(); + + foreach ($fields as $field) { + $field_types = $field->getBulkEditTypes(); + + if ($field_types === null) { + continue; + } + + foreach ($field_types as $field_type) { + $field_type->setField($field); + $types[$field_type->getEditType()] = $field_type; + } + } + + return $types; + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php index 3cc6fe2fff..643bb2fca4 100644 --- a/src/applications/transactions/editfield/PhabricatorEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -17,6 +17,7 @@ abstract class PhabricatorEditField extends Phobject { private $previewPanel; private $controlID; private $controlInstructions; + private $bulkEditLabel; private $description; private $conduitDescription; @@ -45,6 +46,7 @@ abstract class PhabricatorEditField extends Phobject { private $isConduitOnly = false; private $conduitEditTypes; + private $bulkEditTypes; public function setKey($key) { $this->key = $key; @@ -64,6 +66,15 @@ abstract class PhabricatorEditField extends Phobject { return $this->label; } + public function setBulkEditLabel($bulk_edit_label) { + $this->bulkEditLabel = $bulk_edit_label; + return $this; + } + + public function getBulkEditLabel() { + return $this->bulkEditLabel; + } + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; @@ -625,6 +636,22 @@ abstract class PhabricatorEditField extends Phobject { return new AphrontStringHTTPParameterType(); } + protected function getBulkParameterType() { + $type = $this->newBulkParameterType(); + + if (!$type) { + return null; + } + + $type->setViewer($this->getViewer()); + + return $type; + } + + protected function newBulkParameterType() { + return null; + } + public function getConduitParameterType() { $type = $this->newConduitParameterType(); @@ -657,8 +684,15 @@ abstract class PhabricatorEditField extends Phobject { return null; } - return id(new PhabricatorSimpleEditType()) + $edit_type = id(new PhabricatorSimpleEditType()) ->setConduitParameterType($parameter_type); + + $bulk_type = $this->getBulkParameterType(); + if ($bulk_type) { + $edit_type->setBulkParameterType($bulk_type); + } + + return $edit_type; } protected function getEditType() { @@ -718,6 +752,31 @@ abstract class PhabricatorEditField extends Phobject { return array($edit_type); } + final public function getBulkEditTypes() { + if ($this->bulkEditTypes === null) { + $edit_types = $this->newBulkEditTypes(); + $edit_types = mpull($edit_types, null, 'getEditType'); + + foreach ($edit_types as $edit_type) { + $edit_type->setEditField($this); + } + + $this->bulkEditTypes = $edit_types; + } + + return $this->bulkEditTypes; + } + + protected function newBulkEditTypes() { + $edit_type = $this->getEditType(); + + if (!$edit_type) { + return array(); + } + + return array($edit_type); + } + public function getCommentAction() { $label = $this->getCommentActionLabel(); if ($label === null) { diff --git a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php index 7195741c43..e29206c8ba 100644 --- a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php +++ b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php @@ -104,6 +104,10 @@ abstract class PhabricatorPHIDListEditField return $type; } + protected function newBulkEditTypes() { + return $this->newConduitEditTypes(); + } + protected function newConduitEditTypes() { if (!$this->getUseEdgeTransactions()) { return parent::newConduitEditTypes(); diff --git a/src/applications/transactions/editfield/PhabricatorTextEditField.php b/src/applications/transactions/editfield/PhabricatorTextEditField.php index fa51ff6142..68854cf2e2 100644 --- a/src/applications/transactions/editfield/PhabricatorTextEditField.php +++ b/src/applications/transactions/editfield/PhabricatorTextEditField.php @@ -29,4 +29,8 @@ final class PhabricatorTextEditField return new ConduitStringParameterType(); } + protected function newBulkParameterType() { + return new BulkStringParameterType(); + } + } diff --git a/src/applications/transactions/edittype/PhabricatorEditType.php b/src/applications/transactions/edittype/PhabricatorEditType.php index 1451ec2c04..0cba9db867 100644 --- a/src/applications/transactions/edittype/PhabricatorEditType.php +++ b/src/applications/transactions/edittype/PhabricatorEditType.php @@ -14,6 +14,9 @@ abstract class PhabricatorEditType extends Phobject { private $conduitTypeDescription; private $conduitParameterType; + private $bulkParameterType; + private $bulkEditLabel; + public function setLabel($label) { $this->label = $label; return $this; @@ -23,6 +26,19 @@ abstract class PhabricatorEditType extends Phobject { return $this->label; } + public function setBulkEditLabel($bulk_edit_label) { + $this->bulkEditLabel = $bulk_edit_label; + return $this; + } + + public function getBulkEditLabel() { + if ($this->bulkEditLabel !== null) { + return $this->bulkEditLabel; + } + + return $this->getField()->getBulkEditLabel(); + } + public function setField(PhabricatorEditField $field) { $this->field = $field; return $this; @@ -85,6 +101,30 @@ abstract class PhabricatorEditType extends Phobject { return $this->editField; } + +/* -( Bulk )--------------------------------------------------------------- */ + + + protected function newBulkParameterType() { + if ($this->bulkParameterType) { + return clone $this->bulkParameterType; + } + + return null; + } + + + public function setBulkParameterType(BulkParameterType $type) { + $this->bulkParameterType = $type; + return $this; + } + + + public function getBulkParameterType() { + return $this->newBulkParameterType(); + } + + /* -( Conduit )------------------------------------------------------------ */ diff --git a/webroot/rsrc/css/application/maniphest/batch-editor.css b/webroot/rsrc/css/application/maniphest/batch-editor.css deleted file mode 100644 index ea98fd013c..0000000000 --- a/webroot/rsrc/css/application/maniphest/batch-editor.css +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @provides maniphest-batch-editor - */ -.maniphest-batch-actions-table { - width: 100%; - margin: 12px 0; -} - -.maniphest-batch-actions-table td { - padding: 4px 8px; - vertical-align: middle; -} - -.batch-editor-input { - width: 100%; - text-align: left; -} diff --git a/webroot/rsrc/css/phui/phui-bulk-editor.css b/webroot/rsrc/css/phui/phui-bulk-editor.css new file mode 100644 index 0000000000..d7364b1d75 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-bulk-editor.css @@ -0,0 +1,22 @@ +/** + * @provides phui-bulk-editor-css + */ + +.bulk-edit-table { + width: 100%; + margin: 12px 0; +} + +.bulk-edit-table td { + padding: 4px 8px; + vertical-align: middle; +} + +.bulk-edit-input { + width: 100%; + text-align: left; +} + +.bulk-edit-input input { + width: 100%; +} diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js deleted file mode 100644 index ad2cf32d9e..0000000000 --- a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @provides javelin-behavior-maniphest-batch-editor - * @requires javelin-behavior - * javelin-dom - * javelin-util - * phabricator-prefab - * multirow-row-manager - * javelin-json - */ - -JX.behavior('maniphest-batch-editor', function(config) { - var root = JX.$(config.root); - var editor_table = JX.DOM.find(root, 'table', 'maniphest-batch-actions'); - var manager = new JX.MultirowRowManager(editor_table); - var action_rows = []; - - function renderRow() { - var action_select = JX.Prefab.renderSelect( - { - 'add_project': 'Add Projects', - 'remove_project' : 'Remove Projects', - 'priority': 'Change Priority', - 'status': 'Change Status', - 'add_comment': 'Comment', - 'assign': 'Assign', - 'add_ccs' : 'Add CCs', - 'remove_ccs' : 'Remove CCs', - 'space': 'Shift to Space' - }); - - var proj_tokenizer = build_tokenizer(config.sources.project); - var owner_tokenizer = build_tokenizer(config.sources.owner); - var cc_tokenizer = build_tokenizer(config.sources.cc); - var space_tokenizer = build_tokenizer(config.sources.spaces); - - var priority_select = JX.Prefab.renderSelect(config.priorityMap); - var status_select = JX.Prefab.renderSelect(config.statusMap); - var comment_input = JX.$N('input', {style: {width: '100%'}}); - - var cell = JX.$N('td', {className: 'batch-editor-input'}); - var vfunc = null; - - function update() { - switch (action_select.value) { - case 'add_project': - case 'remove_project': - JX.DOM.setContent(cell, proj_tokenizer.template); - vfunc = function() { - return JX.keys(proj_tokenizer.object.getTokens()); - }; - break; - case 'add_ccs': - case 'remove_ccs': - JX.DOM.setContent(cell, cc_tokenizer.template); - vfunc = function() { - return JX.keys(cc_tokenizer.object.getTokens()); - }; - break; - case 'assign': - JX.DOM.setContent(cell, owner_tokenizer.template); - vfunc = function() { - return JX.keys(owner_tokenizer.object.getTokens()); - }; - break; - case 'space': - JX.DOM.setContent(cell, space_tokenizer.template); - vfunc = function() { - return JX.keys(space_tokenizer.object.getTokens()); - }; - break; - case 'add_comment': - JX.DOM.setContent(cell, comment_input); - vfunc = function() { - return comment_input.value; - }; - break; - case 'priority': - JX.DOM.setContent(cell, priority_select); - vfunc = function() { return priority_select.value; }; - break; - case 'status': - JX.DOM.setContent(cell, status_select); - vfunc = function() { return status_select.value; }; - break; - } - } - - JX.DOM.listen(action_select, 'change', null, update); - update(); - - return { - nodes : [JX.$N('td', {}, action_select), cell], - dataCallback : function() { - return { - action: action_select.value, - value: vfunc() - }; - } - }; - } - - function onaddaction(e) { - e.kill(); - addRow({}); - } - - function addRow(info) { - var data = renderRow(info); - var row = manager.addRow(data.nodes); - var id = manager.getRowID(row); - - action_rows[id] = data.dataCallback; - } - - function onsubmit() { - var input = JX.$(config.input); - - var actions = []; - for (var k in action_rows) { - actions.push(action_rows[k]()); - } - - input.value = JX.JSON.stringify(actions); - } - - addRow({}); - - JX.DOM.listen( - root, - 'click', - 'add-action', - onaddaction); - - JX.DOM.listen( - root, - 'submit', - null, - onsubmit); - - manager.listen( - 'row-removed', - function(row_id) { - delete action_rows[row_id]; - }); - - function build_tokenizer(tconfig) { - var built = JX.Prefab.newTokenizerFromTemplate( - config.tokenizerTemplate, - JX.copy({}, tconfig)); - built.tokenizer.start(); - - return { - object: built.tokenizer, - template: built.node - }; - } - -}); diff --git a/webroot/rsrc/js/core/behavior-bulk-editor.js b/webroot/rsrc/js/core/behavior-bulk-editor.js new file mode 100644 index 0000000000..1e6e60e94e --- /dev/null +++ b/webroot/rsrc/js/core/behavior-bulk-editor.js @@ -0,0 +1,113 @@ +/** + * @provides javelin-behavior-bulk-editor + * @requires javelin-behavior + * javelin-dom + * javelin-util + * phabricator-prefab + * multirow-row-manager + * javelin-json + * phuix-form-control-view + */ + +JX.behavior('bulk-editor', function(config) { + + var root = JX.$(config.rootNodeID); + var editor_table = JX.DOM.find(root, 'table', 'bulk-actions'); + + var manager = new JX.MultirowRowManager(editor_table); + var action_rows = []; + + var option_map = {}; + var option_order = []; + var spec_map = {}; + + for (var ii = 0; ii < config.edits.length; ii++) { + var edit = config.edits[ii]; + + option_map[edit.xaction] = edit.label; + option_order.push(edit.xaction); + + spec_map[edit.xaction] = edit; + } + + function renderRow() { + var action_select = JX.Prefab.renderSelect( + option_map, + null, + null, + option_order); + + var cell = JX.$N('td', {className: 'bulk-edit-input'}); + var vfunc = null; + + function update() { + var spec = spec_map[action_select.value]; + var control = spec.control; + + var phuix = new JX.PHUIXFormControl() + .setControl(control.type, control.spec); + + JX.DOM.setContent(cell, phuix.getRawInputNode()); + + vfunc = JX.bind(phuix, phuix.getValue); + } + + JX.DOM.listen(action_select, 'change', null, update); + update(); + + return { + nodes : [JX.$N('td', {}, action_select), cell], + dataCallback : function() { + return { + type: action_select.value, + value: vfunc() + }; + } + }; + } + + function onaddaction(e) { + e.kill(); + addRow({}); + } + + function addRow(info) { + var data = renderRow(info); + var row = manager.addRow(data.nodes); + var id = manager.getRowID(row); + + action_rows[id] = data.dataCallback; + } + + function onsubmit() { + var input = JX.$(config.inputNodeID); + + var actions = []; + for (var k in action_rows) { + actions.push(action_rows[k]()); + } + + input.value = JX.JSON.stringify(actions); + } + + addRow({}); + + JX.DOM.listen( + root, + 'click', + 'add-action', + onaddaction); + + JX.DOM.listen( + root, + 'submit', + null, + onsubmit); + + manager.listen( + 'row-removed', + function(row_id) { + delete action_rows[row_id]; + }); + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXFormControl.js b/webroot/rsrc/js/phuix/PHUIXFormControl.js index 5640b95ae8..0a9b8d45d7 100644 --- a/webroot/rsrc/js/phuix/PHUIXFormControl.js +++ b/webroot/rsrc/js/phuix/PHUIXFormControl.js @@ -14,6 +14,7 @@ JX.install('PHUIXFormControl', { _className: null, _valueSetCallback: null, _valueGetCallback: null, + _rawInputNode: null, setLabel: function(label) { JX.DOM.setContent(this._getLabelNode(), label); @@ -53,6 +54,9 @@ JX.install('PHUIXFormControl', { case 'checkboxes': input = this._newCheckboxes(spec); break; + case 'text': + input = this._newText(spec); + break; default: // TODO: Default or better error? JX.$E('Bad Input Type'); @@ -62,6 +66,7 @@ JX.install('PHUIXFormControl', { JX.DOM.setContent(node, input.node); this._valueGetCallback = input.get; this._valueSetCallback = input.set; + this._rawInputNode = input.node; return this; }, @@ -75,6 +80,10 @@ JX.install('PHUIXFormControl', { return this._valueGetCallback(); }, + getRawInputNode: function() { + return this._rawInputNode; + }, + getNode: function() { if (!this._node) { @@ -281,6 +290,10 @@ JX.install('PHUIXFormControl', { }, _newPoints: function(spec) { + return this._newText(); + }, + + _newText: function(spec) { var attrs = { type: 'text', value: spec.value From a251db461826470bdb694271db8bd7508fcec5d8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jan 2018 13:04:25 -0800 Subject: [PATCH 05/80] Remove the Maniphest-specific bulk job type Summary: Depends on D18863. Ref PHI173. Ref T13025. After D18863, this job type is no longer used: the workflow uses a genric worker instead which can apply transactions to any object. Test Plan: Grepped for callsites, found none. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13025 Differential Revision: https://secure.phabricator.com/D18864 --- src/__phutil_library_map__.php | 2 - .../bulk/ManiphestTaskEditBulkJobType.php | 303 ------------------ 2 files changed, 305 deletions(-) delete mode 100644 src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e9bb60fe22..d8cf93c9ab 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1559,7 +1559,6 @@ phutil_register_library_map(array( 'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 'ManiphestTaskEdgeTransaction' => 'applications/maniphest/xaction/ManiphestTaskEdgeTransaction.php', - 'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php', 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php', 'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php', @@ -6778,7 +6777,6 @@ phutil_register_library_map(array( 'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskDetailController' => 'ManiphestController', 'ManiphestTaskEdgeTransaction' => 'ManiphestTaskTransactionType', - 'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType', 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock', 'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine', diff --git a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php b/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php deleted file mode 100644 index 1b083d88f9..0000000000 --- a/src/applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php +++ /dev/null @@ -1,303 +0,0 @@ -getSize())); - } - - public function getJobSize(PhabricatorWorkerBulkJob $job) { - return count($job->getParameter('taskPHIDs', array())); - } - - public function getDoneURI(PhabricatorWorkerBulkJob $job) { - return $job->getParameter('doneURI'); - } - - public function createTasks(PhabricatorWorkerBulkJob $job) { - $tasks = array(); - - foreach ($job->getParameter('taskPHIDs', array()) as $phid) { - $tasks[] = PhabricatorWorkerBulkTask::initializeNewTask($job, $phid); - } - - return $tasks; - } - - public function runTask( - PhabricatorUser $actor, - PhabricatorWorkerBulkJob $job, - PhabricatorWorkerBulkTask $task) { - - $object = id(new ManiphestTaskQuery()) - ->setViewer($actor) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(array($task->getObjectPHID())) - ->needProjectPHIDs(true) - ->needSubscriberPHIDs(true) - ->executeOne(); - if (!$object) { - return; - } - - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_EDIT); - $field_list->readFieldsFromStorage($object); - - $actions = $job->getParameter('actions'); - $xactions = $this->buildTransactions($actions, $object); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($actor) - ->setContentSource($job->newContentSource()) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true) - ->applyTransactions($object, $xactions); - } - - private function buildTransactions($actions, ManiphestTask $task) { - $value_map = array(); - $type_map = array( - 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, - 'assign' => ManiphestTaskOwnerTransaction::TRANSACTIONTYPE, - 'status' => ManiphestTaskStatusTransaction::TRANSACTIONTYPE, - 'priority' => ManiphestTaskPriorityTransaction::TRANSACTIONTYPE, - 'add_project' => PhabricatorTransactions::TYPE_EDGE, - 'remove_project' => PhabricatorTransactions::TYPE_EDGE, - 'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, - 'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS, - 'space' => PhabricatorTransactions::TYPE_SPACE, - ); - - $edge_edit_types = array( - 'add_project' => true, - 'remove_project' => true, - 'add_ccs' => true, - 'remove_ccs' => true, - ); - - $xactions = array(); - foreach ($actions as $action) { - if (empty($type_map[$action['action']])) { - throw new Exception(pht("Unknown batch edit action '%s'!", $action)); - } - - $type = $type_map[$action['action']]; - - // Figure out the current value, possibly after modifications by other - // batch actions of the same type. For example, if the user chooses to - // "Add Comment" twice, we should add both comments. More notably, if the - // user chooses "Remove Project..." and also "Add Project...", we should - // avoid restoring the removed project in the second transaction. - - if (array_key_exists($type, $value_map)) { - $current = $value_map[$type]; - } else { - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - $current = null; - break; - case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: - $current = $task->getOwnerPHID(); - break; - case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: - $current = $task->getStatus(); - break; - case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE: - $current = $task->getPriority(); - break; - case PhabricatorTransactions::TYPE_EDGE: - $current = $task->getProjectPHIDs(); - break; - case PhabricatorTransactions::TYPE_SUBSCRIBERS: - $current = $task->getSubscriberPHIDs(); - break; - case PhabricatorTransactions::TYPE_SPACE: - $current = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( - $task); - break; - } - } - - // Check if the value is meaningful / provided, and normalize it if - // necessary. This discards, e.g., empty comments and empty owner - // changes. - - $value = $action['value']; - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - if (!strlen($value)) { - continue 2; - } - break; - case PhabricatorTransactions::TYPE_SPACE: - if (empty($value)) { - continue 2; - } - $value = head($value); - break; - case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: - if (empty($value)) { - continue 2; - } - $value = head($value); - $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; - if ($value === $no_owner) { - $value = null; - } - break; - case PhabricatorTransactions::TYPE_EDGE: - if (empty($value)) { - continue 2; - } - break; - case PhabricatorTransactions::TYPE_SUBSCRIBERS: - if (empty($value)) { - continue 2; - } - break; - } - - // If the edit doesn't change anything, go to the next action. This - // check is only valid for changes like "owner", "status", etc, not - // for edge edits, because we should still apply an edit like - // "Remove Projects: A, B" to a task with projects "A, B". - - if (empty($edge_edit_types[$action['action']])) { - if ($value == $current) { - continue; - } - } - - // Apply the value change; for most edits this is just replacement, but - // some need to merge the current and edited values (add/remove project). - - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - if (strlen($current)) { - $value = $current."\n\n".$value; - } - break; - case PhabricatorTransactions::TYPE_EDGE: - $is_remove = $action['action'] == 'remove_project'; - - $current = array_fill_keys($current, true); - $value = array_fill_keys($value, true); - - $new = $current; - $did_something = false; - - if ($is_remove) { - foreach ($value as $phid => $ignored) { - if (isset($new[$phid])) { - unset($new[$phid]); - $did_something = true; - } - } - } else { - foreach ($value as $phid => $ignored) { - if (empty($new[$phid])) { - $new[$phid] = true; - $did_something = true; - } - } - } - - if (!$did_something) { - continue 2; - } - - $value = array_keys($new); - break; - case PhabricatorTransactions::TYPE_SUBSCRIBERS: - $is_remove = $action['action'] == 'remove_ccs'; - - $current = array_fill_keys($current, true); - - $new = array(); - $did_something = false; - - if ($is_remove) { - foreach ($value as $phid) { - if (isset($current[$phid])) { - $new[$phid] = true; - $did_something = true; - } - } - if ($new) { - $value = array('-' => array_keys($new)); - } - } else { - $new = array(); - foreach ($value as $phid) { - $new[$phid] = true; - $did_something = true; - } - if ($new) { - $value = array('+' => array_keys($new)); - } - } - if (!$did_something) { - continue 2; - } - - break; - } - - $value_map[$type] = $value; - } - - $template = new ManiphestTransaction(); - - foreach ($value_map as $type => $value) { - $xaction = clone $template; - $xaction->setTransactionType($type); - - switch ($type) { - case PhabricatorTransactions::TYPE_COMMENT: - $xaction->attachComment( - id(new ManiphestTransactionComment()) - ->setContent($value)); - break; - case PhabricatorTransactions::TYPE_EDGE: - $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; - $xaction - ->setMetadataValue('edge:type', $project_type) - ->setNewValue( - array( - '=' => array_fuse($value), - )); - break; - case ManiphestTaskPriorityTransaction::TRANSACTIONTYPE: - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $value)); - $xaction->setNewValue($keyword); - break; - default: - $xaction->setNewValue($value); - break; - } - - $xactions[] = $xaction; - } - - return $xactions; - } -} From bf1ac701c37c93791143ef31bacb1196f05fca44 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jan 2018 09:27:58 -0800 Subject: [PATCH 06/80] Support "select" types in bulk editor (status, priority) Summary: Depends on D18864. Ref T13025. Adds bulk edit support back for "status" and "priority" using `