mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-21 22:32:41 +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:
parent
aad7ddaa75
commit
386dcfff7e
15 changed files with 787 additions and 45 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -11,3 +11,6 @@
|
|||
|
||||
# NetBeans project files
|
||||
/nbproject/
|
||||
|
||||
# Arcanist scratch directory
|
||||
/.arc
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
29
src/applications/maniphest/controller/batch/__init__.php
Normal file
29
src/applications/maniphest/controller/batch/__init__.php
Normal 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');
|
|
@ -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 »');
|
||||
|
||||
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>';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
19
webroot/rsrc/css/application/maniphest/batch-editor.css
Normal file
19
webroot/rsrc/css/application/maniphest/batch-editor.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
114
webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
Normal file
114
webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
});
|
154
webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
Normal file
154
webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
Normal 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();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in a new issue