mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 08:52:39 +01:00
Add "Change Priority", "Open / Close", "Comment" and "Assign" actions to Maniphest batch editor
Summary: I think these are all the actions which make any sense. Test Plan: - Performed and verified each action through the batch editor. - Performed a large batch edit which applied each action type multiple times and verified the aggregate behavior was correct. Reviewers: btrahan, cpiro Reviewed By: btrahan CC: aran, epriestley Maniphest Tasks: T441 Differential Revision: https://secure.phabricator.com/D1971
This commit is contained in:
parent
5c5ead2666
commit
6c412961eb
5 changed files with 205 additions and 88 deletions
|
@ -536,7 +536,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'javelin-behavior-differential-dropdown-menus' =>
|
||||
array(
|
||||
'uri' => '/res/4bb3ae9a/rsrc/js/application/differential/behavior-dropdown-menus.js',
|
||||
'uri' => '/res/2549921f/rsrc/js/application/differential/behavior-dropdown-menus.js',
|
||||
'type' => 'js',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -695,7 +695,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'javelin-behavior-maniphest-batch-editor' =>
|
||||
array(
|
||||
'uri' => '/res/d7b7f061/rsrc/js/application/maniphest/behavior-batch-editor.js',
|
||||
'uri' => '/res/d22661be/rsrc/js/application/maniphest/behavior-batch-editor.js',
|
||||
'type' => 'js',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -704,10 +704,7 @@ celerity_register_resource_map(array(
|
|||
2 => 'javelin-util',
|
||||
3 => 'phabricator-prefab',
|
||||
4 => 'multirow-row-manager',
|
||||
5 => 'javelin-tokenizer',
|
||||
6 => 'javelin-typeahead-preloaded-source',
|
||||
7 => 'javelin-typeahead',
|
||||
8 => 'javelin-json',
|
||||
5 => 'javelin-json',
|
||||
),
|
||||
'disk' => '/rsrc/js/application/maniphest/behavior-batch-editor.js',
|
||||
),
|
||||
|
@ -1649,7 +1646,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'phabricator-prefab' =>
|
||||
array(
|
||||
'uri' => '/res/956c8474/rsrc/js/application/core/Prefab.js',
|
||||
'uri' => '/res/2734e45f/rsrc/js/application/core/Prefab.js',
|
||||
'type' => 'js',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -2033,7 +2030,7 @@ celerity_register_resource_map(array(
|
|||
'uri' => '/res/pkg/551249fc/differential.pkg.css',
|
||||
'type' => 'css',
|
||||
),
|
||||
'0d08d81b' =>
|
||||
'9b256876' =>
|
||||
array(
|
||||
'name' => 'differential.pkg.js',
|
||||
'symbols' =>
|
||||
|
@ -2056,7 +2053,7 @@ celerity_register_resource_map(array(
|
|||
15 => 'javelin-behavior-differential-dropdown-menus',
|
||||
16 => 'javelin-behavior-buoyant',
|
||||
),
|
||||
'uri' => '/res/pkg/0d08d81b/differential.pkg.js',
|
||||
'uri' => '/res/pkg/9b256876/differential.pkg.js',
|
||||
'type' => 'js',
|
||||
),
|
||||
'61f9d480' =>
|
||||
|
@ -2147,7 +2144,7 @@ celerity_register_resource_map(array(
|
|||
'aphront-typeahead-control-css' => '05e42357',
|
||||
'differential-changeset-view-css' => '551249fc',
|
||||
'differential-core-view-css' => '551249fc',
|
||||
'differential-inline-comment-editor' => '0d08d81b',
|
||||
'differential-inline-comment-editor' => '9b256876',
|
||||
'differential-local-commits-view-css' => '551249fc',
|
||||
'differential-revision-add-comment-css' => '551249fc',
|
||||
'differential-revision-comment-css' => '551249fc',
|
||||
|
@ -2158,27 +2155,27 @@ celerity_register_resource_map(array(
|
|||
'diffusion-commit-view-css' => '61f9d480',
|
||||
'javelin-behavior' => '4fbae2af',
|
||||
'javelin-behavior-aphront-basic-tokenizer' => '2af849fb',
|
||||
'javelin-behavior-aphront-drag-and-drop' => '0d08d81b',
|
||||
'javelin-behavior-aphront-drag-and-drop-textarea' => '0d08d81b',
|
||||
'javelin-behavior-aphront-drag-and-drop' => '9b256876',
|
||||
'javelin-behavior-aphront-drag-and-drop-textarea' => '9b256876',
|
||||
'javelin-behavior-aphront-form-disable-on-submit' => '21d01ed8',
|
||||
'javelin-behavior-buoyant' => '0d08d81b',
|
||||
'javelin-behavior-differential-accept-with-errors' => '0d08d81b',
|
||||
'javelin-behavior-differential-add-reviewers-and-ccs' => '0d08d81b',
|
||||
'javelin-behavior-differential-comment-jump' => '0d08d81b',
|
||||
'javelin-behavior-differential-diff-radios' => '0d08d81b',
|
||||
'javelin-behavior-differential-dropdown-menus' => '0d08d81b',
|
||||
'javelin-behavior-differential-edit-inline-comments' => '0d08d81b',
|
||||
'javelin-behavior-differential-feedback-preview' => '0d08d81b',
|
||||
'javelin-behavior-differential-keyboard-navigation' => '0d08d81b',
|
||||
'javelin-behavior-differential-populate' => '0d08d81b',
|
||||
'javelin-behavior-differential-show-more' => '0d08d81b',
|
||||
'javelin-behavior-buoyant' => '9b256876',
|
||||
'javelin-behavior-differential-accept-with-errors' => '9b256876',
|
||||
'javelin-behavior-differential-add-reviewers-and-ccs' => '9b256876',
|
||||
'javelin-behavior-differential-comment-jump' => '9b256876',
|
||||
'javelin-behavior-differential-diff-radios' => '9b256876',
|
||||
'javelin-behavior-differential-dropdown-menus' => '9b256876',
|
||||
'javelin-behavior-differential-edit-inline-comments' => '9b256876',
|
||||
'javelin-behavior-differential-feedback-preview' => '9b256876',
|
||||
'javelin-behavior-differential-keyboard-navigation' => '9b256876',
|
||||
'javelin-behavior-differential-populate' => '9b256876',
|
||||
'javelin-behavior-differential-show-more' => '9b256876',
|
||||
'javelin-behavior-maniphest-batch-selector' => '86fc0b0c',
|
||||
'javelin-behavior-maniphest-transaction-controls' => '86fc0b0c',
|
||||
'javelin-behavior-maniphest-transaction-expand' => '86fc0b0c',
|
||||
'javelin-behavior-maniphest-transaction-preview' => '86fc0b0c',
|
||||
'javelin-behavior-phabricator-autofocus' => '21d01ed8',
|
||||
'javelin-behavior-phabricator-keyboard-shortcuts' => '21d01ed8',
|
||||
'javelin-behavior-phabricator-object-selector' => '0d08d81b',
|
||||
'javelin-behavior-phabricator-object-selector' => '9b256876',
|
||||
'javelin-behavior-phabricator-watch-anchor' => '21d01ed8',
|
||||
'javelin-behavior-refresh-csrf' => '21d01ed8',
|
||||
'javelin-behavior-workflow' => '21d01ed8',
|
||||
|
@ -2207,7 +2204,7 @@ celerity_register_resource_map(array(
|
|||
'phabricator-core-buttons-css' => '05e42357',
|
||||
'phabricator-core-css' => '05e42357',
|
||||
'phabricator-directory-css' => '05e42357',
|
||||
'phabricator-drag-and-drop-file-upload' => '0d08d81b',
|
||||
'phabricator-drag-and-drop-file-upload' => '9b256876',
|
||||
'phabricator-dropdown-menu' => '21d01ed8',
|
||||
'phabricator-jump-nav' => '05e42357',
|
||||
'phabricator-keyboard-shortcut' => '21d01ed8',
|
||||
|
@ -2216,7 +2213,7 @@ celerity_register_resource_map(array(
|
|||
'phabricator-object-selector-css' => '551249fc',
|
||||
'phabricator-paste-file-upload' => '21d01ed8',
|
||||
'phabricator-remarkup-css' => '05e42357',
|
||||
'phabricator-shaped-request' => '0d08d81b',
|
||||
'phabricator-shaped-request' => '9b256876',
|
||||
'phabricator-standard-page-view' => '05e42357',
|
||||
'phabricator-transaction-view-css' => '05e42357',
|
||||
'syntax-highlighting-css' => '05e42357',
|
||||
|
|
|
@ -45,8 +45,10 @@ final class ManiphestBatchEditController extends ManiphestController {
|
|||
}
|
||||
}
|
||||
|
||||
$task_ids = implode(',', mpull($tasks, 'getID'));
|
||||
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI('/maniphest/');
|
||||
->setURI('/maniphest/view/custom/?s=oc&tasks='.$task_ids);
|
||||
}
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
|
@ -71,9 +73,19 @@ final class ManiphestBatchEditController extends ManiphestController {
|
|||
'root' => 'maniphest-batch-edit-form',
|
||||
'tokenizerTemplate' => $template,
|
||||
'sources' => array(
|
||||
'project' => '/typeahead/common/projects/',
|
||||
'project' => array(
|
||||
'src' => '/typeahead/common/projects/',
|
||||
'placeholder' => 'Type a project name...',
|
||||
),
|
||||
'owner' => array(
|
||||
'src' => '/typeahead/common/searchowner/',
|
||||
'placeholder' => 'Type a user name...',
|
||||
'limit' => 1,
|
||||
),
|
||||
),
|
||||
'input' => 'batch-form-actions',
|
||||
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
|
||||
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
|
||||
));
|
||||
|
||||
$form = new AphrontFormView();
|
||||
|
@ -138,34 +150,96 @@ final class ManiphestBatchEditController extends ManiphestController {
|
|||
}
|
||||
|
||||
private function buildTransactions($actions, ManiphestTask $task) {
|
||||
$template = new ManiphestTransaction();
|
||||
$template->setAuthorPHID($this->getRequest()->getUser()->getPHID());
|
||||
|
||||
// TODO: Set content source to "batch edit".
|
||||
|
||||
$project_xaction = null;
|
||||
$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,
|
||||
);
|
||||
|
||||
$xactions = array();
|
||||
foreach ($actions as $action) {
|
||||
$value = $action['value'];
|
||||
switch ($action['action']) {
|
||||
case 'add_project':
|
||||
case 'remove_project':
|
||||
if (empty($type_map[$action['action']])) {
|
||||
throw new Exception("Unknown batch edit action '{$action}'!");
|
||||
}
|
||||
|
||||
$is_remove = ($action['action'] == 'remove_project');
|
||||
$type = $type_map[$action['action']];
|
||||
|
||||
// We want to roll up all of the add and remove actions into a single
|
||||
// transaction so we don't restore removed projects or remove added
|
||||
// projects in subsequent transactions. If we've already made some
|
||||
// modification to a task's projects, use that as the starting point.
|
||||
// Otherwise, start with the value on the task.
|
||||
// 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 ($project_xaction) {
|
||||
$xaction = $project_xaction;
|
||||
$current = $xaction->getNewValue();
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// If the edit doesn't change anything, go to the next 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');
|
||||
|
||||
$current = array_fill_keys($current, true);
|
||||
$value = array_fill_keys($value, true);
|
||||
|
@ -190,25 +264,35 @@ final class ManiphestBatchEditController extends ManiphestController {
|
|||
}
|
||||
|
||||
if (!$did_something) {
|
||||
break;
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// If this is the first project-related transaction, create a new
|
||||
// transaction object and populate it with appropriate defaults.
|
||||
|
||||
if (!$project_xaction) {
|
||||
$xaction = clone $template;
|
||||
$xaction->setTransactionType(
|
||||
ManiphestTransactionType::TYPE_PROJECTS);
|
||||
|
||||
$xactions[] = $xaction;
|
||||
$project_xaction = $xaction;
|
||||
}
|
||||
|
||||
$new = array_keys($new);
|
||||
$xaction->setNewValue($new);
|
||||
$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;
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/constants/owner');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/constants/priority');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/constants/status');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/editor/transaction');
|
||||
|
|
|
@ -35,7 +35,8 @@ JX.install('Prefab', {
|
|||
* Build a Phabricator tokenizer out of a configuration with application
|
||||
* sorting, datasource and placeholder rules.
|
||||
*
|
||||
* - `id` Root tokenizer ID.
|
||||
* - `id` Root tokenizer ID (alternatively, pass `root`).
|
||||
* - `root` Root tokenizer node (replaces `id`).
|
||||
* - `src` Datasource URI.
|
||||
* - `ondemand` Optional, use an ondemand source.
|
||||
* - `value` Optional, initial value.
|
||||
|
@ -45,7 +46,7 @@ JX.install('Prefab', {
|
|||
*
|
||||
*/
|
||||
buildTokenizer : function(config) {
|
||||
var root = JX.$(config.id);
|
||||
var root = config.root || JX.$(config.id);
|
||||
|
||||
var datasource;
|
||||
if (config.ondemand) {
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
* javelin-util
|
||||
* phabricator-prefab
|
||||
* multirow-row-manager
|
||||
* javelin-tokenizer
|
||||
* javelin-typeahead-preloaded-source
|
||||
* javelin-typeahead
|
||||
* javelin-json
|
||||
*/
|
||||
|
||||
|
@ -25,29 +22,64 @@ JX.behavior('maniphest-batch-editor', function(config) {
|
|||
var action_select = JX.Prefab.renderSelect(
|
||||
{
|
||||
'add_project': 'Add Projects',
|
||||
'remove_project' : 'Remove Projects'/*,
|
||||
'remove_project' : 'Remove Projects',
|
||||
'priority': 'Change Priority',
|
||||
'add_comment': 'Comment',
|
||||
'status': 'Open / Close',
|
||||
'assign': 'Assign'*/
|
||||
'add_comment': 'Comment',
|
||||
'assign': 'Assign'
|
||||
});
|
||||
|
||||
var tokenizer = build_tokenizer(config.sources.project)
|
||||
var proj_tokenizer = build_tokenizer(config.sources.project);
|
||||
var owner_tokenizer = build_tokenizer(config.sources.owner);
|
||||
|
||||
var r = [];
|
||||
r.push([null, action_select]);
|
||||
r.push(['batch-editor-input', tokenizer.template]);
|
||||
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%'}});
|
||||
|
||||
for (var ii = 0; ii < r.length; ii++) {
|
||||
r[ii] = JX.$N('td', {className : r[ii][0]}, r[ii][1]);
|
||||
}
|
||||
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 '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 : r,
|
||||
nodes : [JX.$N('td', {}, action_select), cell],
|
||||
dataCallback : function() {
|
||||
return {
|
||||
action: action_select.value,
|
||||
value: JX.keys(tokenizer.object.getTokens())
|
||||
value: vfunc()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -95,18 +127,18 @@ JX.behavior('maniphest-batch-editor', function(config) {
|
|||
delete action_rows[row_id];
|
||||
});
|
||||
|
||||
function build_tokenizer(source) {
|
||||
function build_tokenizer(tconfig) {
|
||||
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();
|
||||
|
||||
var build_config = JX.copy({}, tconfig);
|
||||
build_config.root = template;
|
||||
|
||||
var built = JX.Prefab.buildTokenizer(build_config);
|
||||
built.tokenizer.start();
|
||||
|
||||
return {
|
||||
object: tokenizer,
|
||||
object: built.tokenizer,
|
||||
template: template
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue