diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'fdb27ef9', + 'core.pkg.css' => '5be8063f', 'core.pkg.js' => '4c79d74f', 'darkconsole.pkg.js' => '1f9a31bc', 'differential.pkg.css' => '45951e9e', @@ -135,14 +135,14 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -89,12 +89,7 @@ ->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 @@ '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 @@ 'name' => 'actions', 'id' => 'batch-form-actions', ))); + $form->appendChild( id(new PHUIFormInsetView()) ->setTitle(pht('Actions')) @@ -210,17 +193,63 @@ ->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 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -29,6 +29,10 @@ private $coverImage; private $description; + private $selectableName; + private $selectableValue; + private $isSelected; + public function setDisabled($disabled) { $this->disabled = $disabled; return $this; @@ -160,6 +164,13 @@ 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 @@ } protected function getTagAttributes() { + $sigils = array(); + $item_classes = array(); $item_classes[] = 'phui-oi'; @@ -286,6 +299,17 @@ 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 @@ return array( 'class' => $item_classes, + 'sigil' => $sigils, ); } @@ -628,6 +653,24 @@ $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 --- 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 @@ 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 --- a/webroot/rsrc/css/phui/phui-box.css +++ b/webroot/rsrc/css/phui/phui-box.css @@ -103,6 +103,10 @@ 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 --- /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); + } + +});