Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2890384
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
95 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php
index 37fd89142c..82cdda49fa 100644
--- a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php
+++ b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php
@@ -1,269 +1,288 @@
<?php
/**
* @group maniphest
*/
abstract class ManiphestAuxiliaryFieldSpecification
extends ManiphestCustomField
implements PhabricatorMarkupInterface {
const RENDER_TARGET_HTML = 'html';
const RENDER_TARGET_TEXT = 'text';
private $label;
private $auxiliaryKey;
private $caption;
private $value;
private $markupEngine;
private $handles;
// TODO: Remove; obsolete.
public function getTask() {
return $this->getObject();
}
// TODO: Remove; obsolete.
public function getUser() {
return $this->getViewer();
}
public function setLabel($val) {
$this->label = $val;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setAuxiliaryKey($val) {
$this->auxiliaryKey = $val;
return $this;
}
public function getAuxiliaryKey() {
return 'std:maniphest:'.$this->auxiliaryKey;
}
public function setCaption($val) {
$this->caption = $val;
return $this;
}
public function getCaption() {
return $this->caption;
}
public function setValue($val) {
$this->value = $val;
return $this;
}
public function getValue() {
return $this->value;
}
public function validate() {
return true;
}
public function isRequired() {
return false;
}
public function setType($val) {
$this->type = $val;
return $this;
}
public function getType() {
return $this->type;
}
public function renderControl() {
return null;
}
public function renderForDetailView() {
return $this->getValue();
}
- /**
- * When the user creates a task, the UI prompts them to "Create another
- * similar task". This copies some fields (e.g., Owner and CCs) but not other
- * fields (e.g., description). If this custom field should also be copied,
- * return true from this method.
- *
- * @return bool True to copy the default value from the template task when
- * creating a new similar task.
- */
- public function shouldCopyWhenCreatingSimilarTask() {
- return false;
- }
-
-
/**
* Render a verb to appear in email titles when a transaction involving this
* field occurs. Specifically, Maniphest emails are formatted like this:
*
* [Maniphest] [Verb Here] TNNN: Task title here
* ^^^^^^^^^
*
* You should optionally return a title-case verb or short phrase like
* "Created", "Retitled", "Closed", "Resolved", "Commented On",
* "Lowered Priority", etc., which describes the transaction.
*
* @param ManiphestTransaction The transaction which needs description.
* @return string|null A short description of the transaction.
*/
public function renderTransactionEmailVerb(
ManiphestTransaction $transaction) {
return null;
}
/**
* Render a short description of the transaction, to appear above comments
* in the Maniphest transaction log. The string will be rendered after the
* acting user's name. Examples are:
*
* added a comment
* added alincoln to CC
* claimed this task
* created this task
* closed this task out of spite
*
* You should return a similar string, describing the transaction.
*
* Note the ##$target## parameter -- Maniphest needs to render transaction
* descriptions for different targets, like web and email. This method will
* be called with a ##ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_*##
* constant describing the intended target.
*
* @param ManiphestTransaction The transaction which needs description.
* @param const Constant describing the rendering target (e.g., html or text).
* @return string|null Description of the transaction.
*/
public function renderTransactionDescription(
ManiphestTransaction $transaction,
$target) {
return 'updated a custom field';
}
public function getRequiredHandlePHIDs() {
return array();
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = array_select_keys(
$handles,
$this->getRequiredHandlePHIDs());
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
"Field is requesting a handle ('{$phid}') it did not require.");
}
return $this->handles[$phid];
}
public function getMarkupFields() {
return array();
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function getMarkupEngine() {
return $this->markupEngine;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
return 'maux:'.$this->getAuxiliaryKey().':'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
public function getMarkupText($field) {
return $this->getValue();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return true;
}
/* -( API Compatibility With New Custom Fields )--------------------------- */
public function getFieldKey() {
return $this->getAuxiliaryKey();
}
public function shouldAppearInEditView() {
return true;
}
public function shouldAppearInPropertyView() {
return true;
}
public function shouldUseStorage() {
return true;
}
public function renderPropertyViewValue() {
return $this->renderForDetailView();
}
public function renderPropertyViewLabel() {
return $this->getLabel();
}
+ public function readValueFromRequest(AphrontRequest $request) {
+ return $this->setValueFromRequest($request);
+ }
-/* -( Legacy Migration Support )------------------------------------------- */
-
-
- // TODO: Migrate to common storage and remove this.
- public static function loadLegacyDataFromStorage(
+ public static function writeLegacyAuxiliaryUpdates(
ManiphestTask $task,
- PhabricatorCustomFieldList $list) {
+ array $map) {
+
+ $table = new ManiphestCustomFieldStorage();
+ $conn_w = $table->establishConnection('w');
+ $update = array();
+ $remove = array();
+
+ foreach ($map as $key => $value) {
+ $index = PhabricatorHash::digestForIndex($key);
+ if ($value === null) {
+ $remove[$index] = true;
+ } else {
+ $update[$index] = $value;
+ }
+ }
- $task->loadAndAttachAuxiliaryAttributes();
+ if ($remove) {
+ queryfx(
+ $conn_w,
+ 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex IN (%Ls)',
+ $table->getTableName(),
+ $task->getPHID(),
+ array_keys($remove));
+ }
- foreach ($list->getFields() as $field) {
- if ($task->getID()) {
- $key = $field->getAuxiliaryKey();
- $field->setValueFromStorage($task->getAuxiliaryAttribute($key));
+ if ($update) {
+ $sql = array();
+ foreach ($update as $index => $val) {
+ $sql[] = qsprintf(
+ $conn_w,
+ '(%s, %s, %s)',
+ $task->getPHID(),
+ $index,
+ $val);
}
+ queryfx(
+ $conn_w,
+ 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
+ VALUES %Q ON DUPLICATE KEY
+ UPDATE fieldValue = VALUES(fieldValue)',
+ $table->getTableName(),
+ implode(', ', $sql));
}
+
}
}
diff --git a/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php b/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php
index ed719f165f..648506e83b 100644
--- a/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php
+++ b/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php
@@ -1,270 +1,291 @@
<?php
/**
* @group conduit
*/
abstract class ConduitAPI_maniphest_Method extends ConduitAPIMethod {
public function getApplication() {
return PhabricatorApplication::getByClass(
'PhabricatorApplicationManiphest');
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.'
);
}
protected function buildTaskInfoDictionary(ManiphestTask $task) {
$results = $this->buildTaskInfoDictionaries(array($task));
return idx($results, $task->getPHID());
}
protected function getTaskFields($is_new) {
$fields = array();
if (!$is_new) {
$fields += array(
'id' => 'optional int',
'phid' => 'optional int',
);
}
$fields += array(
'title' => $is_new ? 'required string' : 'optional string',
'description' => 'optional string',
'ownerPHID' => 'optional phid',
'ccPHIDs' => 'optional list<phid>',
'priority' => 'optional int',
'projectPHIDs' => 'optional list<phid>',
'filePHIDs' => 'optional list<phid>',
'auxiliary' => 'optional dict',
);
if (!$is_new) {
$fields += array(
'status' => 'optional int',
'comments' => 'optional string',
);
}
return $fields;
}
protected function applyRequest(
ManiphestTask $task,
ConduitAPIRequest $request,
$is_new) {
$changes = array();
if ($is_new) {
$task->setTitle((string)$request->getValue('title'));
$task->setDescription((string)$request->getValue('description'));
$changes[ManiphestTransactionType::TYPE_STATUS] =
ManiphestTaskStatus::STATUS_OPEN;
} else {
$comments = $request->getValue('comments');
if (!$is_new && $comments !== null) {
$changes[ManiphestTransactionType::TYPE_NONE] = null;
}
$title = $request->getValue('title');
if ($title !== null) {
$changes[ManiphestTransactionType::TYPE_TITLE] = $title;
}
$desc = $request->getValue('description');
if ($desc !== null) {
$changes[ManiphestTransactionType::TYPE_DESCRIPTION] = $desc;
}
$status = $request->getValue('status');
if ($status !== null) {
$valid_statuses = ManiphestTaskStatus::getTaskStatusMap();
if (!isset($valid_statuses[$status])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription('Status set to invalid value.');
}
$changes[ManiphestTransactionType::TYPE_STATUS] = $status;
}
}
$priority = $request->getValue('priority');
if ($priority !== null) {
$valid_priorities = ManiphestTaskPriority::getTaskPriorityMap();
if (!isset($valid_priorities[$priority])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription('Priority set to invalid value.');
}
$changes[ManiphestTransactionType::TYPE_PRIORITY] = $priority;
}
$owner_phid = $request->getValue('ownerPHID');
if ($owner_phid !== null) {
$this->validatePHIDList(array($owner_phid),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
'ownerPHID');
$changes[ManiphestTransactionType::TYPE_OWNER] = $owner_phid;
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs !== null) {
$this->validatePHIDList($ccs,
PhabricatorPeoplePHIDTypeUser::TYPECONST,
'ccPHIDS');
$changes[ManiphestTransactionType::TYPE_CCS] = $ccs;
}
$project_phids = $request->getValue('projectPHIDs');
if ($project_phids !== null) {
$this->validatePHIDList($project_phids,
PhabricatorProjectPHIDTypeProject::TYPECONST,
'projectPHIDS');
$changes[ManiphestTransactionType::TYPE_PROJECTS] = $project_phids;
}
$file_phids = $request->getValue('filePHIDs');
if ($file_phids !== null) {
$this->validatePHIDList($file_phids,
PhabricatorFilePHIDTypeFile::TYPECONST,
'filePHIDS');
$file_map = array_fill_keys($file_phids, true);
$attached = $task->getAttached();
$attached[PhabricatorFilePHIDTypeFile::TYPECONST] = $file_map;
$changes[ManiphestTransactionType::TYPE_ATTACH] = $attached;
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$template = new ManiphestTransaction();
$template->setContentSource($content_source);
$template->setAuthorPHID($request->getUser()->getPHID());
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
if ($type == ManiphestTransactionType::TYPE_NONE) {
$transaction->setComments($comments);
}
$transactions[] = $transaction;
}
+ $field_list = PhabricatorCustomField::getObjectFields(
+ $task,
+ PhabricatorCustomField::ROLE_EDIT);
+
$auxiliary = $request->getValue('auxiliary');
if ($auxiliary) {
- $task->loadAndAttachAuxiliaryAttributes();
- foreach ($auxiliary as $aux_key => $aux_value) {
+ foreach ($field_list->getFields() as $key => $field) {
+ if (!array_key_exists($key, $auxiliary)) {
+ continue;
+ }
$transaction = clone $template;
$transaction->setTransactionType(
ManiphestTransactionType::TYPE_AUXILIARY);
- $transaction->setMetadataValue('aux:key', $aux_key);
- $transaction->setNewValue($aux_value);
+ $transaction->setMetadataValue('aux:key', $key);
+ $transaction->setOldValue(
+ $field->getOldValueForApplicationTransactions());
+ $transaction->setNewValue($auxiliary[$key]);
$transactions[] = $transaction;
}
}
+ if (!$transactions) {
+ return;
+ }
+
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = new ManiphestTransactionEditor();
$editor->setActor($request->getUser());
+ $editor->setAuxiliaryFields($field_list->getFields());
$editor->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
protected function buildTaskInfoDictionaries(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
if (!$tasks) {
return array();
}
$task_phids = mpull($tasks, 'getPHID');
$all_deps = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($task_phids)
->withEdgeTypes(array(PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK));
$all_deps->execute();
$result = array();
foreach ($tasks as $task) {
// TODO: Batch this get as CustomField gets cleaned up.
- $auxiliary = $task->loadLegacyAuxiliaryFieldMap();
+ $field_list = PhabricatorCustomField::getObjectFields(
+ $task,
+ PhabricatorCustomField::ROLE_EDIT);
+ $field_list->readFieldsFromStorage($task);
+
+ $auxiliary = mpull(
+ $field_list->getFields(),
+ 'getValueForStorage',
+ 'getFieldKey');
$task_deps = $all_deps->getDestinationPHIDs(
array($task->getPHID()),
array(PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK));
$result[$task->getPHID()] = array(
'id' => $task->getID(),
'phid' => $task->getPHID(),
'authorPHID' => $task->getAuthorPHID(),
'ownerPHID' => $task->getOwnerPHID(),
'ccPHIDs' => $task->getCCPHIDs(),
'status' => $task->getStatus(),
'priority' => ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority()),
'title' => $task->getTitle(),
'description' => $task->getDescription(),
'projectPHIDs' => $task->getProjectPHIDs(),
'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()),
'auxiliary' => $auxiliary,
'objectName' => 'T'.$task->getID(),
'dateCreated' => $task->getDateCreated(),
'dateModified' => $task->getDateModified(),
'dependsOnTaskPHIDs' => $task_deps,
);
}
return $result;
}
/**
* Note this is a temporary stop gap since its easy to make malformed Tasks.
* Long-term, the values set in @{method:defineParamTypes} will be used to
* validate data implicitly within the larger Conduit application.
*
* TODO -- remove this in favor of generalized Conduit hotness
*/
private function validatePHIDList(array $phid_list, $phid_type, $field) {
$phid_groups = phid_group_by_type($phid_list);
unset($phid_groups[$phid_type]);
if (!empty($phid_groups)) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
'One or more PHIDs were invalid for '.$field.'.');
}
return true;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index 388720770c..bf10fcf63b 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,583 +1,601 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTaskEditController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$files = array();
$parent_task = null;
$template_id = null;
if ($this->id) {
$task = id(new ManiphestTask())->load($this->id);
if (!$task) {
return new Aphront404Response();
}
} else {
$task = new ManiphestTask();
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
$task->setAuthorPHID($user->getPHID());
// These allow task creation with defaults.
if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title'));
$default_projects = $request->getStr('projects');
if ($default_projects) {
$task->setProjectPHIDs(explode(';', $default_projects));
}
$task->setDescription($request->getStr('description'));
$assign = $request->getStr('assign');
if (strlen($assign)) {
$assign_user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$assign);
if ($assign_user) {
$task->setOwnerPHID($assign_user->getPHID());
}
}
}
$file_phids = $request->getArr('files', array());
if (!$file_phids) {
// Allow a single 'file' key instead, mostly since Mac OS X urlencodes
// square brackets in URLs when passed to 'open', so you can't 'open'
// a URL like '?files[]=xyz' and have PHP interpret it correctly.
$phid = $request->getStr('file');
if ($phid) {
$file_phids = array($phid);
}
}
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
}
$template_id = $request->getInt('template');
// You can only have a parent task if you're creating a new task.
$parent_id = $request->getInt('parent');
if ($parent_id) {
$parent_task = id(new ManiphestTask())->load($parent_id);
if (!$template_id) {
$template_id = $parent_id;
}
}
}
$errors = array();
$e_title = true;
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
foreach ($field_list->getFields() as $field) {
$field->setObject($task);
$field->setViewer($user);
}
- ManiphestAuxiliaryFieldSpecification::loadLegacyDataFromStorage(
- $task,
- $field_list);
+ $field_list->readFieldsFromStorage($task);
$aux_fields = $field_list->getFields();
if ($request->isFormPost()) {
$changes = array();
$new_title = $request->getStr('title');
$new_desc = $request->getStr('description');
$new_status = $request->getStr('status');
$workflow = '';
if ($task->getID()) {
if ($new_title != $task->getTitle()) {
$changes[ManiphestTransactionType::TYPE_TITLE] = $new_title;
}
if ($new_desc != $task->getDescription()) {
$changes[ManiphestTransactionType::TYPE_DESCRIPTION] = $new_desc;
}
if ($new_status != $task->getStatus()) {
$changes[ManiphestTransactionType::TYPE_STATUS] = $new_status;
}
} else {
$task->setTitle($new_title);
$task->setDescription($new_desc);
$changes[ManiphestTransactionType::TYPE_STATUS] =
ManiphestTaskStatus::STATUS_OPEN;
$workflow = 'create';
}
$owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer);
if (!strlen($new_title)) {
$e_title = pht('Required');
$errors[] = pht('Title is required.');
}
+ $old_values = array();
foreach ($aux_fields as $aux_arr_key => $aux_field) {
- $aux_field->setValueFromRequest($request);
- $aux_key = $aux_field->getAuxiliaryKey();
- $aux_old_value = $task->getAuxiliaryAttribute($aux_key);
+ // TODO: This should be buildFieldTransactionsFromRequest() once we
+ // switch to ApplicationTransactions properly.
+
+ $aux_old_value = $aux_field->getOldValueForApplicationTransactions();
+ $aux_field->readValueFromRequest($request);
+ $aux_new_value = $aux_field->getNewValueForApplicationTransactions();
- if ((int)$aux_old_value === $aux_field->getValueForStorage()) {
+ // TODO: What's going on here?
+ if ((int)$aux_old_value === $aux_new_value) {
unset($aux_fields[$aux_arr_key]);
continue;
}
if ($aux_field->isRequired() && !$aux_field->getValue()) {
$errors[] = pht('%s is required.', $aux_field->getLabel());
$aux_field->setError(pht('Required'));
}
try {
$aux_field->validate();
} catch (Exception $e) {
$errors[] = $e->getMessage();
$aux_field->setError(pht('Invalid'));
}
+
+ $old_values[$aux_field->getFieldKey()] = $aux_old_value;
}
if ($errors) {
$task->setPriority($request->getInt('priority'));
$task->setOwnerPHID($owner_phid);
$task->setCCPHIDs($request->getArr('cc'));
$task->setProjectPHIDs($request->getArr('projects'));
} else {
if ($request->getInt('priority') != $task->getPriority()) {
$changes[ManiphestTransactionType::TYPE_PRIORITY] =
$request->getInt('priority');
}
if ($owner_phid != $task->getOwnerPHID()) {
$changes[ManiphestTransactionType::TYPE_OWNER] = $owner_phid;
}
if ($request->getArr('cc') != $task->getCCPHIDs()) {
$changes[ManiphestTransactionType::TYPE_CCS] = $request->getArr('cc');
}
$new_proj_arr = $request->getArr('projects');
$new_proj_arr = array_values($new_proj_arr);
sort($new_proj_arr);
$cur_proj_arr = $task->getProjectPHIDs();
$cur_proj_arr = array_values($cur_proj_arr);
sort($cur_proj_arr);
if ($new_proj_arr != $cur_proj_arr) {
$changes[ManiphestTransactionType::TYPE_PROJECTS] = $new_proj_arr;
}
if ($files) {
$file_map = mpull($files, 'getPHID');
$file_map = array_fill_keys($file_map, array());
$changes[ManiphestTransactionType::TYPE_ATTACH] = array(
PhabricatorFilePHIDTypeFile::TYPECONST => $file_map,
);
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
$template = new ManiphestTransaction();
$template->setAuthorPHID($user->getPHID());
$template->setContentSource($content_source);
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
if ($aux_fields) {
foreach ($aux_fields as $aux_field) {
$transaction = clone $template;
$transaction->setTransactionType(
ManiphestTransactionType::TYPE_AUXILIARY);
- $aux_key = $aux_field->getAuxiliaryKey();
+ $aux_key = $aux_field->getFieldKey();
$transaction->setMetadataValue('aux:key', $aux_key);
- $transaction->setNewValue($aux_field->getValueForStorage());
+ $transaction->setOldValue(idx($old_values, $aux_key));
+ $transaction->setNewValue(
+ $aux_field->getNewValueForApplicationTransactions());
$transactions[] = $transaction;
}
}
if ($transactions) {
$is_new = !$task->getID();
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$editor->setAuxiliaryFields($aux_fields);
$editor->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
if ($parent_task) {
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$parent_task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$task->getPHID())
->save();
$workflow = $parent_task->getID();
}
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent(
array(
'tasks' => $this->renderSingleTask($task),
));
}
$redirect_uri = '/T'.$task->getID();
if ($workflow) {
$redirect_uri .= '?workflow='.$workflow;
}
return id(new AphrontRedirectResponse())
->setURI($redirect_uri);
}
} else {
if (!$task->getID()) {
$task->setCCPHIDs(array(
$user->getPHID(),
));
if ($template_id) {
$template_task = id(new ManiphestTask())->load($template_id);
if ($template_task) {
$task->setCCPHIDs($template_task->getCCPHIDs());
$task->setProjectPHIDs($template_task->getProjectPHIDs());
$task->setOwnerPHID($template_task->getOwnerPHID());
$task->setPriority($template_task->getPriority());
- if ($aux_fields) {
- $template_task->loadAndAttachAuxiliaryAttributes();
- foreach ($aux_fields as $aux_field) {
- if (!$aux_field->shouldCopyWhenCreatingSimilarTask()) {
- continue;
- }
+ $template_fields = PhabricatorCustomField::getObjectFields(
+ $template_task,
+ PhabricatorCustomField::ROLE_EDIT);
+
+ $fields = $template_fields->getFields();
+ foreach ($fields as $key => $field) {
+ if (!$field->shouldCopyWhenCreatingSimilarTask()) {
+ unset($fields[$key]);
+ }
+ if (empty($aux_fields[$key])) {
+ unset($fields[$key]);
+ }
+ }
+
+ if ($fields) {
+ id(new PhabricatorCustomFieldList($fields))
+ ->readFieldsFromStorage($template_task);
- $aux_key = $aux_field->getAuxiliaryKey();
- $value = $template_task->getAuxiliaryAttribute($aux_key);
- $aux_field->setValueFromStorage($value);
+ foreach ($fields as $key => $field) {
+ $aux_fields[$key]->setValueFromStorage(
+ $field->getValueForStorage());
}
}
}
}
}
}
$phids = array_merge(
array($task->getOwnerPHID()),
$task->getCCPHIDs(),
$task->getProjectPHIDs(),
array_mergev(mpull($aux_fields, 'getRequiredHandlePHIDs')));
if ($parent_task) {
$phids[] = $parent_task->getPHID();
}
$phids = array_filter($phids);
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
foreach ($aux_fields as $aux_field) {
$aux_field->setHandles($handles);
}
$tvalues = mpull($handles, 'getFullName', 'getPHID');
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle(pht('Form Errors'));
}
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($task->getOwnerPHID()) {
$assigned_value = array(
$task->getOwnerPHID() => $handles[$task->getOwnerPHID()]->getFullName(),
);
} else {
$assigned_value = array();
}
if ($task->getCCPHIDs()) {
$cc_value = array_select_keys($tvalues, $task->getCCPHIDs());
} else {
$cc_value = array();
}
if ($task->getProjectPHIDs()) {
$projects_value = array_select_keys($tvalues, $task->getProjectPHIDs());
} else {
$projects_value = array();
}
$cancel_id = nonempty($task->getID(), $template_id);
if ($cancel_id) {
$cancel_uri = '/T'.$cancel_id;
} else {
$cancel_uri = '/maniphest/';
}
if ($task->getID()) {
$button_name = pht('Save Task');
$header_name = pht('Edit Task');
} else if ($parent_task) {
$cancel_uri = '/T'.$parent_task->getID();
$button_name = pht('Create Task');
$header_name = pht('Create New Subtask');
} else {
$button_name = pht('Create Task');
$header_name = pht('Create New Task');
}
require_celerity_resource('maniphest-task-edit-css');
$project_tokenizer_id = celerity_generate_unique_node_id();
if ($request->isAjax()) {
$form = new PHUIFormLayoutView();
} else {
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('template', $template_id);
}
if ($parent_task) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Parent Task'))
->setValue($handles[$parent_task->getPHID()]->getFullName()))
->addHiddenInput('parent', $parent_task->getID());
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Title'))
->setName('title')
->setError($e_title)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle()));
if ($task->getID()) {
// Only show this in "edit" mode, not "create" mode, since creating a
// non-open task is kind of silly and it would just clutter up the
// "create" interface.
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setValue($task->getStatus())
->setOptions(ManiphestTaskStatus::getTaskStatusMap()));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assigned To'))
->setName('assigned_to')
->setValue($assigned_value)
->setUser($user)
->setDatasource('/typeahead/common/users/')
->setLimit(1))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CC'))
->setName('cc')
->setValue($cc_value)
->setUser($user)
->setDatasource('/typeahead/common/mailable/'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption(
javelin_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
pht('Create New Project')))
->setDatasource('/typeahead/common/projects/'));
foreach ($aux_fields as $aux_field) {
if ($aux_field->isRequired() &&
!$aux_field->getError() &&
!$aux_field->getValue()) {
$aux_field->setError(true);
}
$aux_control = $aux_field->renderControl();
$form->appendChild($aux_control);
}
require_celerity_resource('aphront-error-view-css');
Javelin::initBehavior('project-create', array(
'tokenizerID' => $project_tokenizer_id,
));
if ($files) {
$file_display = mpull($files, 'getName');
$file_display = phutil_implode_html(phutil_tag('br'), $file_display);
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Files'))
->setValue($file_display));
foreach ($files as $ii => $file) {
$form->addHiddenInput('files['.$ii.']', $file->getPHID());
}
}
$description_control = new PhabricatorRemarkupControl();
// "Upsell" creating tasks via email in create flows if the instance is
// configured for this awesomeness.
$email_create = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
if (!$task->getID() && $email_create) {
$email_hint = pht(
'You can also create tasks by sending an email to: %s',
phutil_tag('tt', array(), $email_create));
$description_control->setCaption($email_hint);
}
$description_control
->setLabel(pht('Description'))
->setName('description')
->setID('description-textarea')
->setValue($task->getDescription())
->setUser($user);
$form
->appendChild($description_control);
if ($request->isAjax()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_name)
->appendChild(
array(
$error_view,
$form,
))
->addCancelButton($cancel_uri)
->addSubmitButton($button_name);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button_name));
$form_box = id(new PHUIFormBoxView())
->setHeaderText($header_name)
->setFormError($error_view)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview'))
->setControlID('description-textarea')
->setPreviewURI($this->getApplicationURI('task/descriptionpreview/'));
if ($task->getID()) {
$page_objects = array( $task->getPHID() );
} else {
$page_objects = array();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($header_name));
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
$preview,
),
array(
'title' => $header_name,
'pageObjects' => $page_objects,
'device' => true,
));
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index bde7aaf22d..b5bf62f196 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,478 +1,487 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTransactionEditor extends PhabricatorEditor {
private $parentMessageID;
private $auxiliaryFields = array();
public function setAuxiliaryFields(array $fields) {
- assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
+ assert_instances_of($fields, 'ManiphestCustomField');
$this->auxiliaryFields = $fields;
return $this;
}
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function applyTransactions(ManiphestTask $task, array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$email_cc = $task->getCCPHIDs();
$email_to = array();
$email_to[] = $task->getOwnerPHID();
$pri_changed = $this->isCreate($transactions);
+ $aux_writes = array();
foreach ($transactions as $key => $transaction) {
$type = $transaction->getTransactionType();
$new = $transaction->getNewValue();
$email_to[] = $transaction->getAuthorPHID();
$value_is_phid_set = false;
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$old = null;
break;
case ManiphestTransactionType::TYPE_STATUS:
$old = $task->getStatus();
break;
case ManiphestTransactionType::TYPE_OWNER:
$old = $task->getOwnerPHID();
break;
case ManiphestTransactionType::TYPE_CCS:
$old = $task->getCCPHIDs();
$value_is_phid_set = true;
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$old = $task->getPriority();
break;
case ManiphestTransactionType::TYPE_EDGE:
$old = $transaction->getOldValue();
break;
case ManiphestTransactionType::TYPE_ATTACH:
$old = $task->getAttached();
break;
case ManiphestTransactionType::TYPE_TITLE:
$old = $task->getTitle();
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$old = $task->getDescription();
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$old = $task->getProjectPHIDs();
$value_is_phid_set = true;
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
if (!$aux_key) {
throw new Exception(
"Expected 'aux:key' metadata on TYPE_AUXILIARY transaction.");
}
- $old = $task->getAuxiliaryAttribute($aux_key);
+ // This has already been populated.
+ $old = $transaction->getOldValue();
break;
default:
throw new Exception('Unknown action type.');
}
$old_cmp = $old;
$new_cmp = $new;
if ($value_is_phid_set) {
// Normalize the old and new values if they are PHID sets so we don't
// get any no-op transactions where the values differ only by keys,
// order, duplicates, etc.
if (is_array($old)) {
$old = array_filter($old);
$old = array_unique($old);
sort($old);
$old = array_values($old);
$old_cmp = $old;
}
if (is_array($new)) {
$new = array_filter($new);
$new = array_unique($new);
$transaction->setNewValue($new);
$new_cmp = $new;
sort($new_cmp);
$new_cmp = array_values($new_cmp);
}
}
if (($old !== null) && ($old_cmp == $new_cmp)) {
if (count($transactions) > 1 && !$transaction->hasComments()) {
// If we have at least one other transaction and this one isn't
// doing anything and doesn't have any comments, just throw it
// away.
unset($transactions[$key]);
continue;
} else {
$transaction->setOldValue(null);
$transaction->setNewValue(null);
$transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE);
}
} else {
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
break;
case ManiphestTransactionType::TYPE_STATUS:
$task->setStatus($new);
break;
case ManiphestTransactionType::TYPE_OWNER:
if ($new) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($new))
->executeOne();
$task->setOwnerOrdering($handle->getName());
} else {
$task->setOwnerOrdering(null);
}
$task->setOwnerPHID($new);
break;
case ManiphestTransactionType::TYPE_CCS:
$task->setCCPHIDs($new);
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$task->setPriority($new);
$pri_changed = true;
break;
case ManiphestTransactionType::TYPE_ATTACH:
$task->setAttached($new);
break;
case ManiphestTransactionType::TYPE_TITLE:
$task->setTitle($new);
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$task->setDescription($new);
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$task->setProjectPHIDs($new);
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
- $task->setAuxiliaryAttribute($aux_key, $new);
+ $aux_writes[$aux_key] = $new;
break;
case ManiphestTransactionType::TYPE_EDGE:
// Edge edits are accomplished through PhabricatorEdgeEditor, which
// has authority.
break;
default:
throw new Exception('Unknown action type.');
}
$transaction->setOldValue($old);
$transaction->setNewValue($new);
}
}
if ($pri_changed) {
$subpriority = ManiphestTransactionEditor::getNextSubpriority(
$task->getPriority(),
null);
$task->setSubpriority($subpriority);
}
$task->save();
+
+ if ($aux_writes) {
+ ManiphestAuxiliaryFieldSpecification::writeLegacyAuxiliaryUpdates(
+ $task,
+ $aux_writes);
+ }
+
foreach ($transactions as $transaction) {
$transaction->setTaskID($task->getID());
$transaction->save();
}
$email_to[] = $task->getOwnerPHID();
$email_cc = array_merge(
$email_cc,
$task->getCCPHIDs());
$mail = $this->sendEmail($task, $transactions, $email_to, $email_cc);
$this->publishFeedStory(
$task,
$transactions,
$mail->buildRecipientList());
id(new PhabricatorSearchIndexer())
->indexDocumentByPHID($task->getPHID());
}
protected function getSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
private function sendEmail($task, $transactions, $email_to, $email_cc) {
$email_to = array_filter(array_unique($email_to));
$email_cc = array_filter(array_unique($email_cc));
$phids = array();
foreach ($transactions as $transaction) {
foreach ($transaction->extractPHIDs() as $phid) {
$phids[$phid] = true;
}
}
foreach ($email_to as $phid) {
$phids[$phid] = true;
}
foreach ($email_cc as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
$view = new ManiphestTransactionDetailView();
$view->setTransactionGroup($transactions);
$view->setHandles($handles);
$view->setAuxiliaryFields($this->auxiliaryFields);
list($action, $main_body) = $view->renderForEmail($with_date = false);
$is_create = $this->isCreate($transactions);
$task_uri = PhabricatorEnv::getProductionURI('/T'.$task->getID());
$reply_handler = $this->buildReplyHandler($task);
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($main_body);
if ($is_create) {
$body->addTextSection(pht('TASK DESCRIPTION'), $task->getDescription());
}
$body->addTextSection(pht('TASK DETAIL'), $task_uri);
$body->addReplySection($reply_handler->getReplyHandlerInstructions());
$thread_id = 'maniphest-task-'.$task->getPHID();
$task_id = $task->getID();
$title = $task->getTitle();
$mailtags = $this->getMailTags($transactions);
$template = id(new PhabricatorMetaMTAMail())
->setSubject("T{$task_id}: {$title}")
->setSubjectPrefix($this->getSubjectPrefix())
->setVarySubjectPrefix("[{$action}]")
->setFrom($transaction->getAuthorPHID())
->setParentMessageID($this->parentMessageID)
->addHeader('Thread-Topic', "T{$task_id}: ".$task->getOriginalTitle())
->setThreadID($thread_id, $is_create)
->setRelatedPHID($task->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setIsBulk(true)
->setMailTags($mailtags)
->setBody($body->render());
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
$template->addTos($email_to);
$template->addCCs($email_cc);
return $template;
}
public function buildReplyHandler(ManiphestTask $task) {
$handler_object = PhabricatorEnv::newObjectFromConfig(
'metamta.maniphest.reply-handler');
$handler_object->setMailReceiver($task);
return $handler_object;
}
private function publishFeedStory(
ManiphestTask $task,
array $transactions,
array $mailed_phids) {
assert_instances_of($transactions, 'ManiphestTransaction');
$actions = array(ManiphestAction::ACTION_UPDATE);
$comments = null;
foreach ($transactions as $transaction) {
if ($transaction->hasComments()) {
$comments = $transaction->getComments();
}
$type = $transaction->getTransactionType();
switch ($type) {
case ManiphestTransactionType::TYPE_OWNER:
$actions[] = ManiphestAction::ACTION_ASSIGN;
break;
case ManiphestTransactionType::TYPE_STATUS:
if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
$actions[] = ManiphestAction::ACTION_CLOSE;
} else if ($this->isCreate($transactions)) {
$actions[] = ManiphestAction::ACTION_CREATE;
} else {
$actions[] = ManiphestAction::ACTION_REOPEN;
}
break;
default:
$actions[] = $type;
break;
}
}
$action_type = ManiphestAction::selectStrongestAction($actions);
$owner_phid = $task->getOwnerPHID();
$actor_phid = head($transactions)->getAuthorPHID();
$author_phid = $task->getAuthorPHID();
id(new PhabricatorFeedStoryPublisher())
->setStoryType('PhabricatorFeedStoryManiphest')
->setStoryData(array(
'taskPHID' => $task->getPHID(),
'transactionIDs' => mpull($transactions, 'getID'),
'ownerPHID' => $owner_phid,
'action' => $action_type,
'comments' => $comments,
'description' => $task->getDescription(),
))
->setStoryTime(time())
->setStoryAuthorPHID($actor_phid)
->setRelatedPHIDs(
array_merge(
array_filter(
array(
$task->getPHID(),
$author_phid,
$actor_phid,
$owner_phid,
)),
$task->getProjectPHIDs()))
->setPrimaryObjectPHID($task->getPHID())
->setSubscribedPHIDs(
array_merge(
array_filter(
array(
$author_phid,
$owner_phid,
$actor_phid)),
$task->getCCPHIDs()))
->setMailRecipientPHIDs($mailed_phids)
->publish();
}
private function isCreate(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$is_create = false;
foreach ($transactions as $transaction) {
$type = $transaction->getTransactionType();
if (($type == ManiphestTransactionType::TYPE_STATUS) &&
($transaction->getOldValue() === null) &&
($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) {
$is_create = true;
}
}
return $is_create;
}
private function getMailTags(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$tags = array();
foreach ($transactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransactionType::TYPE_STATUS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS;
break;
case ManiphestTransactionType::TYPE_OWNER:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER;
break;
case ManiphestTransactionType::TYPE_CCS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC;
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS;
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY;
break;
case ManiphestTransactionType::TYPE_NONE:
// this is a comment which we will check separately below for
// content
break;
default:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER;
break;
}
if ($xaction->hasComments()) {
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT;
}
}
return array_unique($tags);
}
public static function getNextSubpriority($pri, $sub) {
if ($sub === null) {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d ORDER BY subpriority ASC LIMIT 1',
$pri);
if ($next) {
return $next->getSubpriority() - ((double)(2 << 16));
}
} else {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1',
$pri,
$sub);
if ($next) {
return ($sub + $next->getSubpriority()) / 2;
}
}
return (double)(2 << 32);
}
public static function addCC(
ManiphestTask $task,
PhabricatorUser $user) {
$current_ccs = $task->getCCPHIDs();
$new_ccs = array_merge($current_ccs, array($user->getPHID()));
$transaction = new ManiphestTransaction();
$transaction->setTaskID($task->getID());
$transaction->setAuthorPHID($user->getPHID());
$transaction->setTransactionType(ManiphestTransactionType::TYPE_CCS);
$transaction->setNewValue(array_unique($new_ccs));
$transaction->setOldValue($current_ccs);
id(new ManiphestTransactionEditor())
->setActor($user)
->applyTransactions($task, array($transaction));
}
public static function removeCC(
ManiphestTask $task,
PhabricatorUser $user) {
$current_ccs = $task->getCCPHIDs();
$new_ccs = array_diff($current_ccs, array($user->getPHID()));
$transaction = new ManiphestTransaction();
$transaction->setTaskID($task->getID());
$transaction->setAuthorPHID($user->getPHID());
$transaction->setTransactionType(ManiphestTransactionType::TYPE_CCS);
$transaction->setNewValue(array_unique($new_ccs));
$transaction->setOldValue($current_ccs);
id(new ManiphestTransactionEditor())
->setActor($user)
->applyTransactions($task, array($transaction));
}
}
diff --git a/src/applications/maniphest/field/ManiphestCustomField.php b/src/applications/maniphest/field/ManiphestCustomField.php
index 26c18cbb8c..6fd679f0a5 100644
--- a/src/applications/maniphest/field/ManiphestCustomField.php
+++ b/src/applications/maniphest/field/ManiphestCustomField.php
@@ -1,18 +1,32 @@
<?php
abstract class ManiphestCustomField
extends PhabricatorCustomField {
public function newStorageObject() {
return new ManiphestCustomFieldStorage();
}
protected function newStringIndexStorage() {
return new ManiphestCustomFieldStringIndex();
}
protected function newNumericIndexStorage() {
return new ManiphestCustomFieldNumericIndex();
}
+ /**
+ * When the user creates a task, the UI prompts them to "Create another
+ * similar task". This copies some fields (e.g., Owner and CCs) but not other
+ * fields (e.g., description). If this custom field should also be copied,
+ * return true from this method.
+ *
+ * @return bool True to copy the default value from the template task when
+ * creating a new similar task.
+ */
+ public function shouldCopyWhenCreatingSimilarTask() {
+ return false;
+ }
+
+
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index 661f41aeac..312fe5cc52 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,341 +1,241 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhrequentTrackableInterface,
PhabricatorCustomFieldInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $phid;
protected $authorPHID;
protected $ownerPHID;
protected $ccPHIDs = array();
protected $status;
protected $priority;
protected $subpriority;
protected $title;
protected $originalTitle;
protected $description;
protected $originalEmailSource;
protected $mailKey;
protected $attached = array();
protected $projectPHIDs = array();
private $projectsNeedUpdate;
private $subscribersNeedUpdate;
protected $ownerOrdering;
- private $auxiliaryAttributes = self::ATTACHABLE;
- private $auxiliaryDirty = array();
private $groupByProjectPHID = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'ccPHIDs' => self::SERIALIZATION_JSON,
'attached' => self::SERIALIZATION_JSON,
'projectPHIDs' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestPHIDTypeTask::TYPECONST);
}
public function getCCPHIDs() {
return array_values(nonempty($this->ccPHIDs, array()));
}
public function setProjectPHIDs(array $phids) {
$this->projectPHIDs = array_values($phids);
$this->projectsNeedUpdate = true;
return $this;
}
public function getProjectPHIDs() {
return array_values(nonempty($this->projectPHIDs, array()));
}
public function setCCPHIDs(array $phids) {
$this->ccPHIDs = array_values($phids);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
$this->subscribersNeedUpdate = true;
return $this;
}
- public function getAuxiliaryAttribute($key, $default = null) {
- $this->assertAttached($this->auxiliaryAttributes);
- return idx($this->auxiliaryAttributes, $key, $default);
- }
-
- public function setAuxiliaryAttribute($key, $val) {
- $this->assertAttached($this->auxiliaryAttributes);
-
- $this->auxiliaryAttributes[$key] = $val;
- $this->auxiliaryDirty[$key] = true;
- return $this;
- }
-
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function attachGroupByProjectPHID($phid) {
$this->groupByProjectPHID = $phid;
return $this;
}
public function getGroupByProjectPHID() {
return $this->assertAttached($this->groupByProjectPHID);
}
- public function attachAuxiliaryAttributes(array $attrs) {
- if ($this->auxiliaryDirty) {
- throw new Exception(
- "This object has dirty attributes, you can not attach new attributes ".
- "without writing or discarding the dirty attributes.");
- }
- $this->auxiliaryAttributes = $attrs;
- return $this;
- }
-
- public function loadLegacyAuxiliaryFieldMap() {
- $field_list = PhabricatorCustomField::getObjectFields(
- $this,
- PhabricatorCustomField::ROLE_EDIT);
- $field_list->readFieldsFromStorage($this);
-
- $map = array();
- foreach ($field_list->getFields() as $field) {
- $map[$field->getFieldKey()] = $field->getValueForStorage();
- }
-
- return $map;
- }
-
- public function loadAndAttachAuxiliaryAttributes() {
- if (!$this->getPHID()) {
- $this->auxiliaryAttributes = array();
- return $this;
- }
-
- $this->auxiliaryAttributes = $this->loadLegacyAuxiliaryFieldMap();
-
- return $this;
- }
-
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
if ($this->projectsNeedUpdate) {
// If we've changed the project PHIDs for this task, update the link
// table.
ManiphestTaskProject::updateTaskProjects($this);
$this->projectsNeedUpdate = false;
}
if ($this->subscribersNeedUpdate) {
// If we've changed the subscriber PHIDs for this task, update the link
// table.
ManiphestTaskSubscriber::updateTaskSubscribers($this);
$this->subscribersNeedUpdate = false;
}
- if ($this->auxiliaryDirty) {
- $this->writeAuxiliaryUpdates();
- $this->auxiliaryDirty = array();
- }
-
return $result;
}
- private function writeAuxiliaryUpdates() {
- $table = new ManiphestCustomFieldStorage();
- $conn_w = $table->establishConnection('w');
- $update = array();
- $remove = array();
-
- foreach ($this->auxiliaryDirty as $key => $dirty) {
- $value = $this->getAuxiliaryAttribute($key);
-
- $index = PhabricatorHash::digestForIndex($key);
- if ($value === null) {
- $remove[$index] = true;
- } else {
- $update[$index] = $value;
- }
- }
-
- if ($remove) {
- queryfx(
- $conn_w,
- 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex IN (%Ls)',
- $table->getTableName(),
- $this->getPHID(),
- array_keys($remove));
- }
-
- if ($update) {
- $sql = array();
- foreach ($update as $index => $val) {
- $sql[] = qsprintf(
- $conn_w,
- '(%s, %s, %s)',
- $this->getPHID(),
- $index,
- $val);
- }
- queryfx(
- $conn_w,
- 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
- VALUES %Q ON DUPLICATE KEY
- UPDATE fieldValue = VALUES(fieldValue)',
- $table->getTableName(),
- implode(', ', $sql));
- }
- }
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "maniphest:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/maniphest/view/ManiphestTransactionDetailView.php b/src/applications/maniphest/view/ManiphestTransactionDetailView.php
index 3b3c81b482..f08bcb1f95 100644
--- a/src/applications/maniphest/view/ManiphestTransactionDetailView.php
+++ b/src/applications/maniphest/view/ManiphestTransactionDetailView.php
@@ -1,859 +1,859 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTransactionDetailView extends ManiphestView {
private $transactions;
private $handles;
private $markupEngine;
private $forEmail;
private $preview;
private $commentNumber;
private $rangeSpecification;
private $renderSummaryOnly;
private $renderFullSummary;
private $auxiliaryFields;
public function setAuxiliaryFields(array $fields) {
- assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
+ assert_instances_of($fields, 'ManiphestCustomField');
$this->auxiliaryFields = mpull($fields, null, 'getAuxiliaryKey');
return $this;
}
public function getAuxiliaryField($key) {
return idx($this->auxiliaryFields, $key);
}
public function setTransactionGroup(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$this->transactions = $transactions;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setRenderSummaryOnly($render_summary_only) {
$this->renderSummaryOnly = $render_summary_only;
return $this;
}
public function getRenderSummaryOnly() {
return $this->renderSummaryOnly;
}
public function setRenderFullSummary($render_full_summary) {
$this->renderFullSummary = $render_full_summary;
return $this;
}
public function getRenderFullSummary() {
return $this->renderFullSummary;
}
public function setCommentNumber($comment_number) {
$this->commentNumber = $comment_number;
return $this;
}
public function setRangeSpecification($range) {
$this->rangeSpecification = $range;
return $this;
}
public function getRangeSpecification() {
return $this->rangeSpecification;
}
public function renderForEmail($with_date) {
$this->forEmail = true;
$transaction = reset($this->transactions);
$author = $this->renderHandles(array($transaction->getAuthorPHID()));
$action = null;
$descs = array();
$comments = null;
foreach ($this->transactions as $transaction) {
list($verb, $desc, $classes) = $this->describeAction($transaction);
if ($desc === null) {
continue;
}
if ($action === null) {
$action = $verb;
}
$desc = $author.' '.$desc.'.';
if ($with_date) {
// NOTE: This is going into a (potentially multi-recipient) email so
// we can't use a single user's timezone preferences. Use the server's
// instead, but make the timezone explicit.
$datetime = date('M jS \a\t g:i A T', $transaction->getDateCreated());
$desc = "On {$datetime}, {$desc}";
}
$descs[] = $desc;
if ($transaction->hasComments()) {
$comments = $transaction->getComments();
}
}
$descs = implode("\n", $descs);
if ($comments) {
$descs .= "\n".$comments;
}
foreach ($this->transactions as $transaction) {
$supplemental = $this->renderSupplementalInfoForEmail($transaction);
if ($supplemental) {
$descs .= "\n\n".$supplemental;
}
}
$this->forEmail = false;
return array($action, $descs);
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before render()!");
}
$handles = $this->handles;
$transactions = $this->transactions;
require_celerity_resource('maniphest-transaction-detail-css');
$comment_transaction = null;
foreach ($this->transactions as $transaction) {
if ($transaction->hasComments()) {
$comment_transaction = $transaction;
break;
}
}
$any_transaction = reset($transactions);
$author = $this->handles[$any_transaction->getAuthorPHID()];
$more_classes = array();
$descs = array();
foreach ($transactions as $transaction) {
list($verb, $desc, $classes) = $this->describeAction($transaction);
if ($desc === null) {
continue;
}
$more_classes = array_merge($more_classes, $classes);
$full_summary = null;
if ($this->getRenderFullSummary()) {
$full_summary = $this->renderFullSummary($transaction);
}
$descs[] = javelin_tag(
'div',
array(
'sigil' => 'maniphest-transaction-description',
),
array(
$author->renderLink(),
' ',
$desc,
'.',
$full_summary,
));
}
if ($this->getRenderSummaryOnly()) {
return phutil_implode_html("\n", $descs);
}
if ($comment_transaction && $comment_transaction->hasComments()) {
$comment_block = $this->markupEngine->getOutput(
$comment_transaction,
ManiphestTransaction::MARKUP_FIELD_BODY);
$comment_block = phutil_tag(
'div',
array('class' => 'maniphest-transaction-comments phabricator-remarkup'),
$comment_block);
} else {
$comment_block = null;
}
$source_transaction = nonempty($comment_transaction, $any_transaction);
$xaction_view = id(new PhabricatorTransactionView())
->setUser($this->user)
->setImageURI($author->getImageURI())
->setContentSource($source_transaction->getContentSource())
->setActions($descs);
foreach ($more_classes as $class) {
$xaction_view->addClass($class);
}
if ($this->preview) {
$xaction_view->setIsPreview($this->preview);
} else {
$xaction_view->setEpoch($any_transaction->getDateCreated());
if ($this->commentNumber) {
$anchor_name = 'comment-'.$this->commentNumber;
$anchor_text =
'T'.$any_transaction->getTaskID().
'#'.$this->commentNumber;
$xaction_view->setAnchor($anchor_name, $anchor_text);
}
}
$xaction_view->appendChild($comment_block);
return $xaction_view->render();
}
private function renderSupplementalInfoForEmail($transaction) {
$handles = $this->handles;
$type = $transaction->getTransactionType();
$new = $transaction->getNewValue();
$old = $transaction->getOldValue();
switch ($type) {
case ManiphestTransactionType::TYPE_DESCRIPTION:
return "NEW DESCRIPTION\n ".trim($new)."\n\n".
"PREVIOUS DESCRIPTION\n ".trim($old);
case ManiphestTransactionType::TYPE_ATTACH:
$old_raw = nonempty($old, array());
$new_raw = nonempty($new, array());
$attach_types = array(
DifferentialPHIDTypeRevision::TYPECONST,
PhabricatorFilePHIDTypeFile::TYPECONST,
);
foreach ($attach_types as $attach_type) {
$old = array_keys(idx($old_raw, $attach_type, array()));
$new = array_keys(idx($new_raw, $attach_type, array()));
if ($old != $new) {
break;
}
}
$added = array_diff($new, $old);
if (!$added) {
break;
}
$links = array();
foreach (array_select_keys($handles, $added) as $handle) {
$links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
$links = implode("\n", $links);
switch ($attach_type) {
case DifferentialPHIDTypeRevision::TYPECONST:
$title = 'ATTACHED REVISIONS';
break;
case PhabricatorFilePHIDTypeFile::TYPECONST:
$title = 'ATTACHED FILES';
break;
}
return $title."\n".$links;
case ManiphestTransactionType::TYPE_EDGE:
$add = array_diff_key($new, $old);
if (!$add) {
break;
}
$links = array();
foreach ($add as $phid => $ignored) {
$handle = $handles[$phid];
$links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
$links = implode("\n", $links);
$edge_type = $transaction->getMetadataValue('edge:type');
$title = $this->getEdgeEmailTitle($edge_type, $add);
return $title."\n".$links;
default:
break;
}
return null;
}
private function describeAction($transaction) {
$verb = null;
$desc = null;
$classes = array();
$handles = $this->handles;
$type = $transaction->getTransactionType();
$author_phid = $transaction->getAuthorPHID();
$new = $transaction->getNewValue();
$old = $transaction->getOldValue();
switch ($type) {
case ManiphestTransactionType::TYPE_TITLE:
$verb = 'Retitled';
$desc = 'changed the title from '.$this->renderString($old).
' to '.$this->renderString($new);
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$verb = 'Edited';
if ($this->forEmail || $this->getRenderFullSummary()) {
$desc = 'updated the task description';
} else {
$desc = 'updated the task description; '.
$this->renderExpandLink($transaction);
}
break;
case ManiphestTransactionType::TYPE_NONE:
$verb = 'Commented On';
$desc = 'added a comment';
break;
case ManiphestTransactionType::TYPE_OWNER:
if ($transaction->getAuthorPHID() == $new) {
$verb = 'Claimed';
$desc = 'claimed this task';
$classes[] = 'claimed';
} else if (!$new) {
$verb = 'Up For Grabs';
$desc = 'placed this task up for grabs';
$classes[] = 'upforgrab';
} else if (!$old) {
$verb = 'Assigned';
$desc = 'assigned this task to '.$this->renderHandles(array($new));
$classes[] = 'assigned';
} else {
$verb = 'Reassigned';
$desc = 'reassigned this task from '.
$this->renderHandles(array($old)).
' to '.
$this->renderHandles(array($new));
$classes[] = 'reassigned';
}
break;
case ManiphestTransactionType::TYPE_CCS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
// can only add in preview so just show placeholder if nothing to add
if ($this->preview && empty($added)) {
$verb = 'Changed CC';
$desc = 'changed CCs..';
break;
}
if ($added && !$removed) {
$verb = 'Added CC';
if (count($added) == 1) {
$desc = 'added '.$this->renderHandles($added).' to CC';
} else {
$desc = 'added CCs: '.$this->renderHandles($added);
}
} else if ($removed && !$added) {
$verb = 'Removed CC';
if (count($removed) == 1) {
$desc = 'removed '.$this->renderHandles($removed).' from CC';
} else {
$desc = 'removed CCs: '.$this->renderHandles($removed);
}
} else {
$verb = 'Changed CC';
$desc = 'changed CCs, added: '.$this->renderHandles($added).'; '.
'removed: '.$this->renderHandles($removed);
}
break;
case ManiphestTransactionType::TYPE_EDGE:
$edge_type = $transaction->getMetadataValue('edge:type');
$add = array_diff_key($new, $old);
$rem = array_diff_key($old, $new);
if ($add && !$rem) {
$verb = $this->getEdgeAddVerb($edge_type);
$desc = $this->getEdgeAddList($edge_type, $add);
} else if ($rem && !$add) {
$verb = $this->getEdgeRemVerb($edge_type);
$desc = $this->getEdgeRemList($edge_type, $rem);
} else {
$verb = $this->getEdgeEditVerb($edge_type);
$desc = $this->getEdgeEditList($edge_type, $add, $rem);
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
// can only add in preview so just show placeholder if nothing to add
if ($this->preview && empty($added)) {
$verb = 'Changed Projects';
$desc = 'changed projects..';
break;
}
if ($added && !$removed) {
$verb = 'Added Project';
if (count($added) == 1) {
$desc = 'added project '.$this->renderHandles($added);
} else {
$desc = 'added projects: '.$this->renderHandles($added);
}
} else if ($removed && !$added) {
$verb = 'Removed Project';
if (count($removed) == 1) {
$desc = 'removed project '.$this->renderHandles($removed);
} else {
$desc = 'removed projects: '.$this->renderHandles($removed);
}
} else {
$verb = 'Changed Projects';
$desc = 'changed projects, added: '.$this->renderHandles($added).'; '.
'removed: '.$this->renderHandles($removed);
}
break;
case ManiphestTransactionType::TYPE_STATUS:
if ($new == ManiphestTaskStatus::STATUS_OPEN) {
if ($old) {
$verb = 'Reopened';
$desc = 'reopened this task';
$classes[] = 'reopened';
} else {
$verb = 'Created';
$desc = 'created this task';
$classes[] = 'created';
}
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_SPITE) {
$verb = 'Spited';
$desc = 'closed this task out of spite';
$classes[] = 'spited';
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) {
$verb = 'Merged';
$desc = 'closed this task as a duplicate';
$classes[] = 'duplicate';
} else {
$verb = 'Closed';
$full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???');
$desc = 'closed this task as "'.$full.'"';
$classes[] = 'closed';
}
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
$verb = 'Triaged';
$desc = 'triaged this task as "'.$new_name.'" priority';
} else if ($old > $new) {
$verb = 'Lowered Priority';
$desc = 'lowered the priority of this task from "'.$old_name.'" to '.
'"'.$new_name.'"';
} else {
$verb = 'Raised Priority';
$desc = 'raised the priority of this task from "'.$old_name.'" to '.
'"'.$new_name.'"';
}
break;
case ManiphestTransactionType::TYPE_ATTACH:
if ($this->preview) {
$verb = 'Changed Attached';
$desc = 'changed attachments..';
break;
}
$old_raw = nonempty($old, array());
$new_raw = nonempty($new, array());
foreach (array(
DifferentialPHIDTypeRevision::TYPECONST,
ManiphestPHIDTypeTask::TYPECONST,
PhabricatorFilePHIDTypeFile::TYPECONST) as $attach_type) {
$old = array_keys(idx($old_raw, $attach_type, array()));
$new = array_keys(idx($new_raw, $attach_type, array()));
if ($old != $new) {
break;
}
}
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
$add_desc = $this->renderHandles($added);
$rem_desc = $this->renderHandles($removed);
if ($added && !$removed) {
$verb = 'Attached';
$desc =
'attached '.
$this->getAttachName($attach_type, count($added)).': '.
$add_desc;
} else if ($removed && !$added) {
$verb = 'Detached';
$desc =
'detached '.
$this->getAttachName($attach_type, count($removed)).': '.
$rem_desc;
} else {
$verb = 'Changed Attached';
$desc =
'changed attached '.
$this->getAttachName($attach_type, count($added) + count($removed)).
', added: '.$add_desc.'; '.
'removed: '.$rem_desc;
}
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
// TODO: Migrate all legacy data when everything migrates for T2217.
$aux_field = $this->getAuxiliaryField($aux_key);
if (!$aux_field) {
$aux_field = $this->getAuxiliaryField('std:maniphest:'.$aux_key);
}
$verb = null;
if ($aux_field) {
$verb = $aux_field->renderTransactionEmailVerb($transaction);
}
if ($verb === null) {
if ($old === null) {
$verb = "Set Field";
} else if ($new === null) {
$verb = "Removed Field";
} else {
$verb = "Updated Field";
}
}
$desc = null;
if ($aux_field) {
$use_field = $aux_field;
} else {
$use_field = id(new ManiphestAuxiliaryFieldDefaultSpecification())
->setFieldType(
ManiphestAuxiliaryFieldDefaultSpecification::TYPE_STRING);
}
$desc = $use_field->renderTransactionDescription(
$transaction,
$this->forEmail
? ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_TEXT
: ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_HTML);
break;
default:
return array($type, ' brazenly '.$type."'d", $classes);
}
// TODO: [HTML] This code will all be rewritten when we switch to using
// ApplicationTransactions. It does not handle HTML or translations
// correctly right now.
$desc = phutil_safe_html($desc);
return array($verb, $desc, $classes);
}
private function renderFullSummary($transaction) {
switch ($transaction->getTransactionType()) {
case ManiphestTransactionType::TYPE_DESCRIPTION:
$id = $transaction->getID();
$old_text = phutil_utf8_hard_wrap($transaction->getOldValue(), 80);
$old_text = implode("\n", $old_text);
$new_text = phutil_utf8_hard_wrap($transaction->getNewValue(), 80);
$new_text = implode("\n", $new_text);
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent($old_text,
$new_text);
$whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
$parser = new DifferentialChangesetParser();
$parser->setChangeset($changeset);
$parser->setRenderingReference($id);
$parser->setMarkupEngine($this->markupEngine);
$parser->setWhitespaceMode($whitespace_mode);
$spec = $this->getRangeSpecification();
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$output = $parser->render($range_s, $range_e, $mask);
return $output;
}
return null;
}
private function renderExpandLink($transaction) {
$id = $transaction->getID();
Javelin::initBehavior('maniphest-transaction-expand');
return javelin_tag(
'a',
array(
'href' => '/maniphest/task/descriptionchange/'.$id.'/',
'sigil' => 'maniphest-expand-transaction',
'mustcapture' => true,
),
'show details');
}
private function renderHandles($phids, $full = false) {
$links = array();
foreach ($phids as $phid) {
if ($this->forEmail) {
if ($full) {
$links[] = $this->handles[$phid]->getFullName();
} else {
$links[] = $this->handles[$phid]->getName();
}
} else {
$links[] = $this->handles[$phid]->renderLink();
}
}
if ($this->forEmail) {
return implode(', ', $links);
} else {
return phutil_implode_html(', ', $links);
}
}
private function renderString($string) {
if ($this->forEmail) {
return '"'.$string.'"';
} else {
return '"'.phutil_escape_html($string).'"';
}
}
/* -( Strings )------------------------------------------------------------ */
/**
* @task strings
*/
private function getAttachName($attach_type, $count) {
switch ($attach_type) {
case DifferentialPHIDTypeRevision::TYPECONST:
return pht('Differential Revision(s)', $count);
case PhabricatorFilePHIDTypeFile::TYPECONST:
return pht('file(s)', $count);
case ManiphestPHIDTypeTask::TYPECONST:
return pht('Maniphest Task(s)', $count);
}
}
/**
* @task strings
*/
private function getEdgeEmailTitle($type, array $list) {
$count = count($list);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('DIFFERENTIAL %d REVISION(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('DEPENDS ON %d TASK(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('DEPENDENT %d TASK(s)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('ATTACHED %d COMMIT(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht('ATTACHED %d MOCK(S)', $count);
default:
return pht('ATTACHED %d OBJECT(S)', $count);
}
}
/**
* @task strings
*/
private function getEdgeAddVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Added Revision');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Added Dependency');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Added Dependent Task');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Added Commit');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht('Added Mock');
default:
return pht('Added Object');
}
}
/**
* @task strings
*/
private function getEdgeRemVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Removed Revision');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Removed Dependency');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Removed Dependent Task');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Removed Commit');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht('Removed Mock');
default:
return pht('Removed Object');
}
}
/**
* @task strings
*/
private function getEdgeEditVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Changed Revisions');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Changed Dependencies');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Changed Dependent Tasks');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Changed Commits');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht('Changed Mocks');
default:
return pht('Changed Objects');
}
}
/**
* @task strings
*/
private function getEdgeAddList($type, array $add) {
$list = $this->renderHandles(array_keys($add), $full = true);
$count = count($add);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('added %d revision(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('added %d dependencie(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('added %d dependent task(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('added %d commit(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht('added %d mock(s): %s', $count, $list);
default:
return pht('added %d object(s): %s', $count, $list);
}
}
/**
* @task strings
*/
private function getEdgeRemList($type, array $rem) {
$list = $this->renderHandles(array_keys($rem), $full = true);
$count = count($rem);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('removed %d revision(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('removed %d dependencie(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('removed %d dependent task(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('removed %d commit(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht('removed %d mock(s): %s', $count, $list);
default:
return pht('removed %d object(s): %s', $count, $list);
}
}
/**
* @task strings
*/
private function getEdgeEditList($type, array $add, array $rem) {
$add_list = $this->renderHandles(array_keys($add), $full = true);
$rem_list = $this->renderHandles(array_keys($rem), $full = true);
$add_count = count($add_list);
$rem_count = count($rem_list);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht(
'changed %d revision(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht(
'changed %d dependencie(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht(
'changed %d dependent task(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht(
'changed %d commit(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
return pht(
'changed %d mock(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
default:
return pht(
'changed %d object(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
}
}
}
diff --git a/src/applications/maniphest/view/ManiphestTransactionListView.php b/src/applications/maniphest/view/ManiphestTransactionListView.php
index 9e8ccc8f08..00830eea95 100644
--- a/src/applications/maniphest/view/ManiphestTransactionListView.php
+++ b/src/applications/maniphest/view/ManiphestTransactionListView.php
@@ -1,111 +1,111 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTransactionListView extends ManiphestView {
private $transactions;
private $handles;
private $markupEngine;
private $preview;
private $auxiliaryFields;
public function setTransactions(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$this->transactions = $transactions;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setAuxiliaryFields(array $fields) {
- assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
+ assert_instances_of($fields, 'ManiphestCustomField');
$this->auxiliaryFields = $fields;
return $this;
}
private function getAuxiliaryFields() {
if (empty($this->auxiliaryFields)) {
return array();
}
return $this->auxiliaryFields;
}
public function render() {
$views = array();
$last = null;
$group = array();
$groups = array();
$has_description_transaction = false;
foreach ($this->transactions as $transaction) {
if ($transaction->getTransactionType() ==
ManiphestTransactionType::TYPE_DESCRIPTION) {
$has_description_transaction = true;
}
if ($last === null) {
$last = $transaction;
$group[] = $transaction;
continue;
} else if ($last->canGroupWith($transaction)) {
$group[] = $transaction;
if ($transaction->hasComments()) {
$last = $transaction;
}
} else {
$groups[] = $group;
$last = $transaction;
$group = array($transaction);
}
}
if ($group) {
$groups[] = $group;
}
if ($has_description_transaction) {
require_celerity_resource('differential-changeset-view-css');
require_celerity_resource('syntax-highlighting-css');
$whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
Javelin::initBehavior('differential-show-more', array(
'uri' => '/maniphest/task/descriptionchange/',
'whitespace' => $whitespace_mode,
));
}
$sequence = 1;
foreach ($groups as $group) {
$view = new ManiphestTransactionDetailView();
$view->setUser($this->user);
$view->setAuxiliaryFields($this->getAuxiliaryFields());
$view->setTransactionGroup($group);
$view->setHandles($this->handles);
$view->setMarkupEngine($this->markupEngine);
$view->setPreview($this->preview);
$view->setCommentNumber($sequence++);
$views[] = $view->render();
}
return phutil_tag(
'div',
array('class' => 'maniphest-transaction-list-view'),
$views);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 13:28 (3 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1125075
Default Alt Text
(95 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment