1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 13:30:55 +01:00

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
This commit is contained in:
epriestley 2012-02-24 13:00:48 -08:00
parent aad7ddaa75
commit 386dcfff7e
15 changed files with 787 additions and 45 deletions

3
.gitignore vendored
View file

@ -11,3 +11,6 @@
# NetBeans project files
/nbproject/
# Arcanist scratch directory
/.arc

View file

@ -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(

View file

@ -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',

View file

@ -177,6 +177,7 @@ class AphrontDefaultApplicationConfiguration
'$' => 'ManiphestTaskListController',
'view/(?P<view>\w+)/$' => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?$' => 'ManiphestReportController',
'batch/$' => 'ManiphestBatchEditController',
'task/' => array(
'create/$' => 'ManiphestTaskEditController',
'edit/(?P<id>\d+)/$' => 'ManiphestTaskEditController',

View file

@ -0,0 +1,197 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group maniphest
*/
final class ManiphestBatchEditController extends ManiphestController {
public function processRequest() {
$request = $this->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('<p>These tasks will be edited:</p>');
$form->appendChild($list);
$form->appendChild(
'<h1>Actions</h1>'.
'<div class="aphront-form-inset">'.
'<div style="float: right;">'.
javelin_render_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'add-action',
'mustcapture' => true,
),
'Add Another Action').
'</div>'.
'<div style="clear: both;"></div>'.
javelin_render_tag(
'table',
array(
'sigil' => 'maniphest-batch-actions',
'class' => 'maniphest-batch-actions-table',
),
'').
'</div>')
->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;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype');
phutil_require_module('phabricator', 'applications/maniphest/controller/base');
phutil_require_module('phabricator', 'applications/maniphest/editor/transaction');
phutil_require_module('phabricator', 'applications/maniphest/storage/task');
phutil_require_module('phabricator', 'applications/maniphest/storage/transaction');
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/tokenizer');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('ManiphestBatchEditController.php');

View file

@ -222,20 +222,35 @@ class ManiphestTaskListController extends ManiphestController {
"Displaying tasks {$cur} - {$max} of {$tot}.".
'</div>');
$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(
'<h1 class="maniphest-task-group-header">'.
phutil_escape_html($group).' ('.$count.')'.
'</h1>');
$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 &raquo;');
return
'<div class="maniphest-batch-editor">'.
'<div class="batch-editor-header">Batch Task Editor</div>'.
'<table class="maniphest-batch-editor-layout">'.
'<tr>'.
'<td>'.
$select_all.
$select_none.
'</td>'.
'<td id="batch-select-status-cell">'.
'0 Selected Tasks'.
'</td>'.
'<td class="batch-select-submit-cell">'.$submit.'</td>'.
'</tr>'.
'</table>'.
'</table>';
}
}

View file

@ -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');

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ class ManiphestTaskListView extends ManiphestView {
private $tasks;
private $handles;
private $user;
private $showBatchControls;
public function setTasks(array $tasks) {
$this->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();

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ class ManiphestTaskSummaryView extends ManiphestView {
private $task;
private $handles;
private $user;
private $showBatchControls;
public function setTask(ManiphestTask $task) {
$this->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
'<table class="maniphest-task-summary">'.
'<tr>'.
'<td class="maniphest-task-number '.$pri_class.'">'.
'T'.$task->getID().
'</td>'.
'<td class="maniphest-task-status">'.
idx($status_map, $task->getStatus(), 'Unknown').
'</td>'.
'<td class="maniphest-task-owner">'.
($task->getOwnerPHID()
? $handles[$task->getOwnerPHID()]->renderLink()
: '<em>None</em>').
'</td>'.
'<td class="maniphest-task-name">'.
phutil_render_tag(
'a',
array(
'href' => '/T'.$task->getID(),
),
phutil_escape_html($task->getTitle())).
'</td>'.
'<td class="maniphest-task-priority">'.
ManiphestTaskPriority::getTaskPriorityName($task->getPriority()).
'</td>'.
'<td class="maniphest-task-updated">'.
phabricator_datetime($task->getDateModified(), $this->user).
'</td>'.
'</tr>'.
'</table>';
$batch = null;
if ($this->showBatchControls) {
$batch =
'<td class="maniphest-task-batch">'.
javelin_render_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'batch[]',
'value' => $task->getID(),
'sigil' => 'maniphest-batch',
),
null).
'</td>';
}
return javelin_render_tag(
'table',
array(
'class' => 'maniphest-task-summary',
'sigil' => 'maniphest-task',
),
'<tr>'.
'<td class="maniphest-task-handle '.$pri_class.'">'.
'</td>'.
$batch.
'<td class="maniphest-task-number">'.
'T'.$task->getID().
'</td>'.
'<td class="maniphest-task-status">'.
idx($status_map, $task->getStatus(), 'Unknown').
'</td>'.
'<td class="maniphest-task-owner">'.
($task->getOwnerPHID()
? $handles[$task->getOwnerPHID()]->renderLink()
: '<em>None</em>').
'</td>'.
'<td class="maniphest-task-name">'.
phutil_render_tag(
'a',
array(
'href' => '/T'.$task->getID(),
),
phutil_escape_html($task->getTitle())).
'</td>'.
'<td class="maniphest-task-priority">'.
ManiphestTaskPriority::getTaskPriorityName($task->getPriority()).
'</td>'.
'<td class="maniphest-task-updated">'.
phabricator_datetime($task->getDateModified(), $this->user).
'</td>'.
'</tr>');
}
}

View file

@ -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');

View file

@ -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;
}

View file

@ -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%;
}

View file

@ -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
};
}
});

View file

@ -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 <td /> 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 <td />, not (e.g.) the table border.
return;
}
// Check if the clicked <td /> 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 <input />, 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();
}
});
});