diff --git a/.gitignore b/.gitignore index 8e3a6a3135..08287579ff 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ # NetBeans project files /nbproject/ + +# Arcanist scratch directory +/.arc diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index b66e75c4d1..76f3a065b2 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -630,6 +630,36 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/herald/herald-rule-editor.js', ), + 'javelin-behavior-maniphest-batch-editor' => + array( + 'uri' => '/res/d7b7f061/rsrc/js/application/maniphest/behavior-batch-editor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-prefab', + 4 => 'multirow-row-manager', + 5 => 'javelin-tokenizer', + 6 => 'javelin-typeahead-preloaded-source', + 7 => 'javelin-typeahead', + 8 => 'javelin-json', + ), + 'disk' => '/rsrc/js/application/maniphest/behavior-batch-editor.js', + ), + 'javelin-behavior-maniphest-batch-selector' => + array( + 'uri' => '/res/398cf8d7/rsrc/js/application/maniphest/behavior-batch-selector.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + ), + 'disk' => '/rsrc/js/application/maniphest/behavior-batch-selector.js', + ), 'javelin-behavior-maniphest-description-preview' => array( 'uri' => '/res/8acd6f07/rsrc/js/application/maniphest/behavior-task-preview.js', @@ -1266,6 +1296,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/maniphest/task-detail.css', ), + 'maniphest-batch-editor' => + array( + 'uri' => '/res/fb15d744/rsrc/css/application/maniphest/batch-editor.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/maniphest/batch-editor.css', + ), 'maniphest-task-edit-css' => array( 'uri' => '/res/68c7863e/rsrc/css/application/maniphest/task-edit.css', @@ -1277,7 +1316,7 @@ celerity_register_resource_map(array( ), 'maniphest-task-summary-css' => array( - 'uri' => '/res/44e5169a/rsrc/css/application/maniphest/task-summary.css', + 'uri' => '/res/7c52d502/rsrc/css/application/maniphest/task-summary.css', 'type' => 'css', 'requires' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c5e5506332..a902e76aab 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -403,6 +403,7 @@ phutil_register_library_map(array( 'ManiphestAuxiliaryFieldSpecification' => 'applications/maniphest/auxiliaryfield/base', 'ManiphestAuxiliaryFieldTypeException' => 'applications/maniphest/auxiliaryfield/typeexception', 'ManiphestAuxiliaryFieldValidationException' => 'applications/maniphest/auxiliaryfield/validationexception', + 'ManiphestBatchEditController' => 'applications/maniphest/controller/batch', 'ManiphestConstants' => 'applications/maniphest/constants/base', 'ManiphestController' => 'applications/maniphest/controller/base', 'ManiphestDAO' => 'applications/maniphest/storage/base', @@ -1181,6 +1182,7 @@ phutil_register_library_map(array( 'LiskIsolationTestDAO' => 'LiskDAO', 'ManiphestAction' => 'PhrictionConstants', 'ManiphestAuxiliaryFieldDefaultSpecification' => 'ManiphestAuxiliaryFieldSpecification', + 'ManiphestBatchEditController' => 'ManiphestController', 'ManiphestController' => 'PhabricatorController', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestDefaultTaskExtensions' => 'ManiphestTaskExtensions', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 344ad9e550..13507e885c 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -177,6 +177,7 @@ class AphrontDefaultApplicationConfiguration '$' => 'ManiphestTaskListController', 'view/(?P\w+)/$' => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?$' => 'ManiphestReportController', + 'batch/$' => 'ManiphestBatchEditController', 'task/' => array( 'create/$' => 'ManiphestTaskEditController', 'edit/(?P\d+)/$' => 'ManiphestTaskEditController', diff --git a/src/applications/maniphest/controller/batch/ManiphestBatchEditController.php b/src/applications/maniphest/controller/batch/ManiphestBatchEditController.php new file mode 100644 index 0000000000..57aa55005f --- /dev/null +++ b/src/applications/maniphest/controller/batch/ManiphestBatchEditController.php @@ -0,0 +1,197 @@ +getRequest(); + $user = $request->getUser(); + + $task_ids = $request->getArr('batch'); + $tasks = id(new ManiphestTask())->loadAllWhere( + 'id IN (%Ld)', + $task_ids); + + $actions = $request->getStr('actions'); + if ($actions) { + $actions = json_decode($actions, true); + } + + if ($request->isFormPost() && is_array($actions)) { + foreach ($tasks as $task) { + $xactions = $this->buildTransactions($actions, $task); + if ($xactions) { + $editor = new ManiphestTransactionEditor(); + $editor->applyTransactions($task, $xactions); + } + } + + return id(new AphrontRedirectResponse()) + ->setURI('/maniphest/'); + } + + $panel = new AphrontPanelView(); + $panel->setHeader('Maniphest Batch Editor'); + + $handle_phids = mpull($tasks, 'getOwnerPHID'); + $handles = id(new PhabricatorObjectHandleData($handle_phids)) + ->loadHandles(); + + $list = new ManiphestTaskListView(); + $list->setTasks($tasks); + $list->setUser($user); + $list->setHandles($handles); + + $template = new AphrontTokenizerTemplateView(); + $template = $template->render(); + + require_celerity_resource('maniphest-batch-editor'); + Javelin::initBehavior( + 'maniphest-batch-editor', + array( + 'root' => 'maniphest-batch-edit-form', + 'tokenizerTemplate' => $template, + 'sources' => array( + 'project' => '/typeahead/common/projects/', + ), + 'input' => 'batch-form-actions', + )); + + $form = new AphrontFormView(); + $form->setUser($user); + $form->setID('maniphest-batch-edit-form'); + + foreach ($tasks as $task) { + $form->appendChild( + phutil_render_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'batch[]', + 'value' => $task->getID(), + ), + null)); + } + + $form->appendChild( + phutil_render_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'actions', + 'id' => 'batch-form-actions', + ), + null)); + $form->appendChild('

These tasks will be edited:

'); + $form->appendChild($list); + $form->appendChild( + '

Actions

'. + '
'. + '
'. + javelin_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'add-action', + 'mustcapture' => true, + ), + 'Add Another Action'). + '
'. + '
'. + javelin_render_tag( + 'table', + array( + 'sigil' => 'maniphest-batch-actions', + 'class' => 'maniphest-batch-actions-table', + ), + ''). + '
') + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Update Tasks') + ->addCancelButton('/maniphest/', 'Done')); + + $panel->appendChild($form); + + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'Batch Editor', + )); + } + + private function buildTransactions($actions, ManiphestTask $task) { + $template = new ManiphestTransaction(); + $template->setAuthorPHID($this->getRequest()->getUser()->getPHID()); + + // TODO: Set content source to "batch edit". + + $xactions = array(); + foreach ($actions as $action) { + $value = $action['value']; + switch ($action['action']) { + case 'add_project': + case 'remove_project': + + $is_remove = ($action['action'] == 'remove_project'); + + $current = array_fill_keys($task->getProjectPHIDs(), 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) { + break; + } + + $new = array_keys($new); + $xaction = clone $template; + $xaction->setTransactionType(ManiphestTransactionType::TYPE_PROJECTS); + $xaction->setNewValue($new); + $xactions[] = $xaction; + break; + } + } + + return $xactions; + } + +} diff --git a/src/applications/maniphest/controller/batch/__init__.php b/src/applications/maniphest/controller/batch/__init__.php new file mode 100644 index 0000000000..c6a14bffa0 --- /dev/null +++ b/src/applications/maniphest/controller/batch/__init__.php @@ -0,0 +1,29 @@ +'); + $selector = new AphrontNullView(); + foreach ($tasks as $group => $list) { $task_list = new ManiphestTaskListView(); + $task_list->setShowBatchControls(true); $task_list->setUser($user); $task_list->setTasks($list); $task_list->setHandles($handles); $count = number_format(count($list)); - $nav->appendChild( + $selector->appendChild( '

'. phutil_escape_html($group).' ('.$count.')'. '

'); - $nav->appendChild($task_list); + $selector->appendChild($task_list); } + + $selector->appendChild($this->renderBatchEditor()); + + $selector = phabricator_render_form( + $user, + array( + 'method' => 'POST', + 'action' => '/maniphest/batch/', + ), + $selector->render()); + + $nav->appendChild($selector); $nav->appendChild($pager); } @@ -479,4 +494,61 @@ class ManiphestTaskListController extends ManiphestController { return implode("\n", $links); } + private function renderBatchEditor() { + Javelin::initBehavior( + 'maniphest-batch-selector', + array( + 'selectAll' => 'batch-select-all', + 'selectNone' => 'batch-select-none', + 'submit' => 'batch-select-submit', + 'status' => 'batch-select-status-cell', + )); + + $select_all = javelin_render_tag( + 'a', + array( + 'href' => '#', + 'mustcapture' => true, + 'class' => 'grey button', + 'id' => 'batch-select-all', + ), + 'Select All'); + + $select_none = javelin_render_tag( + 'a', + array( + 'href' => '#', + 'mustcapture' => true, + 'class' => 'grey button', + 'id' => 'batch-select-none', + ), + 'Clear Selection'); + + $submit = phutil_render_tag( + 'button', + array( + 'id' => 'batch-select-submit', + 'disabled' => 'disabled', + 'class' => 'disabled', + ), + 'Batch Edit Selected Tasks »'); + + return + '
'. + '
Batch Task Editor
'. + ''. + ''. + ''. + ''. + ''. + ''. + '
'. + $select_all. + $select_none. + ''. + '0 Selected Tasks'. + ''.$submit.'
'. + ''; + } + } diff --git a/src/applications/maniphest/controller/tasklist/__init__.php b/src/applications/maniphest/controller/tasklist/__init__.php index ac7a65b8da..c7fcc3eec3 100644 --- a/src/applications/maniphest/controller/tasklist/__init__.php +++ b/src/applications/maniphest/controller/tasklist/__init__.php @@ -14,6 +14,8 @@ phutil_require_module('phabricator', 'applications/maniphest/query'); phutil_require_module('phabricator', 'applications/maniphest/view/tasklist'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/markup'); phutil_require_module('phabricator', 'view/control/pager'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/submit'); @@ -22,6 +24,7 @@ phutil_require_module('phabricator', 'view/form/control/togglebuttons'); phutil_require_module('phabricator', 'view/form/control/tokenizer'); phutil_require_module('phabricator', 'view/layout/listfilter'); phutil_require_module('phabricator', 'view/layout/sidenavfilter'); +phutil_require_module('phabricator', 'view/null'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'parser/uri'); diff --git a/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php b/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php index 5a51f7b7f1..fbd510a52f 100644 --- a/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php @@ -1,7 +1,7 @@ tasks = $tasks; @@ -40,12 +41,18 @@ class ManiphestTaskListView extends ManiphestView { return $this; } + public function setShowBatchControls($show_batch_controls) { + $this->showBatchControls = $show_batch_controls; + return $this; + } + public function render() { $views = array(); foreach ($this->tasks as $task) { $view = new ManiphestTaskSummaryView(); $view->setTask($task); + $view->setShowBatchControls($this->showBatchControls); $view->setUser($this->user); $view->setHandles($this->handles); $views[] = $view->render(); diff --git a/src/applications/maniphest/view/tasksummary/ManiphestTaskSummaryView.php b/src/applications/maniphest/view/tasksummary/ManiphestTaskSummaryView.php index ca102df1b5..f5e47c56ad 100644 --- a/src/applications/maniphest/view/tasksummary/ManiphestTaskSummaryView.php +++ b/src/applications/maniphest/view/tasksummary/ManiphestTaskSummaryView.php @@ -1,7 +1,7 @@ task = $task; @@ -40,6 +41,11 @@ class ManiphestTaskSummaryView extends ManiphestView { return $this; } + public function setShowBatchControls($show_batch_controls) { + $this->showBatchControls = $show_batch_controls; + return $this; + } + public function render() { if (!$this->user) { @@ -63,36 +69,58 @@ class ManiphestTaskSummaryView extends ManiphestView { $pri_class = idx($classes, $task->getPriority()); $status_map = ManiphestTaskStatus::getTaskStatusMap(); - return - ''. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '
'. - 'T'.$task->getID(). - ''. - idx($status_map, $task->getStatus(), 'Unknown'). - ''. - ($task->getOwnerPHID() - ? $handles[$task->getOwnerPHID()]->renderLink() - : 'None'). - ''. - phutil_render_tag( - 'a', - array( - 'href' => '/T'.$task->getID(), - ), - phutil_escape_html($task->getTitle())). - ''. - ManiphestTaskPriority::getTaskPriorityName($task->getPriority()). - ''. - phabricator_datetime($task->getDateModified(), $this->user). - '
'; + $batch = null; + if ($this->showBatchControls) { + $batch = + ''. + javelin_render_tag( + 'input', + array( + 'type' => 'checkbox', + 'name' => 'batch[]', + 'value' => $task->getID(), + 'sigil' => 'maniphest-batch', + ), + null). + ''; + } + + return javelin_render_tag( + 'table', + array( + 'class' => 'maniphest-task-summary', + 'sigil' => 'maniphest-task', + ), + ''. + ''. + ''. + $batch. + ''. + 'T'.$task->getID(). + ''. + ''. + idx($status_map, $task->getStatus(), 'Unknown'). + ''. + ''. + ($task->getOwnerPHID() + ? $handles[$task->getOwnerPHID()]->renderLink() + : 'None'). + ''. + ''. + phutil_render_tag( + 'a', + array( + 'href' => '/T'.$task->getID(), + ), + phutil_escape_html($task->getTitle())). + ''. + ''. + ManiphestTaskPriority::getTaskPriorityName($task->getPriority()). + ''. + ''. + phabricator_datetime($task->getDateModified(), $this->user). + ''. + ''); } } diff --git a/src/applications/maniphest/view/tasksummary/__init__.php b/src/applications/maniphest/view/tasksummary/__init__.php index 8e43605347..bf6b16559a 100644 --- a/src/applications/maniphest/view/tasksummary/__init__.php +++ b/src/applications/maniphest/view/tasksummary/__init__.php @@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'applications/maniphest/constants/priority' phutil_require_module('phabricator', 'applications/maniphest/constants/status'); phutil_require_module('phabricator', 'applications/maniphest/view/base'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/markup'); phutil_require_module('phabricator', 'view/utils'); phutil_require_module('phutil', 'markup'); diff --git a/webroot/rsrc/css/application/maniphest/batch-editor.css b/webroot/rsrc/css/application/maniphest/batch-editor.css new file mode 100644 index 0000000000..10e9a61d62 --- /dev/null +++ b/webroot/rsrc/css/application/maniphest/batch-editor.css @@ -0,0 +1,19 @@ +/** + * @provides maniphest-batch-editor + */ + +.maniphest-batch-actions-table { + width: 100%; + margin: 1em 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/application/maniphest/task-summary.css b/webroot/rsrc/css/application/maniphest/task-summary.css index 6883a97a0a..ac26efb526 100644 --- a/webroot/rsrc/css/application/maniphest/task-summary.css +++ b/webroot/rsrc/css/application/maniphest/task-summary.css @@ -3,26 +3,51 @@ */ .maniphest-task-summary { - border: solid #aaaaaa; - border-width: 1px 0; width: 100%; - margin: 2px 0; - border-collapse: separate; - border-spacing: 0 0px; + margin: 4px 0; + border-collapse: collapse; + + font-size: 13px; + color: #222222; } .maniphest-task-summary td { padding: 4px 0.5%; - background: #f0f0f0; + background: #f6f6f6; white-space: nowrap; + + border-style: solid; + border-top-color: #888888; + border-bottom-color: #666666; + border-width: 1px 0; +} + +.maniphest-batch-selected td { + background: #f6ff88; +} + +.maniphest-task-summary td.maniphest-task-handle { + padding: 0 6px; + width: 1px; + + border-right-width: 1px; + border-right-color: #787878; +} + +.maniphest-task-summary td.maniphest-task-batch { + width: 30px; + text-align: center; +} + +.maniphest-task-summary td.maniphest-task-batch, +.maniphest-task-summary td.maniphest-task-batch input { + cursor: pointer; } .maniphest-task-summary td.maniphest-task-number { font-weight: bold; color: #444444; width: 60px; - border-left-width: 3px; - border-left-style: solid; } .maniphest-task-summary td.maniphest-task-status { @@ -41,12 +66,15 @@ } .maniphest-task-summary td.maniphest-task-priority { - width: 80px; + width: 100px; } .maniphest-task-summary td.maniphest-task-updated { text-align: left; width: 180px; + border-right-width: 1px; + border-right-style: solid; + border-right-color: #787878; } .maniphest-task-summary .pri-bullet { @@ -54,26 +82,32 @@ .maniphest-task-summary .pri-unbreak { border-color: #ff0000; + background: #ff0000; } .maniphest-task-summary .pri-triage { border-color: #ee00ee; + background: #ee00ee; } .maniphest-task-summary .pri-high { - border-color: #ff6666; + border-color: #ff6622; + background: #ff6622; } .maniphest-task-summary .pri-normal { border-color: #ffaa66; + background: #ffaa66; } .maniphest-task-summary .pri-low { border-color: #eecc66; + background: #eecc66; } .maniphest-task-summary .pri-wish { border-color: #0099ff; + background: #0099ff; } .maniphest-task-group-header { @@ -88,3 +122,42 @@ font-size: 11px; color: #666666; } + +.batch-editor-header { + font-size: 11px; + color: #666666; + padding: 4px 0px; + font-weight: bold; +} + +.maniphest-batch-editor { + margin: 1em 1em; +} + +.maniphest-batch-editor-layout { + width: 100%; + border-top: 1px solid #bbbbbb; + background: #efefef; +} + +.maniphest-batch-editor-layout td { + padding: 6px 8px; + white-space: nowrap; +} + +.maniphest-batch-editor-layout a.button, +.maniphest-batch-editor-layout button { + margin: 0px 4px; +} + +.maniphest-batch-editor-layout .batch-select-submit-cell { + text-align: right; +} + +#batch-select-status-cell { + text-align: right; + color: #666666; + font-size: 11px; + vertical-align: middle; + width: 100%; +} diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js new file mode 100644 index 0000000000..41a83fd9c7 --- /dev/null +++ b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js @@ -0,0 +1,114 @@ +/** + * @provides javelin-behavior-maniphest-batch-editor + * @requires javelin-behavior + * javelin-dom + * javelin-util + * phabricator-prefab + * multirow-row-manager + * javelin-tokenizer + * javelin-typeahead-preloaded-source + * javelin-typeahead + * 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 = []; + + addRow({}); + + function renderRow(data) { + + var action_select = JX.Prefab.renderSelect( + { + 'add_project': 'Add Projects', + 'remove_project' : 'Remove Projects'/*, + 'priority': 'Change Priority', + 'add_comment': 'Comment', + 'status': 'Open / Close', + 'assign': 'Assign'*/ + }); + + var tokenizer = build_tokenizer(config.sources.project) + + var r = []; + r.push([null, action_select]); + r.push(['batch-editor-input', tokenizer.template]); + + for (var ii = 0; ii < r.length; ii++) { + r[ii] = JX.$N('td', {className : r[ii][0]}, r[ii][1]); + } + + return { + nodes : r, + dataCallback : function() { + return { + action: action_select.value, + value: JX.keys(tokenizer.object.getTokens()) + }; + } + }; + } + + 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(e) { + var input = JX.$(config.input); + + var actions = []; + for (var k in action_rows) { + actions.push(action_rows[k]()); + } + + input.value = JX.JSON.stringify(actions); + } + + 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(source) { + var template = JX.$N('div', JX.$H(config.tokenizerTemplate)).firstChild; + template.id = ''; + var datasource = new JX.TypeaheadPreloadedSource(source); + var typeahead = new JX.Typeahead(template); + typeahead.setDatasource(datasource); + var tokenizer = new JX.Tokenizer(template); + tokenizer.setTypeahead(typeahead); + tokenizer.start(); + + return { + object: tokenizer, + template: template + }; + } + +}); diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js new file mode 100644 index 0000000000..0e37e80a32 --- /dev/null +++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js @@ -0,0 +1,154 @@ +/** + * @provides javelin-behavior-maniphest-batch-selector + * @requires javelin-behavior + * javelin-dom + * javelin-stratcom + */ + +JX.behavior('maniphest-batch-selector', function(config) { + + // When a task row's selection state is changed, this issues updates to other + // parts of the application. + + var onchange = function(task) { + var input = JX.DOM.find(task, 'input', 'maniphest-batch'); + var state = input.checked; + + JX.DOM.alterClass(task, 'maniphest-batch-selected', state); + + JX.Stratcom.invoke( + (state ? 'maniphest-batch-task-add' : 'maniphest-batch-task-rem'), + null, + {id: input.value}) + }; + + + // Change the selected state of a task. + // If 'to' is undefined, toggle. Otherwise, set to true or false. + + var change = function(task, to) { + + var input = JX.DOM.find(task, 'input', 'maniphest-batch'); + var state = input.checked; + if (to === undefined) { + input.checked = !input.checked; + } else { + input.checked = to; + } + onchange(task); + }; + + + // Change all tasks to some state (used by "select all" / "clear selection" + // buttons). + + var changeall = function(to) { + var inputs = JX.DOM.scry(document.body, 'table', 'maniphest-task'); + for (var ii = 0; ii < inputs.length; ii++) { + change(inputs[ii], to); + } + } + + + // Update the status text showing how many tasks are selected, and the button + // state. + + var selected = {}; + var selected_count = 0; + + var update = function() { + var status = (selected_count == 1) + ? '1 Selected Task' + : selected_count + ' Selected Tasks'; + JX.DOM.setContent(JX.$(config.status), status); + + var submit = JX.$(config.submit); + var disable = (selected_count == 0); + submit.disabled = disable; + JX.DOM.alterClass(submit, 'disabled', disable); + }; + + + // When the user clicks the entire surrounding the checkbox, count it + // as a checkbox click. + + JX.Stratcom.listen( + 'click', + 'maniphest-task', + function(e) { + if (!JX.DOM.isNode(e.getTarget(), 'td')) { + // Only count clicks in the , not (e.g.) the table border. + return; + } + + // Check if the clicked contains a checkbox. + var inputs = JX.DOM.scry(e.getTarget(), 'input', 'maniphest-batch'); + if (!inputs.length) { + return; + } + + change(e.getNode('maniphest-task')); + }); + + + // When he user clicks the , update the rest of the application + // state. + + JX.Stratcom.listen( + ['click', 'onchange'], + 'maniphest-batch', + function(e) { + onchange(e.getNode('maniphest-task')); + }); + + + // When the user clicks "Select All", select all tasks. + + JX.DOM.listen( + JX.$(config.selectNone), + 'click', + null, + function(e) { + changeall(false); + e.kill(); + }); + + + // When the user clicks "Clear Selection", clear the selection. + + JX.DOM.listen( + JX.$(config.selectAll), + 'click', + null, + function(e) { + changeall(true); + e.kill(); + }); + + + JX.Stratcom.listen( + 'maniphest-batch-task-add', + null, + function(e) { + var id = e.getData().id; + if (!(id in selected)) { + selected[id] = true; + selected_count++; + update(); + } + }); + + + JX.Stratcom.listen( + 'maniphest-batch-task-rem', + null, + function(e) { + var id = e.getData().id; + if (id in selected) { + delete selected[id]; + selected_count--; + update(); + } + }); + +});