From ad659627b3e931256e295efda941775c4020b713 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Nov 2017 05:14:05 -0800 Subject: [PATCH] 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); + } + +});