Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
index 273c0c5472..6c7d26ca12 100644
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -1,322 +1,324 @@
<?php
final class ManiphestEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'maniphest.task';
public function getEngineName() {
return pht('Maniphest Tasks');
}
public function getSummaryHeader() {
return pht('Configure Maniphest Task Forms');
}
public function getSummaryText() {
return pht('Configure how users create and edit tasks.');
}
public function getEngineApplicationClass() {
return 'PhabricatorManiphestApplication';
}
protected function newEditableObject() {
return ManiphestTask::initializeNewTask($this->getViewer());
}
protected function newObjectQuery() {
return id(new ManiphestTaskQuery());
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Task');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Task: %s', $object->getTitle());
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create Task');
}
protected function getObjectName() {
return pht('Task');
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('task/edit/');
}
protected function getCommentViewHeaderText($object) {
return pht('Weigh In');
}
protected function getCommentViewButtonText($object) {
return pht('Set Sail for Adventure');
}
protected function getObjectViewURI($object) {
return '/'.$object->getMonogram();
}
protected function buildCustomEditFields($object) {
$status_map = $this->getTaskStatusMap($object);
$priority_map = $this->getTaskPriorityMap($object);
if ($object->isClosed()) {
$default_status = ManiphestTaskStatus::getDefaultStatus();
} else {
$default_status = ManiphestTaskStatus::getDefaultClosedStatus();
}
if ($object->getOwnerPHID()) {
$owner_value = array($object->getOwnerPHID());
} else {
$owner_value = array($this->getViewer()->getPHID());
}
$fields = array(
id(new PhabricatorHandlesEditField())
->setKey('parent')
->setLabel(pht('Parent Task'))
->setDescription(pht('Task to make this a subtask of.'))
->setConduitDescription(pht('Create as a subtask of another task.'))
->setConduitTypeDescription(pht('PHID of the parent task.'))
->setAliases(array('parentPHID'))
->setTransactionType(ManiphestTransaction::TYPE_PARENT)
->setHandleParameterType(new ManiphestTaskListHTTPParameterType())
->setSingleValue(null)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false),
id(new PhabricatorHandlesEditField())
->setKey('column')
->setLabel(pht('Column'))
- ->setDescription(pht('Workboard column to create this task into.'))
- ->setConduitDescription(pht('Create into a workboard column.'))
- ->setConduitTypeDescription(pht('PHID of workboard column.'))
- ->setAliases(array('columnPHID'))
- ->setTransactionType(ManiphestTransaction::TYPE_COLUMN)
+ ->setDescription(pht('Create a task in a workboard column.'))
+ ->setConduitDescription(
+ pht('Move a task to one or more workboard columns.'))
+ ->setConduitTypeDescription(
+ pht('PHID or PHIDs of workboard columns.'))
+ ->setAliases(array('columnPHID', 'columns', 'columnPHIDs'))
+ ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setSingleValue(null)
->setIsInvisible(true)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false),
id(new PhabricatorTextEditField())
->setKey('title')
->setLabel(pht('Title'))
->setDescription(pht('Name of the task.'))
->setConduitDescription(pht('Rename the task.'))
->setConduitTypeDescription(pht('New task name.'))
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setIsRequired(true)
->setValue($object->getTitle()),
id(new PhabricatorUsersEditField())
->setKey('owner')
->setAliases(array('ownerPHID', 'assign', 'assigned'))
->setLabel(pht('Assigned To'))
->setDescription(pht('User who is responsible for the task.'))
->setConduitDescription(pht('Reassign the task.'))
->setConduitTypeDescription(
pht('New task owner, or `null` to unassign.'))
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setIsCopyable(true)
->setSingleValue($object->getOwnerPHID())
->setCommentActionLabel(pht('Assign / Claim'))
->setCommentActionValue($owner_value),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setDescription(pht('Status of the task.'))
->setConduitDescription(pht('Change the task status.'))
->setConduitTypeDescription(pht('New task status constant.'))
->setTransactionType(ManiphestTransaction::TYPE_STATUS)
->setIsCopyable(true)
->setValue($object->getStatus())
->setOptions($status_map)
->setCommentActionLabel(pht('Change Status'))
->setCommentActionValue($default_status),
id(new PhabricatorSelectEditField())
->setKey('priority')
->setLabel(pht('Priority'))
->setDescription(pht('Priority of the task.'))
->setConduitDescription(pht('Change the priority of the task.'))
->setConduitTypeDescription(pht('New task priority constant.'))
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setIsCopyable(true)
->setValue($object->getPriority())
->setOptions($priority_map)
->setCommentActionLabel(pht('Change Priority')),
);
if (ManiphestTaskPoints::getIsEnabled()) {
$points_label = ManiphestTaskPoints::getPointsLabel();
$action_label = ManiphestTaskPoints::getPointsActionLabel();
$fields[] = id(new PhabricatorPointsEditField())
->setKey('points')
->setLabel($points_label)
->setDescription(pht('Point value of the task.'))
->setConduitDescription(pht('Change the task point value.'))
->setConduitTypeDescription(pht('New task point value.'))
->setTransactionType(ManiphestTransaction::TYPE_POINTS)
->setIsCopyable(true)
->setValue($object->getPoints())
->setCommentActionLabel($action_label);
}
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Task description.'))
->setConduitDescription(pht('Update the task description.'))
->setConduitTypeDescription(pht('New task description.'))
->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION)
->setValue($object->getDescription())
->setPreviewPanel(
id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview')));
return $fields;
}
private function getTaskStatusMap(ManiphestTask $task) {
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$current_status = $task->getStatus();
// If the current status is something we don't recognize (maybe an older
// status which was deleted), put a dummy entry in the status map so that
// saving the form doesn't destroy any data by accident.
if (idx($status_map, $current_status) === null) {
$status_map[$current_status] = pht('<Unknown: %s>', $current_status);
}
$dup_status = ManiphestTaskStatus::getDuplicateStatus();
foreach ($status_map as $status => $status_name) {
// Always keep the task's current status.
if ($status == $current_status) {
continue;
}
// Don't allow tasks to be changed directly into "Closed, Duplicate"
// status. Instead, you have to merge them. See T4819.
if ($status == $dup_status) {
unset($status_map[$status]);
continue;
}
// Don't let new or existing tasks be moved into a disabled status.
if (ManiphestTaskStatus::isDisabledStatus($status)) {
unset($status_map[$status]);
continue;
}
}
return $status_map;
}
private function getTaskPriorityMap(ManiphestTask $task) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$current_priority = $task->getPriority();
// If the current value isn't a legitimate one, put it in the dropdown
// anyway so saving the form doesn't cause a side effects.
if (idx($priority_map, $current_priority) === null) {
$priority_map[$current_priority] = pht(
'<Unknown: %s>',
$current_priority);
}
foreach ($priority_map as $priority => $priority_name) {
// Always keep the current priority.
if ($priority == $current_priority) {
continue;
}
if (ManiphestTaskPriority::isDisabledPriority($priority)) {
unset($priority_map[$priority]);
continue;
}
}
return $priority_map;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
if ($request->isAjax()) {
// Reload the task to make sure we pick up the final task state.
$viewer = $this->getViewer();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($object->getID()))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
switch ($request->getStr('responseType')) {
case 'card':
return $this->buildCardResponse($task);
default:
return $this->buildListResponse($task);
}
}
return parent::newEditResponse($request, $object, $xactions);
}
private function buildListResponse(ManiphestTask $task) {
$controller = $this->getController();
$payload = array(
'tasks' => $controller->renderSingleTask($task),
'data' => array(),
);
return id(new AphrontAjaxResponse())->setContent($payload);
}
private function buildCardResponse(ManiphestTask $task) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $request->getViewer();
$column_phid = $request->getStr('columnPHID');
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withPHIDs(array($column_phid))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$board_phid = $column->getProjectPHID();
$object_phid = $task->getPHID();
return id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
->setVisiblePHIDs($visible_phids)
->buildResponse();
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index b88c5a5923..6c9a7d0089 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1082 +1,1197 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
+ private $moreValidationErrors = array();
+
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = ManiphestTransaction::TYPE_PRIORITY;
$types[] = ManiphestTransaction::TYPE_STATUS;
$types[] = ManiphestTransaction::TYPE_TITLE;
$types[] = ManiphestTransaction::TYPE_DESCRIPTION;
$types[] = ManiphestTransaction::TYPE_OWNER;
$types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
- $types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
$types[] = ManiphestTransaction::TYPE_MERGED_INTO;
$types[] = ManiphestTransaction::TYPE_MERGED_FROM;
$types[] = ManiphestTransaction::TYPE_UNBLOCK;
$types[] = ManiphestTransaction::TYPE_PARENT;
- $types[] = ManiphestTransaction::TYPE_COLUMN;
$types[] = ManiphestTransaction::TYPE_COVER_IMAGE;
$types[] = ManiphestTransaction::TYPE_POINTS;
+ $types[] = PhabricatorTransactions::TYPE_COLUMNS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getPriority();
case ManiphestTransaction::TYPE_STATUS:
if ($this->getIsNewObject()) {
return null;
}
return $object->getStatus();
case ManiphestTransaction::TYPE_TITLE:
if ($this->getIsNewObject()) {
return null;
}
return $object->getTitle();
case ManiphestTransaction::TYPE_DESCRIPTION:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDescription();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($object->getOwnerPHID(), null);
- case ManiphestTransaction::TYPE_PROJECT_COLUMN:
- // These are pre-populated.
- return $xaction->getOldValue();
case ManiphestTransaction::TYPE_SUBPRIORITY:
return $object->getSubpriority();
case ManiphestTransaction::TYPE_COVER_IMAGE:
return $object->getCoverImageFilePHID();
case ManiphestTransaction::TYPE_POINTS:
$points = $object->getPoints();
if ($points !== null) {
$points = (double)$points;
}
return $points;
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
return null;
case ManiphestTransaction::TYPE_PARENT:
- case ManiphestTransaction::TYPE_COLUMN:
+ case PhabricatorTransactions::TYPE_COLUMNS:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return (int)$xaction->getNewValue();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($xaction->getNewValue(), null);
case ManiphestTransaction::TYPE_STATUS:
case ManiphestTransaction::TYPE_TITLE:
case ManiphestTransaction::TYPE_DESCRIPTION:
case ManiphestTransaction::TYPE_SUBPRIORITY:
- case ManiphestTransaction::TYPE_PROJECT_COLUMN:
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
case ManiphestTransaction::TYPE_UNBLOCK:
case ManiphestTransaction::TYPE_COVER_IMAGE:
return $xaction->getNewValue();
case ManiphestTransaction::TYPE_PARENT:
- case ManiphestTransaction::TYPE_COLUMN:
+ case PhabricatorTransactions::TYPE_COLUMNS:
return $xaction->getNewValue();
case ManiphestTransaction::TYPE_POINTS:
$value = $xaction->getNewValue();
if (!strlen($value)) {
$value = null;
}
if ($value !== null) {
$value = (double)$value;
}
return $value;
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
- case ManiphestTransaction::TYPE_PROJECT_COLUMN:
- $new_column_phids = $new['columnPHIDs'];
- $old_column_phids = $old['columnPHIDs'];
- sort($new_column_phids);
- sort($old_column_phids);
- return ($old !== $new);
+ case PhabricatorTransactions::TYPE_COLUMNS:
+ return (bool)$new;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return $object->setPriority($xaction->getNewValue());
case ManiphestTransaction::TYPE_STATUS:
return $object->setStatus($xaction->getNewValue());
case ManiphestTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case ManiphestTransaction::TYPE_DESCRIPTION:
return $object->setDescription($xaction->getNewValue());
case ManiphestTransaction::TYPE_OWNER:
$phid = $xaction->getNewValue();
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($phid) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
return $object->setOwnerPHID($phid);
case ManiphestTransaction::TYPE_SUBPRIORITY:
$object->setSubpriority($xaction->getNewValue());
return;
- case ManiphestTransaction::TYPE_PROJECT_COLUMN:
- // these do external (edge) updates
- return;
case ManiphestTransaction::TYPE_MERGED_INTO:
$object->setStatus(ManiphestTaskStatus::getDuplicateStatus());
return;
case ManiphestTransaction::TYPE_COVER_IMAGE:
$file_phid = $xaction->getNewValue();
if ($file_phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs(array($file_phid))
->executeOne();
} else {
$file = null;
}
if (!$file || !$file->isTransformableImage()) {
$object->setProperty('cover.filePHID', null);
$object->setProperty('cover.thumbnailPHID', null);
return;
}
$xform_key = PhabricatorFileThumbnailTransform::TRANSFORM_WORKCARD;
$xform = PhabricatorFileTransform::getTransformByKey($xform_key)
->executeTransform($file);
$object->setProperty('cover.filePHID', $file->getPHID());
$object->setProperty('cover.thumbnailPHID', $xform->getPHID());
return;
case ManiphestTransaction::TYPE_POINTS:
$object->setPoints($xaction->getNewValue());
return;
case ManiphestTransaction::TYPE_MERGED_FROM:
case ManiphestTransaction::TYPE_PARENT:
- case ManiphestTransaction::TYPE_COLUMN:
+ case PhabricatorTransactions::TYPE_COLUMNS:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PARENT:
$parent_phid = $xaction->getNewValue();
$parent_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$task_phid = $object->getPHID();
id(new PhabricatorEdgeEditor())
->addEdge($parent_phid, $parent_type, $task_phid)
->save();
break;
- case ManiphestTransaction::TYPE_PROJECT_COLUMN:
- $board_phid = idx($xaction->getNewValue(), 'projectPHID');
- if (!$board_phid) {
- throw new Exception(
- pht(
- "Expected '%s' in column transaction.",
- 'projectPHID'));
+ case PhabricatorTransactions::TYPE_COLUMNS:
+ foreach ($xaction->getNewValue() as $move) {
+ $this->applyBoardMove($object, $move);
}
-
- $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
- $new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
- if (count($new_phids) !== 1) {
- throw new Exception(
- pht(
- "Expected exactly one '%s' in column transaction.",
- 'columnPHIDs'));
- }
-
- $before_phid = idx($xaction->getNewValue(), 'beforePHID');
- $after_phid = idx($xaction->getNewValue(), 'afterPHID');
-
- if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) {
- // If we are not moving the object between columns and also not
- // reordering the position, this is a move on some other order
- // (like priority). We can leave the positions untouched and just
- // bail, there's no work to be done.
- return;
- }
-
- // Otherwise, we're either moving between columns or adjusting the
- // object's position in the "natural" ordering, so we do need to update
- // some rows.
-
- $object_phid = $object->getPHID();
-
- // We're doing layout with the ominpotent viewer to make sure we don't
- // remove positions in columns that exist, but which the actual actor
- // can't see.
- $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
-
- $select_phids = array($board_phid);
-
- $descendants = id(new PhabricatorProjectQuery())
- ->setViewer($omnipotent_viewer)
- ->withAncestorProjectPHIDs($select_phids)
- ->execute();
- foreach ($descendants as $descendant) {
- $select_phids[] = $descendant->getPHID();
- }
-
- $board_tasks = id(new ManiphestTaskQuery())
- ->setViewer($omnipotent_viewer)
- ->withEdgeLogicPHIDs(
- PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
- PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
- array($select_phids))
- ->execute();
-
- $board_tasks = mpull($board_tasks, null, 'getPHID');
- $board_tasks[$object_phid] = $object;
-
- // Make sure tasks are sorted by ID, so we lay out new positions in
- // a consistent way.
- $board_tasks = msort($board_tasks, 'getID');
-
- $object_phids = array_keys($board_tasks);
-
- $engine = id(new PhabricatorBoardLayoutEngine())
- ->setViewer($omnipotent_viewer)
- ->setBoardPHIDs(array($board_phid))
- ->setObjectPHIDs($object_phids)
- ->executeLayout();
-
- // TODO: This logic needs to be revised if we legitimately support
- // multiple column positions.
-
- // NOTE: When a task is newly created, it's implicitly added to the
- // backlog but we don't currently record that in the "$old_phids". Just
- // clean it up for now.
- $columns = $engine->getObjectColumns($board_phid, $object_phid);
- foreach ($columns as $column) {
- $engine->queueRemovePosition(
- $board_phid,
- $column->getPHID(),
- $object_phid);
- }
-
- // Remove all existing column positions on the board.
- foreach ($old_phids as $column_phid) {
- $engine->queueRemovePosition(
- $board_phid,
- $column_phid,
- $object_phid);
- }
-
- // Add new positions.
- foreach ($new_phids as $column_phid) {
- if ($before_phid) {
- $engine->queueAddPositionBefore(
- $board_phid,
- $column_phid,
- $object_phid,
- $before_phid);
- } else if ($after_phid) {
- $engine->queueAddPositionAfter(
- $board_phid,
- $column_phid,
- $object_phid,
- $after_phid);
- } else {
- $engine->queueAddPosition(
- $board_phid,
- $column_phid,
- $object_phid);
- }
- }
-
- $engine->applyPositionUpdates();
-
break;
default:
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$parent_xaction = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
if ($this->getIsNewObject()) {
$parent_xaction->setMetadataValue('blocker.new', true);
}
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht('One of the tasks a task is blocked by changes status.'),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}")
->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
- $type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN;
+ $type_columns = PhabricatorTransactions::TYPE_COLUMNS;
foreach ($xactions as $xaction) {
- if ($xaction->getTransactionType() == $type_column) {
- $new = $xaction->getNewValue();
- $project_phid = idx($new, 'projectPHID');
- if ($project_phid) {
- $board_phids[] = $project_phid;
+ if ($xaction->getTransactionType() == $type_columns) {
+ $moves = $xaction->getNewValue();
+ foreach ($moves as $move) {
+ $board_phids[] = $move['boardPHID'];
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestEditPriorityCapability::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestEditStatusCapability::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestEditAssignCapability::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = null;
if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$app_capability = ManiphestEditProjectsCapability::CAPABILITY;
break;
}
} else {
$app_capability = idx($app_capability_map, $transaction_type);
}
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_OWNER:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
continue;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after,
$allow_recursion = true) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 0.01;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to move it and all
// tasks with the same subpriority a little farther down the subpriority
// scale.
if ($allow_recursion &&
(abs($adjacent->getSubpriority() - $base) < $epsilon)) {
$conn_w = $adjacent->establishConnection('w');
$min = ($adjacent->getSubpriority() - ($epsilon));
$max = ($adjacent->getSubpriority() + ($epsilon));
// Get all of the tasks with the similar subpriorities to the adjacent
// task, including the adjacent task itself.
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($adjacent->getPriority()))
->withSubpriorityBetween($min, $max);
if (!$is_after) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$shift_all = $query->execute();
$shift_last = last($shift_all);
// Select the most extreme subpriority in the result set as the
// base value.
$shift_base = head($shift_all)->getSubpriority();
// Find the subpriority before or after the task at the end of the
// block.
list($shift_pri, $shift_sub) = self::getAdjacentSubpriority(
$shift_last,
$is_after,
$allow_recursion = false);
$delta = ($shift_sub - $shift_base);
$count = count($shift_all);
$shift = array();
$cursor = 1;
foreach ($shift_all as $shift_task) {
$shift_target = $shift_base + (($cursor / $count) * $delta);
$cursor++;
queryfx(
$conn_w,
'UPDATE %T SET subpriority = %f WHERE id = %d',
$adjacent->getTableName(),
$shift_target,
$shift_task->getID());
// If we're shifting the adjacent task, update it.
if ($shift_task->getID() == $adjacent->getID()) {
$adjacent->setSubpriority($shift_target);
}
// If we're shifting the original target task, update the base
// subpriority.
if ($shift_task->getID() == $dst->getID()) {
$base = $shift_target;
}
}
}
$sub = ($adjacent->getSubpriority() + $base) / 2;
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case ManiphestTransaction::TYPE_TITLE:
$missing = $this->validateIsEmptyTextField(
$object->getTitle(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Task title is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case ManiphestTransaction::TYPE_PARENT:
$with_effect = array();
foreach ($xactions as $xaction) {
$task_phid = $xaction->getNewValue();
if (!$task_phid) {
continue;
}
$with_effect[] = $xaction;
$task = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs(array($task_phid))
->executeOne();
if (!$task) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Parent task identifier "%s" does not identify a visible '.
'task.',
$task_phid),
$xaction);
}
}
if ($with_effect && !$this->getIsNewObject()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can only select a parent task when creating a '.
'transaction for the first time.'),
last($with_effect));
}
break;
- case ManiphestTransaction::TYPE_COLUMN:
- $with_effect = array();
- foreach ($xactions as $xaction) {
- $column_phid = $xaction->getNewValue();
- if (!$column_phid) {
- continue;
- }
-
- $with_effect[] = $xaction;
-
- $column = $this->loadProjectColumn($column_phid);
- if (!$column) {
- $errors[] = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Invalid'),
- pht(
- 'Column PHID "%s" does not identify a visible column.',
- $column_phid),
- $xaction);
- }
- }
-
- if ($with_effect && !$this->getIsNewObject()) {
- $errors[] = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Invalid'),
- pht(
- 'You can only put a task into an initial column during task '.
- 'creation.'),
- last($with_effect));
- }
- break;
case ManiphestTransaction::TYPE_OWNER:
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!strlen($new)) {
continue;
}
if ($new === $old) {
continue;
}
$assignee_list = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs(array($new))
->execute();
if (!$assignee_list) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'User "%s" is not a valid user.',
$new),
$xaction);
}
}
break;
case ManiphestTransaction::TYPE_COVER_IMAGE:
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!$new) {
continue;
}
if ($new === $old) {
continue;
}
$file = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs(array($new))
->executeOne();
if (!$file) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('File "%s" is not valid.', $new),
$xaction);
continue;
}
if (!$file->isTransformableImage()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('File "%s" is not a valid image file.', $new),
$xaction);
continue;
}
}
break;
case ManiphestTransaction::TYPE_POINTS:
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (strlen($new) && !is_numeric($new)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('Points value must be numeric or empty.'),
$xaction);
continue;
}
if ((double)$new < 0) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('Points value must be nonnegative.'),
$xaction);
continue;
}
}
break;
}
return $errors;
}
+ protected function validateAllTransactions(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+
+ $errors = parent::validateAllTransactions($object, $xactions);
+
+ if ($this->moreValidationErrors) {
+ $errors = array_merge($errors, $this->moreValidationErrors);
+ }
+
+ return $errors;
+ }
+
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_unassigned = ($object->getOwnerPHID() === null);
$any_assign = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == ManiphestTransaction::TYPE_OWNER) {
$any_assign = true;
break;
}
}
$is_open = !$object->isClosed();
$new_status = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$new_status = $xaction->getNewValue();
break;
}
}
if ($new_status === null) {
$is_closing = false;
} else {
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
}
// If the task is not assigned, not being assigned, currently open, and
// being closed, try to assign the actor as the owner.
if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
// Don't assign the actor if they aren't a real user.
// Don't claim the task if the status is configured to not claim.
if ($actor_phid && $is_claim) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setNewValue($actor_phid);
}
}
// Automatically subscribe the author when they create a task.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
- switch ($xaction->getTransactionType()) {
- case ManiphestTransaction::TYPE_COLUMN:
- $column_phid = $xaction->getNewValue();
- if (!$column_phid) {
- break;
+ $type = $xaction->getTransactionType();
+ switch ($type) {
+ case PhabricatorTransactions::TYPE_COLUMNS:
+ try {
+ $this->buildMoveTransaction($object, $xaction);
+
+ // Implicilty add the task to any boards that we're moving it
+ // on, since moves on a board the task isn't part of are not
+ // meaningful.
+ $board_phids = ipull($xaction->getNewValue(), 'boardPHID');
+ if ($board_phids) {
+ $results[] = id(new ManiphestTransaction())
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue(
+ 'edge:type',
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
+ ->setIgnoreOnNoEffect(true)
+ ->setNewValue(
+ array(
+ '+' => array_fuse($board_phids),
+ ));
+ }
+ } catch (Exception $ex) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Invalid'),
+ $ex->getMessage(),
+ $xaction);
+ $this->moreValidationErrors[] = $error;
}
-
- // When a task is created into a column, we also generate a transaction
- // to actually put it in that column.
- $column = $this->loadProjectColumn($column_phid);
- $results[] = id(new ManiphestTransaction())
- ->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN)
- ->setOldValue(
- array(
- 'projectPHID' => $column->getProjectPHID(),
- 'columnPHIDs' => array(),
- ))
- ->setNewValue(
- array(
- 'projectPHID' => $column->getProjectPHID(),
- 'columnPHIDs' => array($column->getPHID()),
- ));
break;
case ManiphestTransaction::TYPE_OWNER:
// If this is a no-op update, don't expand it.
$old_value = $object->getOwnerPHID();
$new_value = $xaction->getNewValue();
if ($old_value === $new_value) {
continue;
}
// When a task is reassigned, move the old owner to the subscriber
// list so they're still in the loop.
if ($old_value) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array($old_value => $old_value),
));
}
break;
}
return $results;
}
- private function loadProjectColumn($column_phid) {
- return id(new PhabricatorProjectColumnQuery())
- ->setViewer($this->getActor())
- ->withPHIDs(array($column_phid))
- ->executeOne();
- }
-
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$phids = parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_COVER_IMAGE:
$phids[] = $xaction->getNewValue();
break;
}
return $phids;
}
+ private function buildMoveTransaction(
+ PhabricatorLiskDAO $object,
+ PhabricatorApplicationTransaction $xaction) {
+
+ $new = $xaction->getNewValue();
+ if (!is_array($new)) {
+ $this->validateColumnPHID($new);
+ $new = array($new);
+ }
+
+ $nearby_phids = array();
+ foreach ($new as $key => $value) {
+ if (!is_array($value)) {
+ $this->validateColumnPHID($value);
+ $value = array(
+ 'columnPHID' => $value,
+ );
+ }
+
+ PhutilTypeSpec::checkMap(
+ $value,
+ array(
+ 'columnPHID' => 'string',
+ 'beforePHID' => 'optional string',
+ 'afterPHID' => 'optional string',
+ ));
+
+ $new[$key] = $value;
+
+ if (!empty($value['beforePHID'])) {
+ $nearby_phids[] = $value['beforePHID'];
+ }
+
+ if (!empty($value['afterPHID'])) {
+ $nearby_phids[] = $value['afterPHID'];
+ }
+ }
+
+ if ($nearby_phids) {
+ $nearby_objects = id(new PhabricatorObjectQuery())
+ ->setViewer($this->getActor())
+ ->withPHIDs($nearby_phids)
+ ->execute();
+ $nearby_objects = mpull($nearby_objects, null, 'getPHID');
+ } else {
+ $nearby_objects = array();
+ }
+
+ $column_phids = ipull($new, 'columnPHID');
+ if ($column_phids) {
+ $columns = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($this->getActor())
+ ->withPHIDs($column_phids)
+ ->execute();
+ $columns = mpull($columns, null, 'getPHID');
+ } else {
+ $columns = array();
+ }
+
+ $board_phids = mpull($columns, 'getProjectPHID');
+ $object_phid = $object->getPHID();
+
+ $object_phids = $nearby_phids;
+
+ // Note that we may not have an object PHID if we're creating a new
+ // object.
+ if ($object_phid) {
+ $object_phids[] = $object_phid;
+ }
+
+ if ($object_phids) {
+ $layout_engine = id(new PhabricatorBoardLayoutEngine())
+ ->setViewer($this->getActor())
+ ->setBoardPHIDs($board_phids)
+ ->setObjectPHIDs($object_phids)
+ ->setFetchAllBoards(true)
+ ->executeLayout();
+ }
+
+ foreach ($new as $key => $spec) {
+ $column_phid = $spec['columnPHID'];
+ $column = idx($columns, $column_phid);
+ if (!$column) {
+ throw new Exception(
+ pht(
+ 'Column move transaction specifies column PHID "%s", but there '.
+ 'is no corresponding column with this PHID.',
+ $column_phid));
+ }
+
+ $board_phid = $column->getProjectPHID();
+
+ $nearby = array();
+
+ if (!empty($spec['beforePHID'])) {
+ $nearby['beforePHID'] = $spec['beforePHID'];
+ }
+
+ if (!empty($spec['afterPHID'])) {
+ $nearby['afterPHID'] = $spec['afterPHID'];
+ }
+
+ if (count($nearby) > 1) {
+ throw new Exception(
+ pht(
+ 'Column move transaction moves object to multiple positions. '.
+ 'Specify only "beforePHID" or "afterPHID", not both.'));
+ }
+
+ foreach ($nearby as $where => $nearby_phid) {
+ if (empty($nearby_objects[$nearby_phid])) {
+ throw new Exception(
+ pht(
+ 'Column move transaction specifies object "%s" as "%s", but '.
+ 'there is no corresponding object with this PHID.',
+ $object_phid,
+ $where));
+ }
+
+ $nearby_columns = $layout_engine->getObjectColumns(
+ $board_phid,
+ $nearby_phid);
+ $nearby_columns = mpull($nearby_columns, null, 'getPHID');
+
+ if (empty($nearby_columns[$column_phid])) {
+ throw new Exception(
+ pht(
+ 'Column move transaction specifies object "%s" as "%s" in '.
+ 'column "%s", but this object is not in that column!',
+ $nearby_phid,
+ $where,
+ $column_phid));
+ }
+ }
+
+ if ($object_phid) {
+ $old_columns = $layout_engine->getObjectColumns(
+ $board_phid,
+ $object_phid);
+ $old_column_phids = mpull($old_columns, 'getPHID');
+ } else {
+ $old_column_phids = array();
+ }
+
+ $spec += array(
+ 'boardPHID' => $board_phid,
+ 'fromColumnPHIDs' => $old_column_phids,
+ );
+
+ // Check if the object is already in this column, and isn't being moved.
+ // We can just drop this column change if it has no effect.
+ $from_map = array_fuse($spec['fromColumnPHIDs']);
+ $already_here = isset($from_map[$column_phid]);
+ $is_reordering = (bool)$nearby;
+
+ if ($already_here && !$is_reordering) {
+ unset($new[$key]);
+ } else {
+ $new[$key] = $spec;
+ }
+ }
+
+ $new = array_values($new);
+ $xaction->setNewValue($new);
+ }
+
+ private function applyBoardMove($object, array $move) {
+ $board_phid = $move['boardPHID'];
+ $column_phid = $move['columnPHID'];
+ $before_phid = idx($move, 'beforePHID');
+ $after_phid = idx($move, 'afterPHID');
+
+ $object_phid = $object->getPHID();
+
+ // We're doing layout with the ominpotent viewer to make sure we don't
+ // remove positions in columns that exist, but which the actual actor
+ // can't see.
+ $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
+
+ $select_phids = array($board_phid);
+
+ $descendants = id(new PhabricatorProjectQuery())
+ ->setViewer($omnipotent_viewer)
+ ->withAncestorProjectPHIDs($select_phids)
+ ->execute();
+ foreach ($descendants as $descendant) {
+ $select_phids[] = $descendant->getPHID();
+ }
+
+ $board_tasks = id(new ManiphestTaskQuery())
+ ->setViewer($omnipotent_viewer)
+ ->withEdgeLogicPHIDs(
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
+ PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
+ array($select_phids))
+ ->execute();
+
+ $board_tasks = mpull($board_tasks, null, 'getPHID');
+ $board_tasks[$object_phid] = $object;
+
+ // Make sure tasks are sorted by ID, so we lay out new positions in
+ // a consistent way.
+ $board_tasks = msort($board_tasks, 'getID');
+
+ $object_phids = array_keys($board_tasks);
+
+ $engine = id(new PhabricatorBoardLayoutEngine())
+ ->setViewer($omnipotent_viewer)
+ ->setBoardPHIDs(array($board_phid))
+ ->setObjectPHIDs($object_phids)
+ ->executeLayout();
+
+ // TODO: This logic needs to be revised when we legitimately support
+ // multiple column positions.
+ $columns = $engine->getObjectColumns($board_phid, $object_phid);
+ foreach ($columns as $column) {
+ $engine->queueRemovePosition(
+ $board_phid,
+ $column->getPHID(),
+ $object_phid);
+ }
+
+ if ($before_phid) {
+ $engine->queueAddPositionBefore(
+ $board_phid,
+ $column_phid,
+ $object_phid,
+ $before_phid);
+ } else if ($after_phid) {
+ $engine->queueAddPositionAfter(
+ $board_phid,
+ $column_phid,
+ $object_phid,
+ $after_phid);
+ } else {
+ $engine->queueAddPosition(
+ $board_phid,
+ $column_phid,
+ $object_phid);
+ }
+
+ $engine->applyPositionUpdates();
+ }
+
+
+ private function validateColumnPHID($value) {
+ if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
+ return;
+ }
+
+ throw new Exception(
+ pht(
+ 'When moving objects between columns on a board, columns must '.
+ 'be identified by PHIDs. This transaction uses "%s" to identify '.
+ 'a column, but that is not a valid column PHID.',
+ $value));
+ }
+
+
}
diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index caf57a3f71..79901d7ae6 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -1,991 +1,953 @@
<?php
final class ManiphestTransaction
extends PhabricatorApplicationTransaction {
const TYPE_TITLE = 'title';
const TYPE_STATUS = 'status';
const TYPE_DESCRIPTION = 'description';
const TYPE_OWNER = 'reassign';
const TYPE_PRIORITY = 'priority';
const TYPE_EDGE = 'edge';
const TYPE_SUBPRIORITY = 'subpriority';
- const TYPE_PROJECT_COLUMN = 'projectcolumn';
const TYPE_MERGED_INTO = 'mergedinto';
const TYPE_MERGED_FROM = 'mergedfrom';
const TYPE_UNBLOCK = 'unblock';
const TYPE_PARENT = 'parent';
- const TYPE_COLUMN = 'column';
const TYPE_COVER_IMAGE = 'cover-image';
const TYPE_POINTS = 'points';
// NOTE: this type is deprecated. Keep it around for legacy installs
// so any transactions render correctly.
const TYPE_ATTACH = 'attach';
const MAILTAG_STATUS = 'maniphest-status';
const MAILTAG_OWNER = 'maniphest-owner';
const MAILTAG_PRIORITY = 'maniphest-priority';
const MAILTAG_CC = 'maniphest-cc';
const MAILTAG_PROJECTS = 'maniphest-projects';
const MAILTAG_COMMENT = 'maniphest-comment';
const MAILTAG_COLUMN = 'maniphest-column';
const MAILTAG_UNBLOCK = 'maniphest-unblock';
const MAILTAG_OTHER = 'maniphest-other';
public function getApplicationName() {
return 'maniphest';
}
public function getApplicationTransactionType() {
return ManiphestTaskPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new ManiphestTransactionComment();
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
- case self::TYPE_PROJECT_COLUMN:
case self::TYPE_EDGE:
case self::TYPE_UNBLOCK:
return false;
}
return parent::shouldGenerateOldValue();
}
public function getRemarkupBlocks() {
$blocks = parent::getRemarkupBlocks();
switch ($this->getTransactionType()) {
case self::TYPE_DESCRIPTION:
$blocks[] = $this->getNewValue();
break;
}
return $blocks;
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$new = $this->getNewValue();
$old = $this->getOldValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
if ($new) {
$phids[] = $new;
}
if ($old) {
$phids[] = $old;
}
break;
- case self::TYPE_PROJECT_COLUMN:
- $phids[] = $new['projectPHID'];
- $phids[] = head($new['columnPHIDs']);
- break;
case self::TYPE_MERGED_INTO:
$phids[] = $new;
break;
case self::TYPE_MERGED_FROM:
$phids = array_merge($phids, $new);
break;
case self::TYPE_EDGE:
$phids = array_mergev(
array(
$phids,
array_keys(nonempty($old, array())),
array_keys(nonempty($new, array())),
));
break;
case self::TYPE_ATTACH:
$old = nonempty($old, array());
$new = nonempty($new, array());
$phids = array_mergev(
array(
$phids,
array_keys(idx($new, 'FILE', array())),
array_keys(idx($old, 'FILE', array())),
));
break;
case self::TYPE_UNBLOCK:
foreach (array_keys($new) as $phid) {
$phids[] = $phid;
}
break;
case self::TYPE_STATUS:
$commit_phid = $this->getMetadataValue('commitPHID');
if ($commit_phid) {
$phids[] = $commit_phid;
}
break;
}
return $phids;
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$commit_phid = $this->getMetadataValue('commitPHID');
$edge_type = $this->getMetadataValue('edge:type');
if ($edge_type == ManiphestTaskHasCommitEdgeType::EDGECONST) {
if ($commit_phid) {
return true;
}
}
break;
case self::TYPE_DESCRIPTION:
case self::TYPE_PRIORITY:
case self::TYPE_STATUS:
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case self::TYPE_SUBPRIORITY:
case self::TYPE_PARENT:
- case self::TYPE_COLUMN:
return true;
- case self::TYPE_PROJECT_COLUMN:
- $old_cols = idx($this->getOldValue(), 'columnPHIDs');
- $new_cols = idx($this->getNewValue(), 'columnPHIDs');
-
- $old_cols = array_values($old_cols);
- $new_cols = array_values($new_cols);
- sort($old_cols);
- sort($new_cols);
-
- return ($old_cols === $new_cols);
case self::TYPE_COVER_IMAGE:
// At least for now, don't show these.
return true;
case self::TYPE_POINTS:
if (!ManiphestTaskPoints::getIsEnabled()) {
return true;
}
}
return parent::shouldHide();
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case self::TYPE_POINTS:
return true;
}
return parent::shouldHideForMail($xactions);
}
public function shouldHideForFeed() {
switch ($this->getTransactionType()) {
case self::TYPE_UNBLOCK:
// Hide "alice created X, a task blocking Y." from feed because it
// will almost always appear adjacent to "alice created Y".
$is_new = $this->getMetadataValue('blocker.new');
if ($is_new) {
return true;
}
break;
case self::TYPE_POINTS:
return true;
}
return parent::shouldHideForFeed();
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
return 1.4;
case self::TYPE_STATUS:
return 1.3;
case self::TYPE_OWNER:
return 1.2;
case self::TYPE_PRIORITY:
return 1.1;
}
return parent::getActionStrength();
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
if ($this->getAuthorPHID() == $new) {
return 'green';
} else if (!$new) {
return 'black';
} else if (!$old) {
return 'green';
} else {
return 'green';
}
case self::TYPE_STATUS:
$color = ManiphestTaskStatus::getStatusColor($new);
if ($color !== null) {
return $color;
}
if (ManiphestTaskStatus::isOpenStatus($new)) {
return 'green';
} else {
return 'indigo';
}
case self::TYPE_PRIORITY:
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'green';
} else if ($old > $new) {
return 'grey';
} else {
return 'yellow';
}
case self::TYPE_MERGED_FROM:
return 'orange';
case self::TYPE_MERGED_INTO:
return 'indigo';
}
return parent::getColor();
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return pht('Created');
}
return pht('Retitled');
case self::TYPE_STATUS:
$action = ManiphestTaskStatus::getStatusActionName($new);
if ($action) {
return $action;
}
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
if ($new_closed && !$old_closed) {
return pht('Closed');
} else if (!$new_closed && $old_closed) {
return pht('Reopened');
} else {
return pht('Changed Status');
}
case self::TYPE_DESCRIPTION:
return pht('Edited');
case self::TYPE_OWNER:
if ($this->getAuthorPHID() == $new) {
return pht('Claimed');
} else if (!$new) {
return pht('Unassigned');
} else if (!$old) {
return pht('Assigned');
} else {
return pht('Reassigned');
}
- case self::TYPE_PROJECT_COLUMN:
+ case PhabricatorTransactions::TYPE_COLUMNS:
return pht('Changed Project Column');
case self::TYPE_PRIORITY:
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return pht('Triaged');
} else if ($old > $new) {
return pht('Lowered Priority');
} else {
return pht('Raised Priority');
}
case self::TYPE_EDGE:
case self::TYPE_ATTACH:
return pht('Attached');
case self::TYPE_UNBLOCK:
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
if ($old_closed && !$new_closed) {
return pht('Block');
} else if (!$old_closed && $new_closed) {
return pht('Unblock');
} else {
return pht('Blocker');
}
case self::TYPE_MERGED_INTO:
case self::TYPE_MERGED_FROM:
return pht('Merged');
}
return parent::getActionName();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
return 'fa-user';
case self::TYPE_TITLE:
if ($old === null) {
return 'fa-pencil';
}
return 'fa-pencil';
case self::TYPE_STATUS:
$action = ManiphestTaskStatus::getStatusIcon($new);
if ($action !== null) {
return $action;
}
if (ManiphestTaskStatus::isClosedStatus($new)) {
return 'fa-check';
} else {
return 'fa-pencil';
}
case self::TYPE_DESCRIPTION:
return 'fa-pencil';
- case self::TYPE_PROJECT_COLUMN:
+ case PhabricatorTransactions::TYPE_COLUMNS:
return 'fa-columns';
case self::TYPE_MERGED_INTO:
return 'fa-check';
case self::TYPE_MERGED_FROM:
return 'fa-compress';
case self::TYPE_PRIORITY:
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'fa-arrow-right';
} else if ($old > $new) {
return 'fa-arrow-down';
} else {
return 'fa-arrow-up';
}
case self::TYPE_EDGE:
case self::TYPE_ATTACH:
return 'fa-thumb-tack';
case self::TYPE_UNBLOCK:
return 'fa-shield';
}
return parent::getIcon();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this task.',
$this->renderHandleLink($author_phid));
case self::TYPE_TITLE:
if ($old === null) {
return pht(
'%s created this task.',
$this->renderHandleLink($author_phid));
}
return pht(
'%s changed the title from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_DESCRIPTION:
return pht(
'%s edited the task description.',
$this->renderHandleLink($author_phid));
case self::TYPE_STATUS:
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
$old_name = ManiphestTaskStatus::getTaskStatusName($old);
$new_name = ManiphestTaskStatus::getTaskStatusName($new);
$commit_phid = $this->getMetadataValue('commitPHID');
if ($new_closed && !$old_closed) {
if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
if ($commit_phid) {
return pht(
'%s closed this task as a duplicate by committing %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s closed this task as a duplicate.',
$this->renderHandleLink($author_phid));
}
} else {
if ($commit_phid) {
return pht(
'%s closed this task as "%s" by committing %s.',
$this->renderHandleLink($author_phid),
$new_name,
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s closed this task as "%s".',
$this->renderHandleLink($author_phid),
$new_name);
}
}
} else if (!$new_closed && $old_closed) {
if ($commit_phid) {
return pht(
'%s reopened this task as "%s" by committing %s.',
$this->renderHandleLink($author_phid),
$new_name,
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s reopened this task as "%s".',
$this->renderHandleLink($author_phid),
$new_name);
}
} else {
if ($commit_phid) {
return pht(
'%s changed the task status from "%s" to "%s" by committing %s.',
$this->renderHandleLink($author_phid),
$old_name,
$new_name,
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s changed the task status from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old_name,
$new_name);
}
}
case self::TYPE_UNBLOCK:
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($this->getMetadataValue('blocker.new')) {
return pht(
'%s created blocking task %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid));
} else if ($old_closed && !$new_closed) {
return pht(
'%s reopened blocking task %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$new_name);
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed blocking task %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$new_name);
} else {
return pht(
'%s changed the status of blocking task %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$old_name,
$new_name);
}
case self::TYPE_OWNER:
if ($author_phid == $new) {
return pht(
'%s claimed this task.',
$this->renderHandleLink($author_phid));
} else if (!$new) {
return pht(
'%s removed %s as the assignee of this task.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($old));
} else if (!$old) {
return pht(
'%s assigned this task to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s reassigned this task from %s to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case self::TYPE_PRIORITY:
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return pht(
'%s triaged this task as "%s" priority.',
$this->renderHandleLink($author_phid),
$new_name);
} else if ($old > $new) {
return pht(
'%s lowered the priority of this task from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old_name,
$new_name);
} else {
return pht(
'%s raised the priority of this task from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old_name,
$new_name);
}
case self::TYPE_ATTACH:
$old = nonempty($old, array());
$new = nonempty($new, array());
$new = array_keys(idx($new, 'FILE', array()));
$old = array_keys(idx($old, 'FILE', array()));
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
if ($added && !$removed) {
return pht(
'%s attached %s file(s): %s.',
$this->renderHandleLink($author_phid),
phutil_count($added),
$this->renderHandleList($added));
} else if ($removed && !$added) {
return pht(
'%s detached %s file(s): %s.',
$this->renderHandleLink($author_phid),
phutil_count($removed),
$this->renderHandleList($removed));
} else {
return pht(
'%s changed file(s), attached %s: %s; detached %s: %s.',
$this->renderHandleLink($author_phid),
phutil_count($added),
$this->renderHandleList($added),
phutil_count($removed),
$this->renderHandleList($removed));
}
- case self::TYPE_PROJECT_COLUMN:
- $project_phid = $new['projectPHID'];
- $column_phid = head($new['columnPHIDs']);
- return pht(
- '%s moved this task to %s on the %s workboard.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink($column_phid),
- $this->renderHandleLink($project_phid));
- break;
-
case self::TYPE_MERGED_INTO:
return pht(
'%s closed this task as a duplicate of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($new));
break;
case self::TYPE_MERGED_FROM:
return pht(
'%s merged %s task(s): %s.',
$this->renderHandleLink($author_phid),
phutil_count($new),
$this->renderHandleList($new));
break;
case self::TYPE_POINTS:
if ($old === null) {
return pht(
'%s set the point value for this task to %s.',
$this->renderHandleLink($author_phid),
$new);
} else if ($new === null) {
return pht(
'%s removed the point value for this task.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s changed the point value for this task from %s to %s.',
$this->renderHandleLink($author_phid),
$old,
$new);
}
}
return parent::getTitle();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
return pht(
'%s renamed %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old,
$new);
case self::TYPE_DESCRIPTION:
return pht(
'%s edited the description of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case self::TYPE_STATUS:
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
$old_name = ManiphestTaskStatus::getTaskStatusName($old);
$new_name = ManiphestTaskStatus::getTaskStatusName($new);
$commit_phid = $this->getMetadataValue('commitPHID');
if ($new_closed && !$old_closed) {
if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
if ($commit_phid) {
return pht(
'%s closed %s as a duplicate by committing %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s closed %s as a duplicate.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
} else {
if ($commit_phid) {
return pht(
'%s closed %s as "%s" by committing %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$new_name,
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s closed %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$new_name);
}
}
} else if (!$new_closed && $old_closed) {
if ($commit_phid) {
return pht(
'%s reopened %s as "%s" by committing %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$new_name,
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s reopened %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$new_name);
}
} else {
if ($commit_phid) {
return pht(
'%s changed the status of %s from "%s" to "%s" by committing %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old_name,
$new_name,
$this->renderHandleLink($commit_phid));
} else {
return pht(
'%s changed the status of %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old_name,
$new_name);
}
}
case self::TYPE_UNBLOCK:
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($old_closed && !$new_closed) {
return pht(
'%s reopened %s, a task blocking %s, as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$this->renderHandleLink($object_phid),
$new_name);
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed %s, a task blocking %s, as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$this->renderHandleLink($object_phid),
$new_name);
} else {
return pht(
'%s changed the status of %s, a task blocking %s, '.
'from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$this->renderHandleLink($object_phid),
$old_name,
$new_name);
}
case self::TYPE_OWNER:
if ($author_phid == $new) {
return pht(
'%s claimed %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
} else if (!$new) {
return pht(
'%s placed %s up for grabs.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
} else if (!$old) {
return pht(
'%s assigned %s to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s reassigned %s from %s to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case self::TYPE_PRIORITY:
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return pht(
'%s triaged %s as "%s" priority.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$new_name);
} else if ($old > $new) {
return pht(
'%s lowered the priority of %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old_name,
$new_name);
} else {
return pht(
'%s raised the priority of %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old_name,
$new_name);
}
case self::TYPE_ATTACH:
$old = nonempty($old, array());
$new = nonempty($new, array());
$new = array_keys(idx($new, 'FILE', array()));
$old = array_keys(idx($old, 'FILE', array()));
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
if ($added && !$removed) {
return pht(
'%s attached %d file(s) of %s: %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
count($added),
$this->renderHandleList($added));
} else if ($removed && !$added) {
return pht(
'%s detached %d file(s) of %s: %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
count($removed),
$this->renderHandleList($removed));
} else {
return pht(
'%s changed file(s) for %s, attached %d: %s; detached %d: %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
count($added),
$this->renderHandleList($added),
count($removed),
$this->renderHandleList($removed));
}
- case self::TYPE_PROJECT_COLUMN:
- $project_phid = $new['projectPHID'];
- $column_phid = head($new['columnPHIDs']);
- return pht(
- '%s moved %s to %s on the %s workboard.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink($object_phid),
- $this->renderHandleLink($column_phid),
- $this->renderHandleLink($project_phid));
-
case self::TYPE_MERGED_INTO:
return pht(
'%s merged task %s into %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($new));
case self::TYPE_MERGED_FROM:
return pht(
'%s merged %s task(s) %s into %s.',
$this->renderHandleLink($author_phid),
phutil_count($new),
$this->renderHandleList($new),
$this->renderHandleLink($object_phid));
}
return parent::getTitleForFeed();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case self::TYPE_DESCRIPTION:
return true;
}
return parent::hasChangeDetails();
}
public function renderChangeDetails(PhabricatorUser $viewer) {
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case self::TYPE_MERGED_INTO:
case self::TYPE_STATUS:
$tags[] = self::MAILTAG_STATUS;
break;
case self::TYPE_OWNER:
$tags[] = self::MAILTAG_OWNER;
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$tags[] = self::MAILTAG_CC;
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$tags[] = self::MAILTAG_PROJECTS;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
break;
case self::TYPE_PRIORITY:
$tags[] = self::MAILTAG_PRIORITY;
break;
case self::TYPE_UNBLOCK:
$tags[] = self::MAILTAG_UNBLOCK;
break;
- case self::TYPE_PROJECT_COLUMN:
+ case PhabricatorTransactions::TYPE_COLUMNS:
$tags[] = self::MAILTAG_COLUMN;
break;
case PhabricatorTransactions::TYPE_COMMENT:
$tags[] = self::MAILTAG_COMMENT;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case self::TYPE_STATUS:
return pht('The task already has the selected status.');
case self::TYPE_OWNER:
return pht('The task already has the selected owner.');
case self::TYPE_PRIORITY:
return pht('The task already has the selected priority.');
}
return parent::getNoEffectDescription();
}
}
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
index e2b2f9688d..5ead896035 100644
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1,1473 +1,1468 @@
<?php
final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertTrue((bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertFalse((bool)$this->refreshProject($proj, $user2));
}
public function testIsViewerMemberOrWatcher() {
$user1 = $this->createUser()
->save();
$user2 = $this->createUser()
->save();
$user3 = $this->createUser()
->save();
$proj1 = $this->createProject($user1);
$proj1 = $this->refreshProject($proj1, $user1);
$this->joinProject($proj1, $user1);
$this->joinProject($proj1, $user3);
$this->watchProject($proj1, $user3);
$proj1 = $this->refreshProject($proj1, $user1);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, false, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, false);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user2->getPHID()));
$this->assertTrue($proj1->isUserWatcher($user3->getPHID()));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertTrue($this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testAncestorMembers() {
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$parent = $this->createProject($user1);
$child = $this->createProject($user1, $parent);
$this->joinProject($child, $user1);
$this->joinProject($child, $user2);
$project = id(new PhabricatorProjectQuery())
->setViewer($user1)
->withPHIDs(array($child->getPHID()))
->needAncestorMembers(true)
->executeOne();
$members = array_fuse($project->getParentProject()->getMemberPHIDs());
ksort($members);
$expect = array_fuse(
array(
$user1->getPHID(),
$user2->getPHID(),
));
ksort($expect);
$this->assertEqual($expect, $members);
}
public function testAncestryQueries() {
$user = $this->createUser();
$user->save();
$ancestor = $this->createProject($user);
$parent = $this->createProject($user, $ancestor);
$child = $this->createProject($user, $parent);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$parent->getPHID(),
head($projects)->getPHID());
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$child->getPHID(),
head($projects)->getPHID());
$parent2 = $this->createProject($user, $ancestor);
$child2 = $this->createProject($user, $parent2);
$grandchild2 = $this->createProject($user, $child2);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(5, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(3, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(3, null)
->execute();
$this->assertEqual(1, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(
array(
$child->getPHID(),
$grandchild2->getPHID(),
))
->execute();
$this->assertEqual(2, count($projects));
}
public function testMemberMaterialization() {
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->joinProject($child, $user);
$parent_material = PhabricatorEdgeQuery::loadDestinationPHIDs(
$parent->getPHID(),
$material_type);
$this->assertEqual(
array($user->getPHID()),
$parent_material);
}
public function testMilestones() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$m1 = $this->createProject($user, $parent, true);
$m2 = $this->createProject($user, $parent, true);
$m3 = $this->createProject($user, $parent, true);
$this->assertEqual(1, $m1->getMilestoneNumber());
$this->assertEqual(2, $m2->getMilestoneNumber());
$this->assertEqual(3, $m3->getMilestoneNumber());
}
public function testMilestoneMembership() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$milestone = $this->createProject($user, $parent, true);
$this->joinProject($parent, $user);
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue($milestone->isUserMember($user->getPHID()));
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->needMembers(true)
->executeOne();
$this->assertEqual(
array($user->getPHID()),
$milestone->getMemberPHIDs());
}
public function testSameSlugAsName() {
// It should be OK to type the primary hashtag into "additional hashtags",
// even if the primary hashtag doesn't exist yet because you're creating
// or renaming the project.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
// In this first case, set the name and slugs at the same time.
$name = 'slugproject';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
$this->applyTransactions($project, $user, $xactions);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($name));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name, $slugs));
// In this second case, set the name first and then the slugs separately.
$name2 = 'slugproject2';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name2);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($name2));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name2, $slugs));
}
public function testDuplicateSlugs() {
// Creating a project with multiple duplicate slugs should succeed.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'duplicate';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($input, $input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($input, $slugs));
}
public function testNormalizeSlugs() {
// When a user creates a project with slug "XxX360n0sc0perXxX", normalize
// it before writing it.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'NoRmAlIzE';
$expect = 'normalize';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($expect, $slugs));
// If another user tries to add the same slug in denormalized form, it
// should be caught and fail, even though the database version of the slug
// is normalized.
$project2 = $this->createProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($input));
$caught = null;
try {
$this->applyTransactions($project2, $user, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$caught = $ex;
}
$this->assertTrue((bool)$caught);
}
public function testProjectMembersVisibility() {
// This is primarily testing that you can create a project and set the
// visibility or edit policy to "Project Members" immediately.
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$project = PhabricatorProject::initializeNewProject($user1);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue(
id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey());
$edge_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'=' => array($user1->getPHID() => $user1->getPHID()),
));
$this->applyTransactions($project, $user1, $xactions);
$this->assertTrue((bool)$this->refreshProject($project, $user1));
$this->assertFalse((bool)$this->refreshProject($project, $user2));
$this->leaveProject($project, $user1);
$this->assertFalse((bool)$this->refreshProject($project, $user1));
}
public function testParentProject() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->assertTrue(true);
$child = $this->refreshProject($child, $user);
$this->assertEqual(
$parent->getPHID(),
$child->getParentProject()->getPHID());
$this->assertEqual(1, (int)$child->getProjectDepth());
$this->assertFalse(
$child->isUserMember($user->getPHID()));
$this->assertFalse(
$child->getParentProject()->isUserMember($user->getPHID()));
$this->joinProject($child, $user);
$child = $this->refreshProject($child, $user);
$this->assertTrue(
$child->isUserMember($user->getPHID()));
$this->assertTrue(
$child->getParentProject()->isUserMember($user->getPHID()));
// Test that hiding a parent hides the child.
$user2 = $this->createUser();
$user2->save();
// Second user can see the project for now.
$this->assertTrue((bool)$this->refreshProject($child, $user2));
// Hide the parent.
$this->setViewPolicy($parent, $user, $user->getPHID());
// First user (who can see the parent because they are a member of
// the child) can see the project.
$this->assertTrue((bool)$this->refreshProject($child, $user));
// Second user can not, because they can't see the parent.
$this->assertFalse((bool)$this->refreshProject($child, $user2));
}
public function testSlugMaps() {
// When querying by slugs, slugs should be normalized and the mapping
// should be reported correctly.
$user = $this->createUser();
$user->save();
$name = 'queryslugproject';
$name2 = 'QUERYslugPROJECT';
$slug = 'queryslugextra';
$slug2 = 'QuErYSlUgExTrA';
$project = PhabricatorProject::initializeNewProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS)
->setNewValue(array($slug));
$this->applyTransactions($project, $user, $xactions);
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$name => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($slug));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$slug => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name, $slug, $name2, $slug2));
$project_query->execute();
$map = $project_query->getSlugMap();
$expect = array(
$name => $project->getPHID(),
$slug => $project->getPHID(),
$name2 => $project->getPHID(),
$slug2 => $project->getPHID(),
);
$actual = ipull($map, 'projectPHID');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
$expect = array(
$name => $name,
$slug => $slug,
$name2 => $name,
$slug2 => $slug,
);
$actual = ipull($map, 'slug');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
(bool)$proj,
pht(
'Assumption that projects are default visible '.
'to any user when created.'));
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Arbitrary user not member of project.'));
// Join the project.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join works.'));
// Join the project again.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Joining an already-joined project is a no-op.'));
// Leave the project.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave works.'));
// Leave the project again.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leaving an already-left project is a no-op.'));
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
$this->joinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($ex instanceof Exception);
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with edit permission.'));
$this->leaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with join permission.'));
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave allowed without any permission.'));
}
public function testComplexConstraints() {
$user = $this->createUser();
$user->save();
$engineering = $this->createProject($user);
$engineering_scan = $this->createProject($user, $engineering);
$engineering_warp = $this->createProject($user, $engineering);
$exploration = $this->createProject($user);
$exploration_diplomacy = $this->createProject($user, $exploration);
$task_engineering = $this->newTask(
$user,
array($engineering),
pht('Engineering Only'));
$task_exploration = $this->newTask(
$user,
array($exploration),
pht('Exploration Only'));
$task_warp_explore = $this->newTask(
$user,
array($engineering_warp, $exploration),
pht('Warp to New Planet'));
$task_diplomacy_scan = $this->newTask(
$user,
array($engineering_scan, $exploration_diplomacy),
pht('Scan Diplomat'));
$task_diplomacy = $this->newTask(
$user,
array($exploration_diplomacy),
pht('Diplomatic Meeting'));
$task_warp_scan = $this->newTask(
$user,
array($engineering_scan, $engineering_warp),
pht('Scan Warp Drives'));
$this->assertQueryByProjects(
$user,
array(
$task_engineering,
$task_warp_explore,
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering),
pht('All Engineering'));
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering_scan),
pht('All Scan'));
$this->assertQueryByProjects(
$user,
array(
$task_warp_explore,
$task_diplomacy_scan,
),
array($engineering, $exploration),
pht('Engineering + Exploration'));
// This is testing that a query for "Parent" and "Parent > Child" works
// properly.
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering, $engineering_scan),
pht('Engineering + Scan'));
}
public function testTagAncestryConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_masonry = $this->createProject($user, $stonework);
$stonework_sculpting = $this->createProject($user, $stonework);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a descendant should remove the parent.
$this->addProjectTags($user, $task, array($stonework_masonry->getPHID()));
$this->assertEqual(
array(
$stonework_masonry->getPHID(),
),
$this->getTaskProjects($task));
// Adding an ancestor should remove the descendant.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding two tags in the same hierarchy which are not mutual ancestors
// should remove the ancestor but otherwise work fine.
$this->addProjectTags(
$user,
$task,
array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
));
$expect = array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
);
sort($expect);
$this->assertEqual($expect, $this->getTaskProjects($task));
}
public function testTagMilestoneConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_1 = $this->createProject($user, $stonework, true);
$stonework_2 = $this->createProject($user, $stonework, true);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a milesone should remove the parent.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
$this->assertEqual(
array(
$stonework_1->getPHID(),
),
$this->getTaskProjects($task));
// Adding the parent should remove the milestone.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// First, add one milestone.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
// Now, adding a second milestone should remove the first milestone.
$this->addProjectTags($user, $task, array($stonework_2->getPHID()));
$this->assertEqual(
array(
$stonework_2->getPHID(),
),
$this->getTaskProjects($task));
}
public function testBoardMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
$column = $this->addColumn($user, $board, 1);
// New tasks should appear in the backlog.
$task1 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Moving a task should move it to the destination column.
$this->moveToColumn($user, $board, $task1, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Same thing again, with a new task.
$task2 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Move it, too.
$this->moveToColumn($user, $board, $task2, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Now the stuff should be in the column, in order, with the more recently
// moved task on top.
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
// Move the second task after the first task.
$options = array(
'afterPHID' => $task1->getPHID(),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task1->getPHID(),
$task2->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
// Move the second task before the first task.
$options = array(
'beforePHID' => $task1->getPHID(),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
}
public function testMilestoneMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
// Create a task into the backlog.
$task = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
$milestone = $this->createProject($user, $board, true);
$this->addProjectTags($user, $task, array($milestone->getPHID()));
// We just want the side effect of looking at the board: creation of the
// milestone column.
$this->loadColumns($user, $board, $task);
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs(array($board->getPHID()))
->withProxyPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue((bool)$column);
// Moving the task to the milestone should have moved it to the milestone
// column.
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
}
public function testColumnExtendedPolicies() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$column = $this->addColumn($user, $board, 0);
// At first, the user should be able to view and edit the column.
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
// Now, set the project edit policy to "Members of Project". This should
// disable editing.
$members_policy = id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey();
$board->setEditPolicy($members_policy)->save();
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertFalse($can_edit);
// Now, join the project. This should make the column editable again.
$this->joinProject($board, $user);
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
}
private function moveToColumn(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task,
PhabricatorProjectColumn $src,
PhabricatorProjectColumn $dst,
$options = null) {
$xactions = array();
if (!$options) {
$options = array();
}
+ $value = array(
+ 'columnPHID' => $dst->getPHID(),
+ ) + $options;
+
$xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN)
- ->setOldValue(
- array(
- 'projectPHID' => $board->getPHID(),
- 'columnPHIDs' => array($src->getPHID()),
- ))
- ->setNewValue(
- array(
- 'projectPHID' => $board->getPHID(),
- 'columnPHIDs' => array($dst->getPHID()),
- ) + $options);
+ ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
+ ->setNewValue(array($value));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function assertColumns(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$column_phids = $this->loadColumns($viewer, $board, $task);
$this->assertEqual($expect, $column_phids);
}
private function loadColumns(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs(
array(
$task->getPHID(),
))
->executeLayout();
$columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID());
$column_phids = mpull($columns, 'getPHID');
$column_phids = array_values($column_phids);
return $column_phids;
}
private function assertTasksInColumn(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
PhabricatorProjectColumn $column) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs($expect)
->executeLayout();
$object_phids = $engine->getColumnObjectPHIDs(
$board->getPHID(),
$column->getPHID());
$object_phids = array_values($object_phids);
$this->assertEqual($expect, $object_phids);
}
private function addColumn(
PhabricatorUser $viewer,
PhabricatorProject $project,
$sequence) {
$project->setHasWorkboard(1)->save();
return PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', ($sequence == 0))
->setProjectPHID($project->getPHID())
->save();
}
private function getTaskProjects(ManiphestTask $task) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
sort($project_phids);
return $project_phids;
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME);
$xaction->setNewValue($new_name);
$this->applyTransactions($proj, $user, array($xaction));
return true;
}
private function addProjectTags(
PhabricatorUser $viewer,
ManiphestTask $task,
array $phids) {
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'+' => array_fuse($phids),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function newTask(
PhabricatorUser $viewer,
array $projects,
$name = null) {
$task = ManiphestTask::initializeNewTask($viewer);
if (!strlen($name)) {
$name = pht('Test Task');
}
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setNewValue($name);
if ($projects) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse(mpull($projects, 'getPHID')),
));
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
private function assertQueryByProjects(
PhabricatorUser $viewer,
array $expect,
array $projects,
$label = null) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$project_phids = mpull($projects, 'getPHID');
$constraints = $datasource->evaluateTokens($project_phids);
$query = id(new ManiphestTaskQuery())
->setViewer($viewer);
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
$tasks = $query->execute();
$expect_phids = mpull($expect, 'getTitle', 'getPHID');
ksort($expect_phids);
$actual_phids = mpull($tasks, 'getTitle', 'getPHID');
ksort($actual_phids);
$this->assertEqual($expect_phids, $actual_phids, $label);
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false,
$need_watchers = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->needWatchers($need_watchers)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function refreshColumn(
PhabricatorUser $viewer,
PhabricatorProjectColumn $column) {
$results = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($column->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject(
PhabricatorUser $user,
PhabricatorProject $parent = null,
$is_milestone = false) {
$project = PhabricatorProject::initializeNewProject($user);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setNewValue($name);
if ($parent) {
if ($is_milestone) {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
->setNewValue($parent->getPHID());
} else {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
->setNewValue($parent->getPHID());
}
}
$this->applyTransactions($project, $user, $xactions);
// Force these values immediately; they are normally updated by the
// index engine.
if ($parent) {
if ($is_milestone) {
$parent->setHasMilestones(1)->save();
} else {
$parent->setHasSubprojects(1)->save();
}
}
return $project;
}
private function setViewPolicy(
PhabricatorProject $project,
PhabricatorUser $user,
$policy) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($policy);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject($author);
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName(pht('Unit Test User %d', $rand));
return $user;
}
private function joinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '+');
}
private function leaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '-');
}
private function watchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '+');
}
private function unwatchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '-');
}
private function joinOrLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
private function watchOrUnwatchProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
private function applyProjectEdgeTransaction(
PhabricatorProject $project,
PhabricatorUser $user,
$operation,
$edge_type) {
$spec = array(
$operation => array($user->getPHID() => $user->getPHID()),
);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue($spec);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function applyTransactions(
PhabricatorProject $project,
PhabricatorUser $user,
array $xactions) {
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php
index d3540a1781..e529fab61e 100644
--- a/src/applications/project/controller/PhabricatorProjectMoveController.php
+++ b/src/applications/project/controller/PhabricatorProjectMoveController.php
@@ -1,233 +1,229 @@
<?php
final class PhabricatorProjectMoveController
extends PhabricatorProjectController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$request->validateCSRF();
$column_phid = $request->getStr('columnPHID');
$object_phid = $request->getStr('objectPHID');
$after_phid = $request->getStr('afterPHID');
$before_phid = $request->getStr('beforePHID');
$order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->withIDs(array($id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$board_phid = $project->getPHID();
$object = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->needProjectPHIDs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
->execute();
$columns = mpull($columns, null, 'getPHID');
$column = idx($columns, $column_phid);
if (!$column) {
// User is trying to drop this object into a nonexistent column, just kick
// them out.
return new Aphront404Response();
}
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs(array($object_phid))
->executeLayout();
$columns = $engine->getObjectColumns($board_phid, $object_phid);
$old_column_phids = mpull($columns, 'getPHID');
$xactions = array();
+ $order_params = array();
if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
- $order_params = array(
- 'afterPHID' => $after_phid,
- 'beforePHID' => $before_phid,
- );
- } else {
- $order_params = array();
+ if ($after_phid) {
+ $order_params['afterPHID'] = $after_phid;
+ } else if ($before_phid) {
+ $order_params['beforePHID'] = $before_phid;
+ }
}
$xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN)
+ ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(
array(
- 'columnPHIDs' => array($column->getPHID()),
- 'projectPHID' => $column->getProjectPHID(),
- ) + $order_params)
- ->setOldValue(
- array(
- 'columnPHIDs' => $old_column_phids,
- 'projectPHID' => $column->getProjectPHID(),
+ array(
+ 'columnPHID' => $column->getPHID(),
+ ) + $order_params,
));
if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) {
$priority_xactions = $this->getPriorityTransactions(
$object,
$after_phid,
$before_phid);
foreach ($priority_xactions as $xaction) {
$xactions[] = $xaction;
}
}
$proxy = $column->getProxy();
if ($proxy) {
// We're moving the task into a subproject or milestone column, so add
// the subproject or milestone.
$add_projects = array($proxy->getPHID());
} else if ($project->getHasSubprojects() || $project->getHasMilestones()) {
// We're moving the task into the "Backlog" column on the parent project,
// so add the parent explicitly. This gets rid of any subproject or
// milestone tags.
$add_projects = array($project->getPHID());
} else {
$add_projects = array();
}
if ($add_projects) {
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'+' => array_fuse($add_projects),
));
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($object, $xactions);
return $this->newCardResponse($board_phid, $object_phid);
}
private function getPriorityTransactions(
ManiphestTask $task,
$after_phid,
$before_phid) {
list($after_task, $before_task) = $this->loadPriorityTasks(
$after_phid,
$before_phid);
$must_move = false;
if ($after_task && !$task->isLowerPriorityThan($after_task)) {
$must_move = true;
}
if ($before_task && !$task->isHigherPriorityThan($before_task)) {
$must_move = true;
}
// The move doesn't require a priority change to be valid, so don't
// change the priority since we are not being forced to.
if (!$must_move) {
return array();
}
$try = array(
array($after_task, true),
array($before_task, false),
);
$pri = null;
$sub = null;
foreach ($try as $spec) {
list($task, $is_after) = $spec;
if (!$task) {
continue;
}
list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority(
$task,
$is_after);
}
$xactions = array();
if ($pri !== null) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($pri);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue($sub);
}
return $xactions;
}
private function loadPriorityTasks($after_phid, $before_phid) {
$viewer = $this->getViewer();
$task_phids = array();
if ($after_phid) {
$task_phids[] = $after_phid;
}
if ($before_phid) {
$task_phids[] = $before_phid;
}
if (!$task_phids) {
return array(null, null);
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs($task_phids)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
if ($after_phid) {
$after_task = idx($tasks, $after_phid);
} else {
$after_task = null;
}
if ($before_phid) {
$before_task = idx($tasks, $before_phid);
} else {
$before_task = null;
}
return array($after_task, $before_task);
}
}
diff --git a/src/applications/transactions/constants/PhabricatorTransactions.php b/src/applications/transactions/constants/PhabricatorTransactions.php
index 172f80ba31..7d8e8afb1a 100644
--- a/src/applications/transactions/constants/PhabricatorTransactions.php
+++ b/src/applications/transactions/constants/PhabricatorTransactions.php
@@ -1,39 +1,40 @@
<?php
final class PhabricatorTransactions extends Phobject {
const TYPE_COMMENT = 'core:comment';
const TYPE_SUBSCRIBERS = 'core:subscribers';
const TYPE_VIEW_POLICY = 'core:view-policy';
const TYPE_EDIT_POLICY = 'core:edit-policy';
const TYPE_JOIN_POLICY = 'core:join-policy';
const TYPE_EDGE = 'core:edge';
const TYPE_CUSTOMFIELD = 'core:customfield';
const TYPE_BUILDABLE = 'harbormaster:buildable';
const TYPE_TOKEN = 'token:give';
const TYPE_INLINESTATE = 'core:inlinestate';
const TYPE_SPACE = 'core:space';
const TYPE_CREATE = 'core:create';
+ const TYPE_COLUMNS = 'core:columns';
const COLOR_RED = 'red';
const COLOR_ORANGE = 'orange';
const COLOR_YELLOW = 'yellow';
const COLOR_GREEN = 'green';
const COLOR_SKY = 'sky';
const COLOR_BLUE = 'blue';
const COLOR_INDIGO = 'indigo';
const COLOR_VIOLET = 'violet';
const COLOR_GREY = 'grey';
const COLOR_BLACK = 'black';
public static function getInlineStateMap() {
return array(
PhabricatorInlineCommentInterface::STATE_DRAFT =>
PhabricatorInlineCommentInterface::STATE_DONE,
PhabricatorInlineCommentInterface::STATE_UNDRAFT =>
PhabricatorInlineCommentInterface::STATE_UNDONE,
);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Mar 17, 12:31 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
963836
Default Alt Text
(138 KB)

Event Timeline