From 386dcfff7ed16427961df4d914578c388c5acf65 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Feb 2012 13:00:48 -0800 Subject: [PATCH] Rough batch editor for Maniphest Summary: First stab at a batch editor for Maniphest. Basically, you can select a group of tasks and then import them into the "batch" interface, where you can edit all of them at once. High level goal is to make it easier for users in PM/filer/support/QA roles to deal with large numbers of tasks quickly. This implementation has a few major limitations: - The only available actions are "add projects" and "remove projects". - There is no review / undo / log stuff. - All the changes are applied in-process, which may not scale terribly well. However, the immediate need is just around projects and this seemed like a reasonable place to draw the line for a minimal useful version of the tool. Test Plan: Used batch editor to add and remove projects from groups of tasks. Reviewers: btrahan, yairlivne Reviewed By: btrahan CC: aran, epriestley, sandra Maniphest Tasks: T441 Differential Revision: https://secure.phabricator.com/D1680 --- .gitignore | 3 + src/__celerity_resource_map__.php | 41 +++- src/__phutil_library_map__.php | 2 + ...AphrontDefaultApplicationConfiguration.php | 1 + .../batch/ManiphestBatchEditController.php | 197 ++++++++++++++++++ .../maniphest/controller/batch/__init__.php | 29 +++ .../tasklist/ManiphestTaskListController.php | 76 ++++++- .../controller/tasklist/__init__.php | 3 + .../view/tasklist/ManiphestTaskListView.php | 9 +- .../tasksummary/ManiphestTaskSummaryView.php | 90 +++++--- .../maniphest/view/tasksummary/__init__.php | 1 + .../application/maniphest/batch-editor.css | 19 ++ .../application/maniphest/task-summary.css | 93 ++++++++- .../maniphest/behavior-batch-editor.js | 114 ++++++++++ .../maniphest/behavior-batch-selector.js | 154 ++++++++++++++ 15 files changed, 787 insertions(+), 45 deletions(-) create mode 100644 src/applications/maniphest/controller/batch/ManiphestBatchEditController.php create mode 100644 src/applications/maniphest/controller/batch/__init__.php create mode 100644 webroot/rsrc/css/application/maniphest/batch-editor.css create mode 100644 webroot/rsrc/js/application/maniphest/behavior-batch-editor.js create mode 100644 webroot/rsrc/js/application/maniphest/behavior-batch-selector.js 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(); + } + }); + +});