mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 08:52:39 +01:00
Make Maniphest task statuses user configurable
Summary: Fixes T1812. Moves the internal configuration into public space and documents it. Test Plan: - Tried to set it to some invalid stuff. - Set it to various valid things. - Browsed around, changed statuses, filtered statuses, viewed statuses, merged duplictes, examined transaction record, created tasks. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T1812 Differential Revision: https://secure.phabricator.com/D8585
This commit is contained in:
parent
7713fb5d99
commit
9ca86b69b7
6 changed files with 235 additions and 78 deletions
|
@ -882,6 +882,7 @@ phutil_register_library_map(array(
|
||||||
'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
|
'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
|
||||||
'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
|
'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
|
||||||
'ManiphestSearchIndexer' => 'applications/maniphest/search/ManiphestSearchIndexer.php',
|
'ManiphestSearchIndexer' => 'applications/maniphest/search/ManiphestSearchIndexer.php',
|
||||||
|
'ManiphestStatusConfigOptionType' => 'applications/maniphest/config/ManiphestStatusConfigOptionType.php',
|
||||||
'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
|
'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
|
||||||
'ManiphestSubscribeController' => 'applications/maniphest/controller/ManiphestSubscribeController.php',
|
'ManiphestSubscribeController' => 'applications/maniphest/controller/ManiphestSubscribeController.php',
|
||||||
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
|
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
|
||||||
|
@ -1323,6 +1324,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
|
'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
|
||||||
'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
|
'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
|
||||||
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
|
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
|
||||||
|
'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php',
|
||||||
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
|
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
|
||||||
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
|
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
|
||||||
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
|
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
|
||||||
|
@ -3521,6 +3523,7 @@ phutil_register_library_map(array(
|
||||||
'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler',
|
'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler',
|
||||||
'ManiphestReportController' => 'ManiphestController',
|
'ManiphestReportController' => 'ManiphestController',
|
||||||
'ManiphestSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
|
'ManiphestSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
|
||||||
|
'ManiphestStatusConfigOptionType' => 'PhabricatorConfigJSONOptionType',
|
||||||
'ManiphestSubpriorityController' => 'ManiphestController',
|
'ManiphestSubpriorityController' => 'ManiphestController',
|
||||||
'ManiphestSubscribeController' => 'ManiphestController',
|
'ManiphestSubscribeController' => 'ManiphestController',
|
||||||
'ManiphestTask' =>
|
'ManiphestTask' =>
|
||||||
|
@ -4032,6 +4035,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorConfigIgnoreController' => 'PhabricatorApplicationsController',
|
'PhabricatorConfigIgnoreController' => 'PhabricatorApplicationsController',
|
||||||
'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
|
'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
|
||||||
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
|
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
|
||||||
|
'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType',
|
||||||
'PhabricatorConfigListController' => 'PhabricatorConfigController',
|
'PhabricatorConfigListController' => 'PhabricatorConfigController',
|
||||||
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
|
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
|
||||||
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
||||||
|
|
|
@ -102,7 +102,6 @@ final class PhabricatorConfigEditController
|
||||||
$error_view = null;
|
$error_view = null;
|
||||||
if ($errors) {
|
if ($errors) {
|
||||||
$error_view = id(new AphrontErrorView())
|
$error_view = id(new AphrontErrorView())
|
||||||
->setTitle(pht('You broke everything!'))
|
|
||||||
->setErrors($errors);
|
->setErrors($errors);
|
||||||
} else if ($option->getHidden()) {
|
} else if ($option->getHidden()) {
|
||||||
$msg = pht(
|
$msg = pht(
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class PhabricatorConfigJSONOptionType
|
||||||
|
extends PhabricatorConfigOptionType {
|
||||||
|
|
||||||
|
public function readRequest(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
AphrontRequest $request) {
|
||||||
|
|
||||||
|
$e_value = null;
|
||||||
|
$errors = array();
|
||||||
|
$storage_value = $request->getStr('value');
|
||||||
|
$display_value = $request->getStr('value');
|
||||||
|
|
||||||
|
if (strlen($display_value)) {
|
||||||
|
$storage_value = phutil_json_decode($display_value);
|
||||||
|
if ($storage_value === null) {
|
||||||
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = pht(
|
||||||
|
'Configuration value should be specified in JSON. The provided '.
|
||||||
|
'value is not valid JSON.');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$this->validateOption($option, $storage_value);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = $ex->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$storage_value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array($e_value, $errors, $storage_value, $display_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayValue(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
PhabricatorConfigEntry $entry) {
|
||||||
|
$value = $entry->getValue();
|
||||||
|
if (!$value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = new PhutilJSON();
|
||||||
|
return $json->encodeFormatted($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderControl(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
$display_value,
|
||||||
|
$e_value) {
|
||||||
|
|
||||||
|
return id(new AphrontFormTextAreaControl())
|
||||||
|
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
|
||||||
|
->setName('value')
|
||||||
|
->setLabel(pht('Value'))
|
||||||
|
->setValue($display_value)
|
||||||
|
->setError($e_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class ManiphestStatusConfigOptionType
|
||||||
|
extends PhabricatorConfigJSONOptionType {
|
||||||
|
|
||||||
|
public function validateOption(PhabricatorConfigOption $option, $value) {
|
||||||
|
ManiphestTaskStatus::validateConfiguration($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -46,6 +46,160 @@ final class PhabricatorManiphestConfigOptions
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$status_type = 'custom:ManiphestStatusConfigOptionType';
|
||||||
|
$status_defaults = array(
|
||||||
|
'open' => array(
|
||||||
|
'name' => pht('Open'),
|
||||||
|
'special' => ManiphestTaskStatus::SPECIAL_DEFAULT,
|
||||||
|
),
|
||||||
|
'resolved' => array(
|
||||||
|
'name' => pht('Resolved'),
|
||||||
|
'name.full' => pht('Closed, Resolved'),
|
||||||
|
'closed' => true,
|
||||||
|
'special' => ManiphestTaskStatus::SPECIAL_CLOSED,
|
||||||
|
'prefixes' => array(
|
||||||
|
'closed',
|
||||||
|
'closes',
|
||||||
|
'close',
|
||||||
|
'fix',
|
||||||
|
'fixes',
|
||||||
|
'fixed',
|
||||||
|
'resolve',
|
||||||
|
'resolves',
|
||||||
|
'resolved',
|
||||||
|
),
|
||||||
|
'suffixes' => array(
|
||||||
|
'as resolved',
|
||||||
|
'as fixed',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'wontfix' => array(
|
||||||
|
'name' => pht('Wontfix'),
|
||||||
|
'name.full' => pht('Closed, Wontfix'),
|
||||||
|
'closed' => true,
|
||||||
|
'prefixes' => array(
|
||||||
|
'wontfix',
|
||||||
|
'wontfixes',
|
||||||
|
'wontfixed',
|
||||||
|
),
|
||||||
|
'suffixes' => array(
|
||||||
|
'as wontfix',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'invalid' => array(
|
||||||
|
'name' => pht('Invalid'),
|
||||||
|
'name.full' => pht('Closed, Invalid'),
|
||||||
|
'closed' => true,
|
||||||
|
'prefixes' => array(
|
||||||
|
'invalidate',
|
||||||
|
'invalidates',
|
||||||
|
'invalidated',
|
||||||
|
),
|
||||||
|
'suffixes' => array(
|
||||||
|
'as invalid',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'duplicate' => array(
|
||||||
|
'name' => pht('Duplicate'),
|
||||||
|
'name.full' => pht('Closed, Duplicate'),
|
||||||
|
'transaction.icon' => 'delete',
|
||||||
|
'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE,
|
||||||
|
'closed' => true,
|
||||||
|
),
|
||||||
|
'spite' => array(
|
||||||
|
'name' => pht('Spite'),
|
||||||
|
'name.full' => pht('Closed, Spite'),
|
||||||
|
'name.action' => pht('Spited'),
|
||||||
|
'transaction.icon' => 'dislike',
|
||||||
|
'silly' => true,
|
||||||
|
'closed' => true,
|
||||||
|
'prefixes' => array(
|
||||||
|
'spite',
|
||||||
|
'spites',
|
||||||
|
'spited',
|
||||||
|
),
|
||||||
|
'suffixes' => array(
|
||||||
|
'out of spite',
|
||||||
|
'as spite',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$status_description = $this->deformat(pht(<<<EOTEXT
|
||||||
|
Allows you to edit, add, or remove the task statuses available in Maniphest,
|
||||||
|
like "Open", "Resolved" and "Invalid". The configuration should contain a map
|
||||||
|
of status constants to status specifications (see defaults below for examples).
|
||||||
|
|
||||||
|
The constant for each status should be 1-12 characters long and contain only
|
||||||
|
lowercase letters and digits. Valid examples are "open", "closed", and
|
||||||
|
"invalid". Users will not normally see these values.
|
||||||
|
|
||||||
|
The keys you can provide in a specification are:
|
||||||
|
|
||||||
|
- `name` //Required string.// Name of the status, like "Invalid".
|
||||||
|
- `name.full` //Optional string.// Longer name, like "Closed, Invalid". This
|
||||||
|
appears on the task detail view in the header.
|
||||||
|
- `name.action` //Optional string.// Action name for email subjects, like
|
||||||
|
"Marked Invalid".
|
||||||
|
- `closed` //Optional bool.// Statuses are either "open" or "closed".
|
||||||
|
Specifying `true` here will mark the status as closed (like "Resolved" or
|
||||||
|
"Invalid"). By default, statuses are open.
|
||||||
|
- `special` //Optional string.// Mark this status as special. The special
|
||||||
|
statuses are:
|
||||||
|
- `default` This is the default status for newly created tasks. You must
|
||||||
|
designate one status as default, and it must be an open status.
|
||||||
|
- `closed` This is the default status for closed tasks (for example, tasks
|
||||||
|
closed via the "!close" action in email). You must designate one status
|
||||||
|
as the default closed status, and it must be a closed status.
|
||||||
|
- `duplicate` This is the status used when tasks are merged into one
|
||||||
|
another as duplicates. You must designate one status for duplicates,
|
||||||
|
and it must be a closed status.
|
||||||
|
- `transaction.icon` //Optional string.// Allows you to choose a different
|
||||||
|
icon to use for this status when showing status changes in the transaction
|
||||||
|
log.
|
||||||
|
- `transaction.color` //Optional string.// Allows you to choose a different
|
||||||
|
color to use for this status when showing status changes in the transaction
|
||||||
|
log.
|
||||||
|
- `silly` //Optional bool.// Marks this status as silly, and thus wholly
|
||||||
|
inappropriate for use by serious businesses.
|
||||||
|
- `prefixes` //Optional list<string>.// Allows you to specify a list of
|
||||||
|
text prefixes which will trigger a task transition into this status
|
||||||
|
when mentioned in a commit message. For example, providing "closes" here
|
||||||
|
will allow users to move tasks to this status by writing `Closes T123` in
|
||||||
|
commit messages.
|
||||||
|
- `suffixes` //Optional list<string>.// Allows you to specify a list of
|
||||||
|
text suffixes which will trigger a task transition into this status
|
||||||
|
when mentioned in a commit message, after a valid prefix. For example,
|
||||||
|
providing "as invalid" here will allow users to move tasks
|
||||||
|
to this status by writing `Closes T123 as invalid`, even if another status
|
||||||
|
is selected by the "Closes" prefix.
|
||||||
|
|
||||||
|
Examining the default configuration and examples below will probably be helpful
|
||||||
|
in understanding these options.
|
||||||
|
|
||||||
|
EOTEXT
|
||||||
|
));
|
||||||
|
|
||||||
|
$status_example = array(
|
||||||
|
'open' => array(
|
||||||
|
'name' => 'Open',
|
||||||
|
'special' => 'default',
|
||||||
|
),
|
||||||
|
'closed' => array(
|
||||||
|
'name' => 'Closed',
|
||||||
|
'special' => 'closed',
|
||||||
|
'closed' => true,
|
||||||
|
),
|
||||||
|
'duplicate' => array(
|
||||||
|
'name' => 'Duplicate',
|
||||||
|
'special' => 'duplicate',
|
||||||
|
'closed' => true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$json = new PhutilJSON();
|
||||||
|
$status_example = $json->encodeFormatted($status_example);
|
||||||
|
|
||||||
// This is intentionally blank for now, until we can move more Maniphest
|
// This is intentionally blank for now, until we can move more Maniphest
|
||||||
// logic to custom fields.
|
// logic to custom fields.
|
||||||
$default_fields = array();
|
$default_fields = array();
|
||||||
|
@ -92,6 +246,10 @@ final class PhabricatorManiphestConfigOptions
|
||||||
"\n\n".
|
"\n\n".
|
||||||
'You can choose which priority is the default for newly created '.
|
'You can choose which priority is the default for newly created '.
|
||||||
'tasks with `maniphest.default-priority`.')),
|
'tasks with `maniphest.default-priority`.')),
|
||||||
|
$this->newOption('maniphest.statuses', $status_type, $status_defaults)
|
||||||
|
->setSummary(pht('Configure Maniphest task statuses.'))
|
||||||
|
->setDescription($status_description)
|
||||||
|
->addExample($status_example, pht('Minimal Valid Config')),
|
||||||
$this->newOption('maniphest.default-priority', 'int', 90)
|
$this->newOption('maniphest.default-priority', 'int', 90)
|
||||||
->setSummary(pht("Default task priority for create flows."))
|
->setSummary(pht("Default task priority for create flows."))
|
||||||
->setDescription(
|
->setDescription(
|
||||||
|
|
|
@ -17,83 +17,7 @@ final class ManiphestTaskStatus extends ManiphestConstants {
|
||||||
const SPECIAL_DUPLICATE = 'duplicate';
|
const SPECIAL_DUPLICATE = 'duplicate';
|
||||||
|
|
||||||
private static function getStatusConfig() {
|
private static function getStatusConfig() {
|
||||||
return array(
|
return PhabricatorEnv::getEnvConfig('maniphest.statuses');
|
||||||
self::STATUS_OPEN => array(
|
|
||||||
'name' => pht('Open'),
|
|
||||||
'special' => self::SPECIAL_DEFAULT,
|
|
||||||
),
|
|
||||||
self::STATUS_CLOSED_RESOLVED => array(
|
|
||||||
'name' => pht('Resolved'),
|
|
||||||
'name.full' => pht('Closed, Resolved'),
|
|
||||||
'closed' => true,
|
|
||||||
'special' => self::SPECIAL_CLOSED,
|
|
||||||
'prefixes' => array(
|
|
||||||
'closed',
|
|
||||||
'closes',
|
|
||||||
'close',
|
|
||||||
'fix',
|
|
||||||
'fixes',
|
|
||||||
'fixed',
|
|
||||||
'resolve',
|
|
||||||
'resolves',
|
|
||||||
'resolved',
|
|
||||||
),
|
|
||||||
'suffixes' => array(
|
|
||||||
'as resolved',
|
|
||||||
'as fixed',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
self::STATUS_CLOSED_WONTFIX => array(
|
|
||||||
'name' => pht('Wontfix'),
|
|
||||||
'name.full' => pht('Closed, Wontfix'),
|
|
||||||
'closed' => true,
|
|
||||||
'prefixes' => array(
|
|
||||||
'wontfix',
|
|
||||||
'wontfixes',
|
|
||||||
'wontfixed',
|
|
||||||
),
|
|
||||||
'suffixes' => array(
|
|
||||||
'as wontfix',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
self::STATUS_CLOSED_INVALID => array(
|
|
||||||
'name' => pht('Invalid'),
|
|
||||||
'name.full' => pht('Closed, Invalid'),
|
|
||||||
'closed' => true,
|
|
||||||
'prefixes' => array(
|
|
||||||
'invalidate',
|
|
||||||
'invalidates',
|
|
||||||
'invalidated',
|
|
||||||
),
|
|
||||||
'suffixes' => array(
|
|
||||||
'as invalid',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
self::STATUS_CLOSED_DUPLICATE => array(
|
|
||||||
'name' => pht('Duplicate'),
|
|
||||||
'name.full' => pht('Closed, Duplicate'),
|
|
||||||
'transaction.icon' => 'delete',
|
|
||||||
'special' => self::SPECIAL_DUPLICATE,
|
|
||||||
'closed' => true,
|
|
||||||
),
|
|
||||||
self::STATUS_CLOSED_SPITE => array(
|
|
||||||
'name' => pht('Spite'),
|
|
||||||
'name.full' => pht('Closed, Spite'),
|
|
||||||
'name.action' => pht('Spited'),
|
|
||||||
'transaction.icon' => 'dislike',
|
|
||||||
'silly' => true,
|
|
||||||
'closed' => true,
|
|
||||||
'prefixes' => array(
|
|
||||||
'spite',
|
|
||||||
'spites',
|
|
||||||
'spited',
|
|
||||||
),
|
|
||||||
'suffixes' => array(
|
|
||||||
'out of spite',
|
|
||||||
'as spite',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getEnabledStatusMap() {
|
private static function getEnabledStatusMap() {
|
||||||
|
|
Loading…
Reference in a new issue