Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2893523
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
14 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
index e73c632bdf..5b6fdbf2cf 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php
@@ -1,295 +1,316 @@
<?php
/**
* @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->setActor($user);
$editor->applyTransactions($task, $xactions);
}
}
$task_ids = implode(',', mpull($tasks, 'getID'));
return id(new AphrontRedirectResponse())
->setURI('/maniphest/view/custom/?s=oc&tasks='.$task_ids);
}
$panel = new AphrontPanelView();
$panel->setHeader(pht('Maniphest Batch Editor'));
$panel->setNoBackground();
$handle_phids = mpull($tasks, 'getOwnerPHID');
$handles = $this->loadViewerHandles($handle_phids);
$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' => array(
'src' => '/typeahead/common/projects/',
'placeholder' => pht('Type a project name...'),
),
'owner' => array(
'src' => '/typeahead/common/searchowner/',
'placeholder' => pht('Type a user name...'),
'limit' => 1,
),
+ 'cc' => array(
+ 'src' => '/typeahead/common/mailable/',
+ 'placeholder' => pht('Type a user name...'),
+ )
),
'input' => 'batch-form-actions',
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
));
$form = new AphrontFormView();
$form->setUser($user);
$form->setID('maniphest-batch-edit-form');
foreach ($tasks as $task) {
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'batch[]',
'value' => $task->getID(),
)));
}
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'actions',
'id' => 'batch-form-actions',
)));
$form->appendChild(
phutil_tag('p', array(), pht('These tasks will be edited:')));
$form->appendChild($list);
$form->appendChild(
id(new AphrontFormInsetView())
->setTitle('Actions')
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'add-action',
'mustcapture' => true,
),
pht('Add Another Action')))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'maniphest-batch-actions',
'class' => 'maniphest-batch-actions-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Update Tasks'))
->addCancelButton('/maniphest/', 'Done'));
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => pht('Batch Editor'),
));
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => ManiphestTransactionType::TYPE_NONE,
'assign' => ManiphestTransactionType::TYPE_OWNER,
'status' => ManiphestTransactionType::TYPE_STATUS,
'priority' => ManiphestTransactionType::TYPE_PRIORITY,
'add_project' => ManiphestTransactionType::TYPE_PROJECTS,
'remove_project' => ManiphestTransactionType::TYPE_PROJECTS,
+ 'add_ccs' => ManiphestTransactionType::TYPE_CCS,
+ 'remove_ccs' => ManiphestTransactionType::TYPE_CCS,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
+ 'add_ccs' => true,
+ 'remove_ccs' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
throw new Exception("Unknown batch edit action '{$action}'!");
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$current = null;
break;
case ManiphestTransactionType::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransactionType::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$current = $task->getProjectPHIDs();
break;
+ case ManiphestTransactionType::TYPE_CCS:
+ $current = $task->getCCPHIDs();
+ break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
if (!strlen($value)) {
continue 2;
}
break;
case ManiphestTransactionType::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
$value = null;
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
if (empty($value)) {
continue 2;
}
break;
+ case ManiphestTransactionType::TYPE_CCS:
+ if (empty($value)) {
+ continue 2;
+ }
+ break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
- $is_remove = ($action['action'] == 'remove_project');
+ case ManiphestTransactionType::TYPE_CCS:
+ $remove_actions = array(
+ 'remove_project' => true,
+ 'remove_ccs' => true,
+ );
+ $is_remove = isset($remove_actions[$action['action']]);
$current = array_fill_keys($current, 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) {
continue 2;
}
$value = array_keys($new);
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
$template->setAuthorPHID($this->getRequest()->getUser()->getPHID());
// TODO: Set content source to "batch edit".
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$xaction->setComments($value);
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}
diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
index 56235cd00f..7581318730 100644
--- a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
@@ -1,146 +1,156 @@
/**
* @provides javelin-behavior-maniphest-batch-editor
* @requires javelin-behavior
* javelin-dom
* javelin-util
* phabricator-prefab
* multirow-row-manager
* 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',
'status': 'Open / Close',
'add_comment': 'Comment',
- 'assign': 'Assign'
+ 'assign': 'Assign',
+ 'add_ccs' : 'Add CCs',
+ 'remove_ccs' : 'Remove CCs'
});
var proj_tokenizer = build_tokenizer(config.sources.project);
var owner_tokenizer = build_tokenizer(config.sources.owner);
+ var cc_tokenizer = build_tokenizer(config.sources.cc);
var priority_select = JX.Prefab.renderSelect(config.priorityMap);
var status_select = JX.Prefab.renderSelect(config.statusMap);
var comment_input = JX.$N('input', {style: {width: '100%'}});
var cell = JX.$N('td', {className: 'batch-editor-input'});
var vfunc = null;
function update() {
switch (action_select.value) {
case 'add_project':
case 'remove_project':
JX.DOM.setContent(cell, proj_tokenizer.template);
vfunc = function() {
return JX.keys(proj_tokenizer.object.getTokens());
};
break;
+ case 'add_ccs':
+ case 'remove_ccs':
+ JX.DOM.setContent(cell, cc_tokenizer.template);
+ vfunc = function() {
+ return JX.keys(cc_tokenizer.object.getTokens());
+ };
+ break;
case 'assign':
JX.DOM.setContent(cell, owner_tokenizer.template);
vfunc = function() {
return JX.keys(owner_tokenizer.object.getTokens());
};
break;
case 'add_comment':
JX.DOM.setContent(cell, comment_input);
vfunc = function() {
return comment_input.value;
};
break;
case 'priority':
JX.DOM.setContent(cell, priority_select);
vfunc = function() { return priority_select.value; };
break;
case 'status':
JX.DOM.setContent(cell, status_select);
vfunc = function() { return status_select.value; };
break;
}
};
JX.DOM.listen(action_select, 'change', null, update);
update();
return {
nodes : [JX.$N('td', {}, action_select), cell],
dataCallback : function() {
return {
action: action_select.value,
value: vfunc()
};
}
};
}
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(tconfig) {
var template = JX.$N('div', JX.$H(config.tokenizerTemplate)).firstChild;
template.id = '';
var build_config = JX.copy({}, tconfig);
build_config.root = template;
var built = JX.Prefab.buildTokenizer(build_config);
built.tokenizer.start();
return {
object: built.tokenizer,
template: template
};
}
});
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 18:33 (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127469
Default Alt Text
(14 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment