diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php
index c6e053efc3..f4df0e4fdc 100644
--- a/src/applications/maniphest/constants/ManiphestTaskStatus.php
+++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php
@@ -1,160 +1,211 @@
$open,
self::STATUS_CLOSED_RESOLVED => $resolved,
self::STATUS_CLOSED_WONTFIX => $wontfix,
self::STATUS_CLOSED_INVALID => $invalid,
self::STATUS_CLOSED_DUPLICATE => $duplicate,
self::STATUS_CLOSED_SPITE => $spite,
);
+
+ $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
+ if (!$is_serious) {
+ $statuses[self::STATUS_CLOSED_SPITE] = pht('Spite');
+ }
+
+ return $statuses;
+ }
+
+ public static function getTaskStatusName($status) {
+ return idx(self::getTaskStatusMap(), $status, pht('Unknown Status'));
}
public static function getTaskStatusFullName($status) {
$open = pht('Open');
$resolved = pht('Closed, Resolved');
$wontfix = pht('Closed, Wontfix');
$invalid = pht('Closed, Invalid');
$duplicate = pht('Closed, Duplicate');
$spite = pht('Closed, Spite');
$map = array(
self::STATUS_OPEN => $open,
self::STATUS_CLOSED_RESOLVED => $resolved,
self::STATUS_CLOSED_WONTFIX => $wontfix,
self::STATUS_CLOSED_INVALID => $invalid,
self::STATUS_CLOSED_DUPLICATE => $duplicate,
self::STATUS_CLOSED_SPITE => $spite,
);
return idx($map, $status, '???');
}
public static function getTaskStatusColor($status) {
$default = self::COLOR_STATUS_OPEN;
$map = array(
self::STATUS_OPEN => self::COLOR_STATUS_OPEN,
self::STATUS_CLOSED_RESOLVED => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_WONTFIX => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_INVALID => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_DUPLICATE => self::COLOR_STATUS_CLOSED,
self::STATUS_CLOSED_SPITE => self::COLOR_STATUS_CLOSED,
);
return idx($map, $status, $default);
}
public static function getIcon($status) {
$default = 'oh-open';
$map = array(
self::STATUS_OPEN => 'oh-open',
self::STATUS_CLOSED_RESOLVED => 'oh-closed-dark',
self::STATUS_CLOSED_WONTFIX => 'oh-closed-dark',
self::STATUS_CLOSED_INVALID => 'oh-closed-dark',
self::STATUS_CLOSED_DUPLICATE => 'oh-closed-dark',
self::STATUS_CLOSED_SPITE => 'oh-closed-dark',
);
return idx($map, $status, $default);
}
public static function renderFullDescription($status) {
$color = self::getTaskStatusColor($status);
$img = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_STATUS)
->setSpriteIcon(self::getIcon($status));
$tag = phutil_tag(
'span',
array(
'class' => 'phui-header-'.$color.' plr',
),
array(
$img,
self::getTaskStatusFullName($status),
));
return $tag;
}
public static function getDefaultStatus() {
return self::STATUS_OPEN;
}
+ public static function getDefaultClosedStatus() {
+ return self::STATUS_CLOSED_RESOLVED;
+ }
+
+ public static function getDuplicateStatus() {
+ return self::STATUS_CLOSED_DUPLICATE;
+ }
+
public static function getOpenStatusConstants() {
return array(
self::STATUS_OPEN,
);
}
+ public static function getClosedStatusConstants() {
+ $all = array_keys(self::getTaskStatusMap());
+ $open = self::getOpenStatusConstants();
+ return array_diff($all, $open);
+ }
+
public static function isOpenStatus($status) {
foreach (self::getOpenStatusConstants() as $constant) {
if ($status == $constant) {
return true;
}
}
return false;
}
+ public static function isClosedStatus($status) {
+ return !self::isOpenStatus($status);
+ }
+
+ public static function getStatusActionName($status) {
+ switch ($status) {
+ case self::STATUS_CLOSED_SPITE:
+ return pht('Spited');
+ }
+ return null;
+ }
+
+ public static function getStatusColor($status) {
+ if (self::isOpenStatus($status)) {
+ return 'green';
+ }
+ return 'black';
+ }
+
+ public static function getStatusIcon($status) {
+ switch ($status) {
+ case ManiphestTaskStatus::STATUS_CLOSED_SPITE:
+ return 'dislike';
+ case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE:
+ return 'delete';
+ }
+ }
+
+
public static function getStatusPrefixMap() {
return array(
'resolve' => self::STATUS_CLOSED_RESOLVED,
'resolves' => self::STATUS_CLOSED_RESOLVED,
'resolved' => self::STATUS_CLOSED_RESOLVED,
'fix' => self::STATUS_CLOSED_RESOLVED,
'fixes' => self::STATUS_CLOSED_RESOLVED,
'fixed' => self::STATUS_CLOSED_RESOLVED,
'wontfix' => self::STATUS_CLOSED_WONTFIX,
'wontfixes' => self::STATUS_CLOSED_WONTFIX,
'wontfixed' => self::STATUS_CLOSED_WONTFIX,
'spite' => self::STATUS_CLOSED_SPITE,
'spites' => self::STATUS_CLOSED_SPITE,
'spited' => self::STATUS_CLOSED_SPITE,
'invalidate' => self::STATUS_CLOSED_INVALID,
'invaldiates' => self::STATUS_CLOSED_INVALID,
'invalidated' => self::STATUS_CLOSED_INVALID,
'close' => self::STATUS_CLOSED_RESOLVED,
'closes' => self::STATUS_CLOSED_RESOLVED,
'closed' => self::STATUS_CLOSED_RESOLVED,
'ref' => null,
'refs' => null,
'references' => null,
'cf.' => null,
);
}
public static function getStatusSuffixMap() {
return array(
'as resolved' => self::STATUS_CLOSED_RESOLVED,
'as fixed' => self::STATUS_CLOSED_RESOLVED,
'as wontfix' => self::STATUS_CLOSED_WONTFIX,
'as spite' => self::STATUS_CLOSED_SPITE,
'out of spite' => self::STATUS_CLOSED_SPITE,
'as invalid' => self::STATUS_CLOSED_INVALID,
);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index 8d64cb12d7..b123ae96d9 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,716 +1,703 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$e_title = null;
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$workflow = $request->getStr('workflow');
$parent_task = null;
if ($workflow && is_numeric($workflow)) {
$parent_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($workflow))
->executeOne();
}
$transactions = id(new ManiphestTransactionQuery())
->setViewer($user)
->withObjectPHIDs(array($task->getPHID()))
->needComments(true)
->execute();
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($user)
->readFieldsFromStorage($task);
$e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT;
$e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK;
$e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK;
$e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
$e_mock = PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK;
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes(
array(
$e_commit,
$e_dep_on,
$e_dep_by,
$e_rev,
$e_mock,
));
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
foreach ($task->getCCPHIDs() as $phid) {
$phids[$phid] = true;
}
foreach ($task->getProjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$attached = $task->getAttached();
foreach ($attached as $type => $list) {
foreach ($list as $phid => $info) {
$phids[$phid] = true;
}
}
if ($parent_task) {
$phids[$parent_task->getPHID()] = true;
}
$phids = array_keys($phids);
$this->loadHandles($phids);
$handles = $this->getLoadedHandles();
$context_bar = null;
if ($parent_task) {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?parent='.$parent_task->getID(),
'class' => 'green button',
),
pht('Create Another Subtask')));
$context_bar->appendChild(hsprintf(
'Created a subtask of %s',
$this->getHandle($parent_task->getPHID())->renderLink()));
} else if ($workflow == 'create') {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag('label', array(), 'Create Another'));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?template='.$task->getID(),
'class' => 'green button',
),
pht('Similar Task')));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/',
'class' => 'green button',
),
pht('Empty Task')));
$context_bar->appendChild(pht('New task created.'));
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION);
foreach ($transactions as $modern_xaction) {
if ($modern_xaction->getComment()) {
$engine->addObject(
$modern_xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$resolution_types = ManiphestTaskStatus::getTaskStatusMap();
$transaction_types = array(
PhabricatorTransactions::TYPE_COMMENT => pht('Comment'),
- ManiphestTransaction::TYPE_STATUS => pht('Close Task'),
+ ManiphestTransaction::TYPE_STATUS => pht('Change Status'),
ManiphestTransaction::TYPE_OWNER => pht('Reassign / Claim'),
ManiphestTransaction::TYPE_CCS => pht('Add CCs'),
ManiphestTransaction::TYPE_PRIORITY => pht('Change Priority'),
ManiphestTransaction::TYPE_ATTACH => pht('Upload File'),
ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'),
);
// Remove actions the user doesn't have permission to take.
$requires = array(
ManiphestTransaction::TYPE_OWNER =>
ManiphestCapabilityEditAssign::CAPABILITY,
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestCapabilityEditPriority::CAPABILITY,
ManiphestTransaction::TYPE_PROJECTS =>
ManiphestCapabilityEditProjects::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestCapabilityEditStatus::CAPABILITY,
);
foreach ($transaction_types as $type => $name) {
if (isset($requires[$type])) {
if (!$this->hasApplicationCapability($requires[$type])) {
unset($transaction_types[$type]);
}
}
}
- if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
- $resolution_types = array_select_keys(
- $resolution_types,
- array(
- ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
- ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
- ManiphestTaskStatus::STATUS_CLOSED_INVALID,
- ManiphestTaskStatus::STATUS_CLOSED_SPITE,
- ));
- } else {
- $resolution_types = array(
- ManiphestTaskStatus::STATUS_OPEN => 'Reopened',
- );
- $transaction_types[ManiphestTransaction::TYPE_STATUS] =
- 'Reopen Task';
+ // Don't show an option to change to the current status, or to change to
+ // the duplicate status explicitly.
+ unset($resolution_types[$task->getStatus()]);
+ unset($resolution_types[ManiphestTaskStatus::getDuplicateStatus()]);
+
+ // Don't show owner/priority changes for closed tasks, as they don't make
+ // much sense.
+ if ($task->isClosed()) {
unset($transaction_types[ManiphestTransaction::TYPE_PRIORITY]);
unset($transaction_types[ManiphestTransaction::TYPE_OWNER]);
}
$default_claim = array(
$user->getPHID() => $user->getUsername().' ('.$user->getRealName().')',
);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft_text = $draft->getDraft();
} else {
$draft_text = null;
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
- if ($is_serious) {
- // Prevent tasks from being closed "out of spite" in serious business
- // installs.
- unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]);
- }
-
$comment_form = new AphrontFormView();
$comment_form
->setUser($user)
->setWorkflow(true)
->setAction('/maniphest/transaction/save/')
->setEncType('multipart/form-data')
->addHiddenInput('taskID', $task->getID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Action'))
->setName('action')
->setOptions($transaction_types)
->setID('transaction-action'))
->appendChild(
id(new AphrontFormSelectControl())
- ->setLabel(pht('Resolution'))
+ ->setLabel(pht('Status'))
->setName('resolution')
->setControlID('resolution')
->setControlStyle('display: none')
->setOptions($resolution_types))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assign To'))
->setName('assign_to')
->setControlID('assign_to')
->setControlStyle('display: none')
->setID('assign-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CCs'))
->setName('ccs')
->setControlID('ccs')
->setControlStyle('display: none')
->setID('cc-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setControlID('priority')
->setControlStyle('display: none')
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setControlID('projects')
->setControlStyle('display: none')
->setID('projects-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormFileControl())
->setLabel(pht('File'))
->setName('file')
->setControlID('file')
->setControlStyle('display: none'))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Comments'))
->setName('comments')
->setValue($draft_text)
->setID('transaction-comments')
->setUser($user))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? pht('Submit') : pht('Avast!')));
$control_map = array(
ManiphestTransaction::TYPE_STATUS => 'resolution',
ManiphestTransaction::TYPE_OWNER => 'assign_to',
ManiphestTransaction::TYPE_CCS => 'ccs',
ManiphestTransaction::TYPE_PRIORITY => 'priority',
ManiphestTransaction::TYPE_PROJECTS => 'projects',
ManiphestTransaction::TYPE_ATTACH => 'file',
);
$tokenizer_map = array(
ManiphestTransaction::TYPE_PROJECTS => array(
'id' => 'projects-tokenizer',
'src' => '/typeahead/common/projects/',
'placeholder' => pht('Type a project name...'),
),
ManiphestTransaction::TYPE_OWNER => array(
'id' => 'assign-tokenizer',
'src' => '/typeahead/common/users/',
'value' => $default_claim,
'limit' => 1,
'placeholder' => pht('Type a user name...'),
),
ManiphestTransaction::TYPE_CCS => array(
'id' => 'cc-tokenizer',
'src' => '/typeahead/common/mailable/',
'placeholder' => pht('Type a user or mailing list...'),
),
);
// TODO: Initializing these behaviors for logged out users fatals things.
if ($user->isLoggedIn()) {
Javelin::initBehavior('maniphest-transaction-controls', array(
'select' => 'transaction-action',
'controlMap' => $control_map,
'tokenizers' => $tokenizer_map,
));
Javelin::initBehavior('maniphest-transaction-preview', array(
'uri' => '/maniphest/transaction/preview/'.$task->getID().'/',
'preview' => 'transaction-preview',
'comments' => 'transaction-comments',
'action' => 'transaction-action',
'map' => $control_map,
'tokenizers' => $tokenizer_map,
));
}
$comment_header = $is_serious
? pht('Add Comment')
: pht('Weigh In');
$preview_panel = phutil_tag_div(
'aphront-panel-preview',
phutil_tag(
'div',
array('id' => 'transaction-preview'),
phutil_tag_div(
'aphront-panel-preview-loading-text',
pht('Loading preview...'))));
$timeline = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setObjectPHID($task->getPHID())
->setTransactions($transactions)
->setMarkupEngine($engine);
$object_name = 'T'.$task->getID();
$actions = $this->buildActionView($task);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($object_name, '/'.$object_name)
->setActionList($actions);
$header = $this->buildHeaderView($task);
$properties = $this->buildPropertyView(
$task, $field_list, $edges, $actions);
$description = $this->buildDescriptionView($task, $engine);
if (!$user->isLoggedIn()) {
// TODO: Eventually, everything should run through this. For now, we're
// only using it to get a consistent "Login to Comment" button.
$comment_box = id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setRequestURI($request->getRequestURI());
$preview_panel = null;
} else {
$comment_box = id(new PHUIObjectBoxView())
->setFlush(true)
->setHeaderText($comment_header)
->appendChild($comment_form);
}
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
if ($description) {
$object_box->addPropertyList($description);
}
return $this->buildApplicationPage(
array(
$crumbs,
$context_bar,
$object_box,
$timeline,
$comment_box,
$preview_panel,
),
array(
'title' => 'T'.$task->getID().' '.$task->getTitle(),
'pageObjects' => array($task->getPHID()),
'device' => true,
));
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PHUIHeaderView())
->setHeader($task->getTitle())
->setUser($this->getRequest()->getUser())
->setPolicyObject($task);
$status = $task->getStatus();
$status_name = ManiphestTaskStatus::renderFullDescription($status);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);
return $view;
}
private function buildActionView(ManiphestTask $task) {
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_cc = in_array($viewer_phid, $task->getCCPHIDs());
$id = $task->getID();
$phid = $task->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$task,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($task)
->setObjectURI($this->getRequest()->getRequestURI());
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('edit')
->setHref($this->getApplicationURI("/task/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($task->getOwnerPHID() === $viewer_phid) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Automatically Subscribed'))
->setDisabled(true)
->setIcon('enable'));
} else {
$action = $viewer_is_cc ? 'rem' : 'add';
$name = $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe');
$icon = $viewer_is_cc ? 'disable' : 'check';
$view->addAction(
id(new PhabricatorActionView())
->setName($name)
->setHref("/maniphest/subscribe/{$action}/{$id}/")
->setRenderAsForm(true)
->setUser($viewer)
->setIcon($icon));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Merge Duplicates In'))
->setHref("/search/attach/{$phid}/TASK/merge/")
->setWorkflow(true)
->setIcon('merge')
->setDisabled(!$can_edit)
->setWorkflow(true));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($this->getApplicationURI("/task/create/?parent={$id}"))
->setIcon('fork'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Dependencies'))
->setHref("/search/attach/{$phid}/TASK/dependencies/")
->setWorkflow(true)
->setIcon('link')
->setDisabled(!$can_edit)
->setWorkflow(true));
return $view;
}
private function buildPropertyView(
ManiphestTask $task,
PhabricatorCustomFieldList $field_list,
array $edges,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($task)
->setActionList($actions);
$view->addProperty(
pht('Assigned To'),
$task->getOwnerPHID()
? $this->getHandle($task->getOwnerPHID())->renderLink()
: phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Priority'),
ManiphestTaskPriority::getTaskPriorityName($task->getPriority()));
$handles = $this->getLoadedHandles();
$cc_handles = array_select_keys($handles, $task->getCCPHIDs());
$subscriber_html = id(new SubscriptionListStringBuilder())
->setObjectPHID($task->getPHID())
->setHandles($cc_handles)
->buildPropertyString();
$view->addProperty(pht('Subscribers'), $subscriber_html);
$view->addProperty(
pht('Author'),
$this->getHandle($task->getAuthorPHID())->renderLink());
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject
),
$source));
}
$project_phids = $task->getProjectPHIDs();
if ($project_phids) {
require_celerity_resource('maniphest-task-summary-css');
// If we end up with real-world projects with many hundreds of columns, it
// might be better to just load all the edges, then load those columns and
// work backward that way, or denormalize this data more.
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs($project_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
$column_edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN;
$all_column_phids = array_keys($columns);
$column_edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($task->getPHID()))
->withEdgeTypes(array($column_edge_type))
->withDestinationPHIDs($all_column_phids);
$column_edge_query->execute();
$in_column_phids = array_fuse($column_edge_query->getDestinationPHIDs());
$column_groups = mgroup($columns, 'getProjectPHID');
$project_rows = array();
foreach ($project_phids as $project_phid) {
$row = array();
$handle = $this->getHandle($project_phid);
$row[] = $handle->renderLink();
$columns = idx($column_groups, $project_phid, array());
$column = head(array_intersect_key($columns, $in_column_phids));
if ($column) {
$column_name = pht('(%s)', $column->getDisplayName());
// TODO: This is really hacky but there's no cleaner way to do it
// right now, T4022 should give us better tools for this.
$column_href = str_replace(
'project/view',
'project/board',
$handle->getURI());
$column_link = phutil_tag(
'a',
array(
'href' => $column_href,
'class' => 'maniphest-board-link',
),
$column_name);
$row[] = ' ';
$row[] = $column_link;
}
$project_rows[] = phutil_tag('div', array(), $row);
}
} else {
$project_rows = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Projects'), $project_rows);
$edge_types = array(
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK
=> pht('Dependent Tasks'),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK
=> pht('Depends On'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV
=> pht('Differential Revisions'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK
=> pht('Pholio Mocks'),
);
$revisions_commits = array();
$handles = $this->getLoadedHandles();
$commit_phids = array_keys(
$edges[PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT]);
if ($commit_phids) {
$commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV;
$drev_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($commit_drev))
->execute();
foreach ($commit_phids as $phid) {
$revisions_commits[$phid] = $handles[$phid]->renderLink();
$revision_phid = key($drev_edges[$phid][$commit_drev]);
$revision_handle = idx($handles, $revision_phid);
if ($revision_handle) {
$task_drev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
unset($edges[$task_drev][$revision_phid]);
$revisions_commits[$phid] = hsprintf(
'%s / %s',
$revision_handle->renderLink($revision_handle->getName()),
$revisions_commits[$phid]);
}
}
}
foreach ($edge_types as $edge_type => $edge_name) {
if ($edges[$edge_type]) {
$view->addProperty(
$edge_name,
$this->renderHandlesForPHIDs(array_keys($edges[$edge_type])));
}
}
if ($revisions_commits) {
$view->addProperty(
pht('Commits'),
phutil_implode_html(phutil_tag('br'), $revisions_commits));
}
$attached = $task->getAttached();
if (!is_array($attached)) {
$attached = array();
}
$file_infos = idx($attached, PhabricatorFilePHIDTypeFile::TYPECONST);
if ($file_infos) {
$file_phids = array_keys($file_infos);
// TODO: These should probably be handles or something; clean this up
// as we sort out file attachments.
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$file_view = new PhabricatorFileLinkListView();
$file_view->setFiles($files);
$view->addProperty(
pht('Files'),
$file_view->render());
}
$field_list->appendFieldsToPropertyList(
$task,
$viewer,
$view);
$view->invokeWillRenderEvent();
return $view;
}
private function buildDescriptionView(
ManiphestTask $task,
PhabricatorMarkupEngine $engine) {
$section = null;
if (strlen($task->getDescription())) {
$section = new PHUIPropertyListView();
$section->addSectionHeader(
pht('Description'),
PHUIPropertyListView::ICON_SUMMARY);
$section->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION)));
}
return $section;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
index b44060f461..52420fc394 100644
--- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
+++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
@@ -1,257 +1,257 @@
getRequest();
$user = $request->getUser();
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($request->getStr('taskID')))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$task_uri = '/'.$task->getMonogram();
$transactions = array();
$action = $request->getStr('action');
// If we have drag-and-dropped files, attach them first in a separate
// transaction. These can come in on any transaction type, which is why we
// handle them separately.
$files = array();
// Look for drag-and-drop uploads first.
$file_phids = $request->getArr('files');
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($user)
->withPHIDs(array($file_phids))
->execute();
}
// This means "attach a file" even though we store other types of data
// as 'attached'.
if ($action == ManiphestTransaction::TYPE_ATTACH) {
if (!empty($_FILES['file'])) {
$err = idx($_FILES['file'], 'error');
if ($err != UPLOAD_ERR_NO_FILE) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['file'],
array(
'authorPHID' => $user->getPHID(),
));
$files[] = $file;
}
}
}
// If we had explicit or drag-and-drop files, create a transaction
// for those before we deal with whatever else might have happened.
$file_transaction = null;
if ($files) {
$files = mpull($files, 'getPHID', 'getPHID');
$new = $task->getAttached();
foreach ($files as $phid) {
if (empty($new[PhabricatorFilePHIDTypeFile::TYPECONST])) {
$new[PhabricatorFilePHIDTypeFile::TYPECONST] = array();
}
$new[PhabricatorFilePHIDTypeFile::TYPECONST][$phid] = array();
}
$transaction = new ManiphestTransaction();
$transaction
->setTransactionType(ManiphestTransaction::TYPE_ATTACH);
$transaction->setNewValue($new);
$transactions[] = $transaction;
}
// Compute new CCs added by @mentions. Several things can cause CCs to
// be added as side effects: mentions, explicit CCs, users who aren't
// CC'd interacting with the task, and ownership changes. We build up a
// list of all the CCs and then construct a transaction for them at the
// end if necessary.
$added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array(
$request->getStr('comments'),
));
$cc_transaction = new ManiphestTransaction();
$cc_transaction
->setTransactionType(ManiphestTransaction::TYPE_CCS);
$transaction = new ManiphestTransaction();
$transaction
->setTransactionType($action);
switch ($action) {
case ManiphestTransaction::TYPE_STATUS:
$transaction->setNewValue($request->getStr('resolution'));
break;
case ManiphestTransaction::TYPE_OWNER:
$assign_to = $request->getArr('assign_to');
$assign_to = reset($assign_to);
$transaction->setNewValue($assign_to);
break;
case ManiphestTransaction::TYPE_PROJECTS:
$projects = $request->getArr('projects');
$projects = array_merge($projects, $task->getProjectPHIDs());
$projects = array_filter($projects);
$projects = array_unique($projects);
$transaction->setNewValue($projects);
break;
case ManiphestTransaction::TYPE_CCS:
// Accumulate the new explicit CCs into the array that we'll add in
// the CC transaction later.
$added_ccs = array_merge($added_ccs, $request->getArr('ccs'));
// Throw away the primary transaction.
$transaction = null;
break;
case ManiphestTransaction::TYPE_PRIORITY:
$transaction->setNewValue($request->getInt('priority'));
break;
case ManiphestTransaction::TYPE_ATTACH:
// Nuke this, we created it above.
$transaction = null;
break;
case PhabricatorTransactions::TYPE_COMMENT:
// Nuke this, we're going to create it below.
$transaction = null;
break;
default:
throw new Exception('unknown action');
}
if ($transaction) {
$transactions[] = $transaction;
}
// When you interact with a task, we add you to the CC list so you get
// further updates, and possibly assign the task to you if you took an
// ownership action (closing it) but it's currently unowned. We also move
// previous owners to CC if ownership changes. Detect all these conditions
// and create side-effect transactions for them.
$implicitly_claimed = false;
switch ($action) {
case ManiphestTransaction::TYPE_OWNER:
if ($task->getOwnerPHID() == $transaction->getNewValue()) {
// If this is actually no-op, don't generate the side effect.
break;
}
// Otherwise, when a task is reassigned, move the previous owner to CC.
$added_ccs[] = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
+ $resolution = $request->getStr('resolution');
if (!$task->getOwnerPHID() &&
- $request->getStr('resolution') !=
- ManiphestTaskStatus::STATUS_OPEN) {
+ ManiphestTaskStatus::isClosedStatus($resolution)) {
// Closing an unassigned task. Assign the user as the owner of
// this task.
$assign = new ManiphestTransaction();
$assign->setTransactionType(ManiphestTransaction::TYPE_OWNER);
$assign->setNewValue($user->getPHID());
$transactions[] = $assign;
$implicitly_claimed = true;
}
break;
}
$user_owns_task = false;
if ($implicitly_claimed) {
$user_owns_task = true;
} else {
if ($action == ManiphestTransaction::TYPE_OWNER) {
if ($transaction->getNewValue() == $user->getPHID()) {
$user_owns_task = true;
}
} else if ($task->getOwnerPHID() == $user->getPHID()) {
$user_owns_task = true;
}
}
if (!$user_owns_task) {
// If we aren't making the user the new task owner and they aren't the
// existing task owner, add them to CC unless they're aleady CC'd.
if (!in_array($user->getPHID(), $task->getCCPHIDs())) {
$added_ccs[] = $user->getPHID();
}
}
// Evade no-effect detection in the new editor stuff until we can switch
// to subscriptions.
$added_ccs = array_filter(array_diff($added_ccs, $task->getCCPHIDs()));
if ($added_ccs) {
// We've added CCs, so include a CC transaction.
$all_ccs = array_merge($task->getCCPHIDs(), $added_ccs);
$cc_transaction->setNewValue($all_ccs);
$transactions[] = $cc_transaction;
}
$comments = $request->getStr('comments');
if (strlen($comments) || !$transactions) {
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent($comments));
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect($request->isContinueRequest());
try {
$editor->applyTransactions($task, $transactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($task_uri)
->setException($ex);
}
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft->delete();
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
return id(new AphrontRedirectResponse())->setURI($task_uri);
}
}
diff --git a/src/applications/maniphest/mail/ManiphestReplyHandler.php b/src/applications/maniphest/mail/ManiphestReplyHandler.php
index e83698c976..ccd80bdab3 100644
--- a/src/applications/maniphest/mail/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/mail/ManiphestReplyHandler.php
@@ -1,189 +1,189 @@
getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('T');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return "Reply to comment or attach files, or !close, !claim, ".
"!unsubscribe or !assign .";
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// NOTE: We'll drop in here on both the "reply to a task" and "create a
// new task" workflows! Make sure you test both if you make changes!
$task = $this->getMailReceiver();
$is_new_task = !$task->getID();
$user = $this->getActor();
$body_data = $mail->parseBody();
$body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$template = new ManiphestTransaction();
$is_unsub = false;
if ($is_new_task) {
// If this is a new task, create a "User created this task." transaction
// and then set the title and description.
$xaction = clone $template;
$xaction->setTransactionType(ManiphestTransaction::TYPE_STATUS);
$xaction->setNewValue(ManiphestTaskStatus::getDefaultStatus());
$xactions[] = $xaction;
$task->setAuthorPHID($user->getPHID());
$task->setTitle(nonempty($mail->getSubject(), 'Untitled Task'));
$task->setDescription($body);
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
} else {
$command = $body_data['command'];
$command_value = $body_data['command_value'];
$ttype = PhabricatorTransactions::TYPE_COMMENT;
$new_value = null;
switch ($command) {
case 'close':
$ttype = ManiphestTransaction::TYPE_STATUS;
- $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
+ $new_value = ManiphestTaskStatus::getDefaultClosedStatus();
break;
case 'claim':
$ttype = ManiphestTransaction::TYPE_OWNER;
$new_value = $user->getPHID();
break;
case 'assign':
$ttype = ManiphestTransaction::TYPE_OWNER;
if ($command_value) {
$assign_users = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withUsernames(array($command_value))
->execute();
if ($assign_users) {
$assign_user = head($assign_users);
$new_value = $assign_user->getPHID();
}
}
// assign to the user by default
if (!$new_value) {
$new_value = $user->getPHID();
}
break;
case 'unsubscribe':
$is_unsub = true;
$ttype = ManiphestTransaction::TYPE_CCS;
$ccs = $task->getCCPHIDs();
foreach ($ccs as $k => $phid) {
if ($phid == $user->getPHID()) {
unset($ccs[$k]);
}
}
$new_value = array_values($ccs);
break;
}
if ($ttype != PhabricatorTransactions::TYPE_COMMENT) {
$xaction = clone $template;
$xaction->setTransactionType($ttype);
$xaction->setNewValue($new_value);
$xactions[] = $xaction;
}
if (strlen($body)) {
$xaction = clone $template;
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($body));
$xactions[] = $xaction;
}
}
$ccs = $mail->loadCCPHIDs();
$old_ccs = $task->getCCPHIDs();
$new_ccs = array_merge($old_ccs, $ccs);
if (!$is_unsub) {
$new_ccs[] = $user->getPHID();
}
$new_ccs = array_unique($new_ccs);
if (array_diff($new_ccs, $old_ccs)) {
$cc_xaction = clone $template;
$cc_xaction->setTransactionType(ManiphestTransaction::TYPE_CCS);
$cc_xaction->setNewValue($new_ccs);
$xactions[] = $cc_xaction;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'mail' => $mail,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$xactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setParentMessageID($mail->getMessageID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->applyTransactions($task, $xactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php b/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php
index 99bcd1d8ac..7e1145fbd1 100644
--- a/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php
+++ b/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php
@@ -1,76 +1,76 @@
withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$task = $objects[$phid];
$id = $task->getID();
$title = $task->getTitle();
$handle->setName("T{$id}");
$handle->setFullName("T{$id}: {$title}");
$handle->setURI("/T{$id}");
- if (!ManiphestTaskStatus::isOpenStatus($task->getStatus())) {
+ if ($task->isClosed()) {
$handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED);
}
}
}
public function canLoadNamedObject($name) {
return preg_match('/^T\d*[1-9]\d*$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = (int)substr($name, 1);
$id_map[$id][] = $name;
}
$objects = id(new ManiphestTaskQuery())
->setViewer($query->getViewer())
->withIDs(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
foreach (idx($id_map, $id, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index 740856b4aa..7f791b3661 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,921 +1,927 @@
authorPHIDs = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->taskPHIDs = $phids;
return $this;
}
public function withOwners(array $owners) {
$this->includeUnowned = false;
foreach ($owners as $k => $phid) {
if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS || $phid === null) {
$this->includeUnowned = true;
unset($owners[$k]);
break;
}
}
$this->ownerPHIDs = $owners;
return $this;
}
public function withAllProjects(array $projects) {
$this->includeNoProject = false;
foreach ($projects as $k => $phid) {
if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) {
$this->includeNoProject = true;
unset($projects[$k]);
}
}
$this->projectPHIDs = $projects;
return $this;
}
public function withoutProjects(array $projects) {
$this->xprojectPHIDs = $projects;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withPriorities(array $priorities) {
$this->priorities = $priorities;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
return $this;
}
public function setOrderBy($order) {
$this->orderBy = $order;
return $this;
}
public function withAnyProjects(array $projects) {
$this->anyProjectPHIDs = $projects;
return $this;
}
public function withAnyUserProjects(array $users) {
$this->anyUserProjectPHIDs = $users;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withDateModifiedBefore($date_modified_before) {
$this->dateModifiedBefore = $date_modified_before;
return $this;
}
public function withDateModifiedAfter($date_modified_after) {
$this->dateModifiedAfter = $date_modified_after;
return $this;
}
public function loadPage() {
// TODO: (T603) It is possible for a user to find the PHID of a project
// they can't see, then query for tasks in that project and deduce the
// identity of unknown/invisible projects. Before we allow the user to
// execute a project-based PHID query, we should verify that they
// can see the project.
$task_dao = new ManiphestTask();
$conn = $task_dao->establishConnection('r');
$where = array();
$where[] = $this->buildTaskIDsWhereClause($conn);
$where[] = $this->buildTaskPHIDsWhereClause($conn);
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildStatusesWhereClause($conn);
$where[] = $this->buildPrioritiesWhereClause($conn);
$where[] = $this->buildAuthorWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
$where[] = $this->buildSubscriberWhereClause($conn);
$where[] = $this->buildProjectWhereClause($conn);
$where[] = $this->buildAnyProjectWhereClause($conn);
$where[] = $this->buildAnyUserProjectWhereClause($conn);
$where[] = $this->buildXProjectWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->dateModifiedAfter) {
$where[] = qsprintf(
$conn,
'dateModified >= %d',
$this->dateModifiedAfter);
}
if ($this->dateModifiedBefore) {
$where[] = qsprintf(
$conn,
'dateModified <= %d',
$this->dateModifiedBefore);
}
$where[] = $this->buildPagingClause($conn);
$where = $this->formatWhereClause($where);
$having = '';
$count = '';
if (count($this->projectPHIDs) > 1) {
// We want to treat the query as an intersection query, not a union
// query. We sum the project count and require it be the same as the
// number of projects we're searching for.
$count = ', COUNT(project.projectPHID) projectCount';
$having = qsprintf(
$conn,
'HAVING projectCount = %d',
count($this->projectPHIDs));
}
$order = $this->buildCustomOrderClause($conn);
// TODO: Clean up this nonstandardness.
if (!$this->getLimit()) {
$this->setLimit(self::DEFAULT_PAGE_SIZE);
}
$group_column = '';
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$group_column = qsprintf(
$conn,
', projectGroupName.indexedObjectPHID projectGroupPHID');
break;
}
$rows = queryfx_all(
$conn,
'SELECT task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
$count,
$group_column,
$task_dao->getTableName(),
$this->buildJoinsClause($conn),
$where,
$this->buildGroupClause($conn),
$having,
$order,
$this->buildLimitClause($conn));
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$data = ipull($rows, null, 'id');
break;
default:
$data = $rows;
break;
}
$tasks = $task_dao->loadAllFromArray($data);
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$results = array();
foreach ($rows as $row) {
$task = clone $tasks[$row['id']];
$task->attachGroupByProjectPHID($row['projectGroupPHID']);
$results[] = $task;
}
$tasks = $results;
break;
}
return $tasks;
}
protected function willFilterPage(array $tasks) {
if ($this->groupBy == self::GROUP_PROJECT) {
// We should only return project groups which the user can actually see.
$project_phids = mpull($tasks, 'getGroupByProjectPHID');
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($tasks as $key => $task) {
if (empty($projects[$task->getGroupByProjectPHID()])) {
unset($tasks[$key]);
}
}
}
return $tasks;
}
private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskIDs) {
return null;
}
return qsprintf(
$conn,
'id in (%Ld)',
$this->taskIDs);
}
private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskPHIDs) {
return null;
}
return qsprintf(
$conn,
'phid in (%Ls)',
$this->taskPHIDs);
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
- return 'status = 0';
+ return qsprintf(
+ $conn,
+ 'status IN (%Ld)',
+ ManiphestTaskStatus::getOpenStatusConstants());
case self::STATUS_CLOSED:
- return 'status > 0';
+ return qsprintf(
+ $conn,
+ 'status IN (%Ld)',
+ ManiphestTaskStatus::getClosedStatusConstants());
default:
$constant = idx($map, $this->status);
if (!$constant) {
throw new Exception("Unknown status query '{$this->status}'!");
}
return qsprintf(
$conn,
'status = %d',
$constant);
}
}
private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
if ($this->statuses) {
return qsprintf(
$conn,
'status IN (%Ld)',
$this->statuses);
}
return null;
}
private function buildPrioritiesWhereClause(AphrontDatabaseConnection $conn) {
if ($this->priorities) {
return qsprintf(
$conn,
'priority IN (%Ld)',
$this->priorities);
}
return null;
}
private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->authorPHIDs) {
return null;
}
return qsprintf(
$conn,
'authorPHID in (%Ls)',
$this->authorPHIDs);
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->ownerPHIDs) {
if ($this->includeUnowned === null) {
return null;
} else if ($this->includeUnowned) {
return qsprintf(
$conn,
'ownerPHID IS NULL');
} else {
return qsprintf(
$conn,
'ownerPHID IS NOT NULL');
}
}
if ($this->includeUnowned) {
return qsprintf(
$conn,
'ownerPHID IN (%Ls) OR ownerPHID IS NULL',
$this->ownerPHIDs);
} else {
return qsprintf(
$conn,
'ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
}
private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
if (!strlen($this->fullTextSearch)) {
return null;
}
// In doing a fulltext search, we first find all the PHIDs that match the
// fulltext search, and then use that to limit the rest of the search
$fulltext_query = id(new PhabricatorSavedQuery())
->setEngineClassName('PhabricatorSearchApplicaionSearchEngine')
->setParameter('query', $this->fullTextSearch);
// NOTE: Setting this to something larger than 2^53 will raise errors in
// ElasticSearch, and billions of results won't fit in memory anyway.
$fulltext_query->setParameter('limit', 100000);
$fulltext_query->setParameter('type', ManiphestPHIDTypeTask::TYPECONST);
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$fulltext_results = $engine->executeSearch($fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
}
return qsprintf(
$conn,
'phid IN (%Ls)',
$fulltext_results);
}
private function buildSubscriberWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->subscriberPHIDs) {
return null;
}
return qsprintf(
$conn,
'subscriber.subscriberPHID IN (%Ls)',
$this->subscriberPHIDs);
}
private function buildProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->projectPHIDs && !$this->includeNoProject) {
return null;
}
$parts = array();
if ($this->projectPHIDs) {
$parts[] = qsprintf(
$conn,
'project.projectPHID in (%Ls)',
$this->projectPHIDs);
}
if ($this->includeNoProject) {
$parts[] = qsprintf(
$conn,
'project.projectPHID IS NULL');
}
return '('.implode(') OR (', $parts).')';
}
private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->anyProjectPHIDs) {
return null;
}
return qsprintf(
$conn,
'anyproject.projectPHID IN (%Ls)',
$this->anyProjectPHIDs);
}
private function buildAnyUserProjectWhereClause(
AphrontDatabaseConnection $conn) {
if (!$this->anyUserProjectPHIDs) {
return null;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($this->anyUserProjectPHIDs)
->execute();
$any_user_project_phids = mpull($projects, 'getPHID');
if (!$any_user_project_phids) {
throw new PhabricatorEmptyQueryException();
}
return qsprintf(
$conn,
'anyproject.projectPHID IN (%Ls)',
$any_user_project_phids);
}
private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->xprojectPHIDs) {
return null;
}
return qsprintf(
$conn,
'xproject.projectPHID IS NULL');
}
private function buildCustomOrderClause(AphrontDatabaseConnection $conn) {
$order = array();
switch ($this->groupBy) {
case self::GROUP_NONE:
break;
case self::GROUP_PRIORITY:
$order[] = 'priority';
break;
case self::GROUP_OWNER:
$order[] = 'ownerOrdering';
break;
case self::GROUP_STATUS:
$order[] = 'status';
break;
case self::GROUP_PROJECT:
$order[] = '';
break;
default:
throw new Exception("Unknown group query '{$this->groupBy}'!");
}
switch ($this->orderBy) {
case self::ORDER_PRIORITY:
$order[] = 'priority';
$order[] = 'subpriority';
$order[] = 'dateModified';
break;
case self::ORDER_CREATED:
$order[] = 'id';
break;
case self::ORDER_MODIFIED:
$order[] = 'dateModified';
break;
case self::ORDER_TITLE:
$order[] = 'title';
break;
default:
throw new Exception("Unknown order query '{$this->orderBy}'!");
}
$order = array_unique($order);
if (empty($order)) {
return null;
}
$reverse = ($this->getBeforeID() xor $this->getReversePaging());
foreach ($order as $k => $column) {
switch ($column) {
case 'subpriority':
case 'ownerOrdering':
case 'title':
if ($reverse) {
$order[$k] = "task.{$column} DESC";
} else {
$order[$k] = "task.{$column} ASC";
}
break;
case '':
// Put "No Project" at the end of the list.
if ($reverse) {
$order[$k] =
'projectGroupName.indexedObjectName IS NULL DESC, '.
'projectGroupName.indexedObjectName DESC';
} else {
$order[$k] =
'projectGroupName.indexedObjectName IS NULL ASC, '.
'projectGroupName.indexedObjectName ASC';
}
break;
default:
if ($reverse) {
$order[$k] = "task.{$column} ASC";
} else {
$order[$k] = "task.{$column} DESC";
}
break;
}
}
return 'ORDER BY '.implode(', ', $order);
}
private function buildJoinsClause(AphrontDatabaseConnection $conn_r) {
$project_dao = new ManiphestTaskProject();
$joins = array();
if ($this->projectPHIDs || $this->includeNoProject) {
$joins[] = qsprintf(
$conn_r,
'%Q JOIN %T project ON project.taskPHID = task.phid',
($this->includeNoProject ? 'LEFT' : ''),
$project_dao->getTableName());
}
if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T anyproject ON anyproject.taskPHID = task.phid',
$project_dao->getTableName());
}
if ($this->xprojectPHIDs) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid
AND xproject.projectPHID IN (%Ls)',
$project_dao->getTableName(),
$this->xprojectPHIDs);
}
if ($this->subscriberPHIDs) {
$subscriber_dao = new ManiphestTaskSubscriber();
$joins[] = qsprintf(
$conn_r,
'JOIN %T subscriber ON subscriber.taskPHID = task.phid',
$subscriber_dao->getTableName());
}
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
if ($ignore_group_phids) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.taskPHID
AND projectGroup.projectPHID NOT IN (%Ls)',
$project_dao->getTableName(),
$ignore_group_phids);
} else {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.taskPHID',
$project_dao->getTableName());
}
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroupName
ON projectGroup.projectPHID = projectGroupName.indexedObjectPHID',
id(new ManiphestNameIndex())->getTableName());
break;
}
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
return implode(' ', $joins);
}
private function buildGroupClause(AphrontDatabaseConnection $conn_r) {
$joined_multiple_rows = (count($this->projectPHIDs) > 1) ||
(count($this->anyProjectPHIDs) > 1) ||
($this->getApplicationSearchMayJoinMultipleRows());
$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
// If we're joining multiple rows, we need to group the results by the
// task IDs.
if ($joined_multiple_rows) {
if ($joined_project_name) {
return 'GROUP BY task.phid, projectGroup.projectPHID';
} else {
return 'GROUP BY task.phid';
}
} else {
return '';
}
}
/**
* Return project PHIDs which we should ignore when grouping tasks by
* project. For example, if a user issues a query like:
*
* Tasks in all projects: Frontend, Bugs
*
* ...then we don't show "Frontend" or "Bugs" groups in the result set, since
* they're meaningless as all results are in both groups.
*
* Similarly, for queries like:
*
* Tasks in any projects: Public Relations
*
* ...we ignore the single project, as every result is in that project. (In
* the case that there are several "any" projects, we do not ignore them.)
*
* @return list Project PHIDs which should be ignored in query
* construction.
*/
private function getIgnoreGroupedProjectPHIDs() {
$phids = array();
if ($this->projectPHIDs) {
$phids[] = $this->projectPHIDs;
}
if (count($this->anyProjectPHIDs) == 1) {
$phids[] = $this->anyProjectPHIDs;
}
// Maybe we should also exclude the "excludeProjectPHIDs"? It won't
// impact the results, but we might end up with a better query plan.
// Investigate this on real data? This is likely very rare.
return array_mergev($phids);
}
private function loadCursorObject($id) {
$results = id(new ManiphestTaskQuery())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$id))
->execute();
return head($results);
}
protected function getPagingValue($result) {
$id = $result->getID();
switch ($this->groupBy) {
case self::GROUP_NONE:
return $id;
case self::GROUP_PRIORITY:
return $id.'.'.$result->getPriority();
case self::GROUP_OWNER:
return rtrim($id.'.'.$result->getOwnerPHID(), '.');
case self::GROUP_STATUS:
return $id.'.'.$result->getStatus();
case self::GROUP_PROJECT:
return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
default:
throw new Exception("Unknown group query '{$this->groupBy}'!");
}
}
protected function buildPagingClause(AphrontDatabaseConnection $conn_r) {
$default = parent::buildPagingClause($conn_r);
$before_id = $this->getBeforeID();
$after_id = $this->getAfterID();
if (!$before_id && !$after_id) {
return $default;
}
$cursor_id = nonempty($before_id, $after_id);
$cursor_parts = explode('.', $cursor_id, 2);
$task_id = $cursor_parts[0];
$group_id = idx($cursor_parts, 1);
$cursor = $this->loadCursorObject($task_id);
if (!$cursor) {
return null;
}
$columns = array();
switch ($this->groupBy) {
case self::GROUP_NONE:
break;
case self::GROUP_PRIORITY:
$columns[] = array(
'name' => 'task.priority',
'value' => (int)$group_id,
'type' => 'int',
);
break;
case self::GROUP_OWNER:
$columns[] = array(
'name' => '(task.ownerOrdering IS NULL)',
'value' => (int)(strlen($group_id) ? 0 : 1),
'type' => 'int',
);
if ($group_id) {
$paging_users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_id))
->execute();
if (!$paging_users) {
return null;
}
$columns[] = array(
'name' => 'task.ownerOrdering',
'value' => head($paging_users)->getUsername(),
'type' => 'string',
'reverse' => true,
);
}
break;
case self::GROUP_STATUS:
$columns[] = array(
'name' => 'task.status',
'value' => (int)$group_id,
'type' => 'int',
);
break;
case self::GROUP_PROJECT:
$columns[] = array(
'name' => '(projectGroupName.indexedObjectName IS NULL)',
'value' => (int)(strlen($group_id) ? 0 : 1),
'type' => 'int',
);
if ($group_id) {
$paging_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_id))
->execute();
if (!$paging_projects) {
return null;
}
$columns[] = array(
'name' => 'projectGroupName.indexedObjectName',
'value' => head($paging_projects)->getName(),
'type' => 'string',
'reverse' => true,
);
}
break;
default:
throw new Exception("Unknown group query '{$this->groupBy}'!");
}
switch ($this->orderBy) {
case self::ORDER_PRIORITY:
if ($this->groupBy != self::GROUP_PRIORITY) {
$columns[] = array(
'name' => 'task.priority',
'value' => (int)$cursor->getPriority(),
'type' => 'int',
);
}
$columns[] = array(
'name' => 'task.subpriority',
'value' => (int)$cursor->getSubpriority(),
'type' => 'int',
'reverse' => true,
);
$columns[] = array(
'name' => 'task.dateModified',
'value' => (int)$cursor->getDateModified(),
'type' => 'int',
);
break;
case self::ORDER_CREATED:
$columns[] = array(
'name' => 'task.id',
'value' => (int)$cursor->getID(),
'type' => 'int',
);
break;
case self::ORDER_MODIFIED:
$columns[] = array(
'name' => 'task.dateModified',
'value' => (int)$cursor->getDateModified(),
'type' => 'int',
);
break;
case self::ORDER_TITLE:
$columns[] = array(
'name' => 'task.title',
'value' => $cursor->getTitle(),
'type' => 'string',
);
$columns[] = array(
'name' => 'task.id',
'value' => $cursor->getID(),
'type' => 'int',
);
break;
default:
throw new Exception("Unknown order query '{$this->orderBy}'!");
}
return $this->buildPagingClauseFromMultipleColumns(
$conn_r,
$columns,
array(
'reversed' => (bool)($before_id xor $this->getReversePaging()),
));
}
protected function getApplicationSearchObjectPHIDColumn() {
return 'task.phid';
}
public function getQueryApplicationClass() {
return 'PhabricatorApplicationManiphest';
}
}
diff --git a/src/applications/maniphest/search/ManiphestSearchIndexer.php b/src/applications/maniphest/search/ManiphestSearchIndexer.php
index 41e699d762..9e88ccb267 100644
--- a/src/applications/maniphest/search/ManiphestSearchIndexer.php
+++ b/src/applications/maniphest/search/ManiphestSearchIndexer.php
@@ -1,86 +1,86 @@
loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($task->getPHID());
$doc->setDocumentType(ManiphestPHIDTypeTask::TYPECONST);
$doc->setDocumentTitle($task->getTitle());
$doc->setDocumentCreated($task->getDateCreated());
$doc->setDocumentModified($task->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$task->getDescription());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$task->getAuthorPHID(),
PhabricatorPeoplePHIDTypeUser::TYPECONST,
$task->getDateCreated());
$doc->addRelationship(
- (ManiphestTaskStatus::isOpenStatus($task->getStatus()))
- ? PhabricatorSearchRelationship::RELATIONSHIP_OPEN
- : PhabricatorSearchRelationship::RELATIONSHIP_CLOSED,
+ $task->isClosed()
+ ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED
+ : PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$task->getPHID(),
ManiphestPHIDTypeTask::TYPECONST,
time());
$this->indexTransactions(
$doc,
new ManiphestTransactionQuery(),
array($phid));
foreach ($task->getProjectPHIDs() as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
$phid,
PhabricatorProjectPHIDTypeProject::TYPECONST,
$task->getDateModified()); // Bogus.
}
$owner = $task->getOwnerPHID();
if ($owner) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$owner,
PhabricatorPeoplePHIDTypeUser::TYPECONST,
time());
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
$task->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_VOID,
$task->getDateCreated());
}
// We need to load handles here since non-users may subscribe (mailing
// lists, e.g.)
$ccs = $task->getCCPHIDs();
$handles = id(new PhabricatorHandleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($ccs)
->execute();
foreach ($ccs as $cc) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$handles[$cc]->getPHID(),
$handles[$cc]->getType(),
time());
}
return $doc;
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index f8d2e9b645..0ad93dc04e 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,281 +1,285 @@
setViewer($actor)
->withClasses(array('PhabricatorApplicationManiphest'))
->executeOne();
$view_policy = $app->getPolicy(ManiphestCapabilityDefaultView::CAPABILITY);
$edit_policy = $app->getPolicy(ManiphestCapabilityDefaultEdit::CAPABILITY);
return id(new ManiphestTask())
->setStatus(ManiphestTaskStatus::getDefaultStatus())
->setPriority(ManiphestTaskPriority::getDefaultPriority())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'ccPHIDs' => self::SERIALIZATION_JSON,
'attached' => self::SERIALIZATION_JSON,
'projectPHIDs' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestPHIDTypeTask::TYPECONST);
}
public function getCCPHIDs() {
return array_values(nonempty($this->ccPHIDs, array()));
}
public function setProjectPHIDs(array $phids) {
$this->projectPHIDs = array_values($phids);
$this->projectsNeedUpdate = true;
return $this;
}
public function getProjectPHIDs() {
return array_values(nonempty($this->projectPHIDs, array()));
}
public function setCCPHIDs(array $phids) {
$this->ccPHIDs = array_values($phids);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function getMonogram() {
return 'T'.$this->getID();
}
public function attachGroupByProjectPHID($phid) {
$this->groupByProjectPHID = $phid;
return $this;
}
public function getGroupByProjectPHID() {
return $this->assertAttached($this->groupByProjectPHID);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
if ($this->projectsNeedUpdate) {
// If we've changed the project PHIDs for this task, update the link
// table.
ManiphestTaskProject::updateTaskProjects($this);
$this->projectsNeedUpdate = false;
}
if ($this->subscribersNeedUpdate) {
// If we've changed the subscriber PHIDs for this task, update the link
// table.
ManiphestTaskSubscriber::updateTaskSubscribers($this);
$this->subscribersNeedUpdate = false;
}
return $result;
}
+ public function isClosed() {
+ return ManiphestTaskStatus::isClosedStatus($this->getStatus());
+ }
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "maniphest:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// The owner of a task can always view and edit it.
$owner_phid = $this->getOwnerPHID();
if ($owner_phid) {
$user_phid = $user->getPHID();
if ($user_phid == $owner_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht(
'The owner of a task can always view and edit it.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
+
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index f708fa843a..f6617530e9 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -1,728 +1,747 @@
getTransactionType()) {
case self::TYPE_PROJECT_COLUMN:
case self::TYPE_EDGE:
return false;
}
return parent::shouldGenerateOldValue();
}
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_CCS:
case self::TYPE_PROJECTS:
$phids = array_mergev(
array(
$phids,
nonempty($old, array()),
nonempty($new, array()),
));
break;
case self::TYPE_PROJECT_COLUMN:
$phids[] = $new['projectPHID'];
$phids[] = head($new['columnPHIDs']);
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;
}
return $phids;
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
case self::TYPE_DESCRIPTION:
case self::TYPE_PRIORITY:
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case self::TYPE_SUBPRIORITY:
return true;
}
return parent::shouldHide();
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
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:
- if ($new == ManiphestTaskStatus::STATUS_OPEN) {
- return 'green';
- } else {
- return 'black';
+ $color = ManiphestTaskStatus::getStatusColor($new);
+ if ($color !== null) {
+ return $color;
}
+ break;
case self::TYPE_PRIORITY:
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'green';
} else if ($old > $new) {
return 'grey';
} else {
return 'yellow';
}
}
return parent::getColor();
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
return pht('Retitled');
case self::TYPE_STATUS:
- switch ($new) {
- case ManiphestTaskStatus::STATUS_OPEN:
- if ($old === null) {
- return pht('Created');
- } else {
- return pht('Reopened');
- }
- case ManiphestTaskStatus::STATUS_CLOSED_SPITE:
- return pht('Spited');
- case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE:
- return pht('Merged');
- default:
- return pht('Closed');
+ if ($old === null) {
+ return pht('Created');
+ }
+
+ $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('Up For Grabs');
} else if (!$old) {
return pht('Assigned');
} else {
return pht('Reassigned');
}
case self::TYPE_CCS:
return pht('Changed CC');
case self::TYPE_PROJECTS:
return pht('Changed Projects');
case self::TYPE_PROJECT_COLUMN:
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');
}
return parent::getActionName();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
return 'user';
case self::TYPE_CCS:
return 'meta-mta';
case self::TYPE_TITLE:
return 'edit';
case self::TYPE_STATUS:
- switch ($new) {
- case ManiphestTaskStatus::STATUS_OPEN:
- return 'create';
- case ManiphestTaskStatus::STATUS_CLOSED_SPITE:
- return 'dislike';
- case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE:
- return 'delete';
- default:
- return 'check';
+ if ($old === null) {
+ return 'create';
+ }
+
+ $action = ManiphestTaskStatus::getStatusIcon($new);
+ if ($action !== null) {
+ return $action;
+ }
+
+ if (ManiphestTaskStatus::isClosedStatus($new)) {
+ return 'check';
+ } else {
+ return 'edit';
}
case self::TYPE_DESCRIPTION:
return 'edit';
case self::TYPE_PROJECTS:
return 'project';
case self::TYPE_PROJECT_COLUMN:
return 'workboard';
case self::TYPE_PRIORITY:
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'normal-priority';
return pht('Triaged');
} else if ($old > $new) {
return 'lower-priority';
} else {
return 'raise-priority';
}
case self::TYPE_EDGE:
case self::TYPE_ATTACH:
return 'attach';
}
return parent::getIcon();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
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:
- switch ($new) {
- case ManiphestTaskStatus::STATUS_OPEN:
- if ($old === null) {
- return pht(
- '%s created this task.',
- $this->renderHandleLink($author_phid));
- } else {
- return pht(
- '%s reopened this task.',
- $this->renderHandleLink($author_phid));
- }
-
- case ManiphestTaskStatus::STATUS_CLOSED_SPITE:
- return pht(
- '%s closed this task out of spite.',
- $this->renderHandleLink($author_phid));
- case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE:
+ if ($old === null) {
+ return pht(
+ '%s created this task.',
+ $this->renderHandleLink($author_phid));
+ }
+
+ $old_closed = ManiphestTaskStatus::isClosedStatus($old);
+ $new_closed = ManiphestTaskStatus::isClosedStatus($new);
+
+ $old_name = ManiphestTaskStatus::getTaskStatusName($old);
+ $new_name = ManiphestTaskStatus::getTaskStatusName($new);
+
+ if ($new_closed && !$old_closed) {
+ if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
return pht(
'%s closed this task as a duplicate.',
$this->renderHandleLink($author_phid));
- default:
- $status_name = idx(
- ManiphestTaskStatus::getTaskStatusMap(),
- $new,
- '???');
+ } else {
return pht(
'%s closed this task as "%s".',
$this->renderHandleLink($author_phid),
- $status_name);
+ $new_name);
+ }
+ } else if (!$new_closed && $old_closed) {
+ return pht(
+ '%s reopened this task as "%s".',
+ $this->renderHandleLink($author_phid),
+ $new_name);
+ } else {
+ return pht(
+ '%s changed the task status from "%s" to "%s".',
+ $this->renderHandleLink($author_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 placed this task up for grabs.',
$this->renderHandleLink($author_phid));
} 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_PROJECTS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
if ($added && !$removed) {
return pht(
'%s added %d project(s): %s',
$this->renderHandleLink($author_phid),
count($added),
$this->renderHandleList($added));
} else if ($removed && !$added) {
return pht(
'%s removed %d project(s): %s',
$this->renderHandleLink($author_phid),
count($removed),
$this->renderHandleList($removed));
} else if ($removed && $added) {
return pht(
'%s changed project(s), added %d: %s; removed %d: %s',
$this->renderHandleLink($author_phid),
count($added),
$this->renderHandleList($added),
count($removed),
$this->renderHandleList($removed));
} else {
// This is hit when rendering previews.
return pht(
'%s changed projects...',
$this->renderHandleLink($author_phid));
}
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_CCS:
// TODO: Remove this when we switch to subscribers. Just reuse the
// code in the parent.
$clone = clone $this;
$clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
return $clone->getTitle();
case self::TYPE_EDGE:
// TODO: Remove this when we switch to real edges. Just reuse the
// code in the parent;
$clone = clone $this;
$clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE);
return $clone->getTitle();
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): %s',
$this->renderHandleLink($author_phid),
count($added),
$this->renderHandleList($added));
} else if ($removed && !$added) {
return pht(
'%s detached %d file(s): %s',
$this->renderHandleLink($author_phid),
count($removed),
$this->renderHandleList($removed));
} else {
return pht(
'%s changed file(s), attached %d: %s; detached %d: %s',
$this->renderHandleLink($author_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 this task to %s on the %s workboard.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($column_phid),
$this->renderHandleLink($project_phid));
break;
}
return parent::getTitle();
}
public function getTitleForFeed(PhabricatorFeedStory $story) {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
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:
- switch ($new) {
- case ManiphestTaskStatus::STATUS_OPEN:
- if ($old === null) {
- return pht(
- '%s created %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink($object_phid));
- } else {
- return pht(
- '%s reopened %s.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink($object_phid));
- }
-
- case ManiphestTaskStatus::STATUS_CLOSED_SPITE:
- return pht(
- '%s closed %s out of spite.',
- $this->renderHandleLink($author_phid),
- $this->renderHandleLink($object_phid));
- case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE:
+ if ($old === null) {
+ return pht(
+ '%s created %s.',
+ $this->renderHandleLink($author_phid),
+ $this->renderHandleLink($object_phid));
+ }
+
+ $old_closed = ManiphestTaskStatus::isClosedStatus($old);
+ $new_closed = ManiphestTaskStatus::isClosedStatus($new);
+
+ $old_name = ManiphestTaskStatus::getTaskStatusName($old);
+ $new_name = ManiphestTaskStatus::getTaskStatusName($new);
+
+ if ($new_closed && !$old_closed) {
+ if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
return pht(
'%s closed %s as a duplicate.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
- default:
- $status_name = idx(
- ManiphestTaskStatus::getTaskStatusMap(),
- $new,
- '???');
+ } else {
return pht(
'%s closed %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
- $status_name);
+ $new_name);
+ }
+ } else if (!$new_closed && $old_closed) {
+ return pht(
+ '%s reopened %s as "%s".',
+ $this->renderHandleLink($author_phid),
+ $this->renderHandleLink($object_phid),
+ $new_name);
+ } 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_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_PROJECTS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
if ($added && !$removed) {
return pht(
'%s added %d project(s) to %s: %s',
$this->renderHandleLink($author_phid),
count($added),
$this->renderHandleLink($object_phid),
$this->renderHandleList($added));
} else if ($removed && !$added) {
return pht(
'%s removed %d project(s) from %s: %s',
$this->renderHandleLink($author_phid),
count($removed),
$this->renderHandleLink($object_phid),
$this->renderHandleList($removed));
} else if ($removed && $added) {
return pht(
'%s changed project(s) of %s, added %d: %s; removed %d: %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
count($added),
$this->renderHandleList($added),
count($removed),
$this->renderHandleList($removed));
}
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_CCS:
// TODO: Remove this when we switch to subscribers. Just reuse the
// code in the parent.
$clone = clone $this;
$clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
return $clone->getTitleForFeed($story);
case self::TYPE_EDGE:
// TODO: Remove this when we switch to real edges. Just reuse the
// code in the parent;
$clone = clone $this;
$clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE);
return $clone->getTitleForFeed($story);
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));
break;
}
return parent::getTitleForFeed($story);
}
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_STATUS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS;
break;
case self::TYPE_OWNER:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER;
break;
case self::TYPE_CCS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC;
break;
case self::TYPE_PROJECTS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS;
break;
case self::TYPE_PRIORITY:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY;
break;
case PhabricatorTransactions::TYPE_COMMENT:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT;
break;
default:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_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_PROJECTS:
return pht('The task is already associated with those projects.');
case self::TYPE_PRIORITY:
return pht('The task already has the selected priority.');
}
return parent::getNoEffectDescription();
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php
index 7d63cddddb..16c5be5dfb 100644
--- a/src/applications/maniphest/view/ManiphestTaskListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskListView.php
@@ -1,131 +1,131 @@
tasks = $tasks;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function setShowSubpriorityControls($show_subpriority_controls) {
$this->showSubpriorityControls = $show_subpriority_controls;
return $this;
}
public function render() {
$handles = $this->handles;
$list = new PHUIObjectItemListView();
$list->setCards(true);
$list->setFlush(true);
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$color_map = ManiphestTaskPriority::getColorMap();
if ($this->showBatchControls) {
Javelin::initBehavior('maniphest-list-editor');
}
foreach ($this->tasks as $task) {
$item = new PHUIObjectItemView();
$item->setObjectName('T'.$task->getID());
$item->setHeader($task->getTitle());
$item->setHref('/T'.$task->getID());
if ($task->getOwnerPHID()) {
$owner = $handles[$task->getOwnerPHID()];
$item->addByline(pht('Assigned: %s', $owner->renderLink()));
}
$status = $task->getStatus();
- if ($status != ManiphestTaskStatus::STATUS_OPEN) {
+ if ($task->isClosed()) {
$item->setDisabled(true);
}
$item->setBarColor(idx($color_map, $task->getPriority(), 'grey'));
$item->addIcon(
'none',
phabricator_datetime($task->getDateModified(), $this->getUser()));
if ($this->showSubpriorityControls) {
$item->setGrippable(true);
}
if ($this->showSubpriorityControls || $this->showBatchControls) {
$item->addSigil('maniphest-task');
}
$projects_view = new ManiphestTaskProjectsView();
$projects_view->setHandles(
array_select_keys(
$handles,
$task->getProjectPHIDs()));
$item->addAttribute($projects_view);
$item->setMetadata(
array(
'taskID' => $task->getID(),
));
if ($this->showBatchControls) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('edit')
->addSigil('maniphest-edit-task')
->setHref('/maniphest/task/edit/'.$task->getID().'/'));
}
$list->addItem($item);
}
return $list;
}
public static function loadTaskHandles(
PhabricatorUser $viewer,
array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$phids = array();
foreach ($tasks as $task) {
$assigned_phid = $task->getOwnerPHID();
if ($assigned_phid) {
$phids[] = $assigned_phid;
}
foreach ($task->getProjectPHIDs() as $project_phid) {
$phids[] = $project_phid;
}
}
if (!$phids) {
return array();
}
return id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
}
}
diff --git a/src/applications/maniphest/view/ManiphestView.php b/src/applications/maniphest/view/ManiphestView.php
index e15765507f..ec121f86dd 100644
--- a/src/applications/maniphest/view/ManiphestView.php
+++ b/src/applications/maniphest/view/ManiphestView.php
@@ -1,17 +1,14 @@
getStatus();
$status_name = ManiphestTaskStatus::getTaskStatusFullName($status);
return id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setName($status_name);
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php
index a2ec3b2fb1..2155d41db5 100644
--- a/src/applications/search/controller/PhabricatorSearchAttachController.php
+++ b/src/applications/search/controller/PhabricatorSearchAttachController.php
@@ -1,337 +1,337 @@
phid = $data['phid'];
$this->type = $data['type'];
$this->action = idx($data, 'action', self::ACTION_ATTACH);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($this->phid))
->executeOne();
$object_type = $handle->getType();
$attach_type = $this->type;
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($this->phid))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$edge_type = null;
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_DEPENDENCIES:
case self::ACTION_ATTACH:
$edge_type = $this->getEdgeType($object_type, $attach_type);
break;
}
if ($request->isFormPost()) {
$phids = explode(';', $request->getStr('phids'));
$phids = array_filter($phids);
$phids = array_values($phids);
if ($edge_type) {
$do_txn = $object instanceof PhabricatorApplicationTransactionInterface;
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
$add_phids = $phids;
$rem_phids = array_diff($old_phids, $add_phids);
if ($do_txn) {
$txn_editor = $object->getApplicationTransactionEditor()
->setActor($user)
->setContentSourceFromRequest($request);
$txn_template = $object->getApplicationTransactionObject()
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array(
'+' => array_fuse($add_phids),
'-' => array_fuse($rem_phids)));
$txn_editor->applyTransactions($object, array($txn_template));
} else {
$editor = id(new PhabricatorEdgeEditor());
$editor->setActor($user);
foreach ($add_phids as $phid) {
$editor->addEdge($this->phid, $edge_type, $phid);
}
foreach ($rem_phids as $phid) {
$editor->removeEdge($this->phid, $edge_type, $phid);
}
try {
$editor->save();
} catch (PhabricatorEdgeCycleException $ex) {
$this->raiseGraphCycleException($ex);
}
}
return id(new AphrontReloadResponse())->setURI($handle->getURI());
} else {
return $this->performMerge($object, $handle, $phids);
}
} else {
if ($edge_type) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
} else {
// This is a merge.
$phids = array();
}
}
$strings = $this->getStrings();
$handles = $this->loadViewerHandles($phids);
$obj_dialog = new PhabricatorObjectSelectorDialog();
$obj_dialog
->setUser($user)
->setHandles($handles)
->setFilters($this->getFilters($strings))
->setSelectedFilter($strings['selected'])
->setExcluded($this->phid)
->setCancelURI($handle->getURI())
->setSearchURI('/search/select/'.$attach_type.'/')
->setTitle($strings['title'])
->setHeader($strings['header'])
->setButtonText($strings['button'])
->setInstructions($strings['instructions']);
$dialog = $obj_dialog->buildDialog();
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function performMerge(
ManiphestTask $task,
PhabricatorObjectHandle $handle,
array $phids) {
$user = $this->getRequest()->getUser();
$response = id(new AphrontReloadResponse())->setURI($handle->getURI());
$phids = array_fill_keys($phids, true);
unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
if (!$phids) {
return $response;
}
$targets = id(new ManiphestTaskQuery())
->setViewer($user)
->withPHIDs(array_keys($phids))
->execute();
if (empty($targets)) {
return $response;
}
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($this->getRequest())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$task_names = array();
$merge_into_name = 'T'.$task->getID();
$cc_vector = array();
$cc_vector[] = $task->getCCPHIDs();
foreach ($targets as $target) {
$cc_vector[] = $target->getCCPHIDs();
$cc_vector[] = array(
$target->getAuthorPHID(),
$target->getOwnerPHID());
$close_task = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_STATUS)
- ->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE);
+ ->setNewValue(ManiphestTaskStatus::getDuplicateStatus());
$merge_comment = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent("\xE2\x9C\x98 Merged into {$merge_into_name}."));
$editor->applyTransactions(
$target,
array(
$close_task,
$merge_comment,
));
$task_names[] = 'T'.$target->getID();
}
$all_ccs = array_mergev($cc_vector);
$all_ccs = array_filter($all_ccs);
$all_ccs = array_unique($all_ccs);
$task_names = implode(', ', $task_names);
$add_ccs = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_CCS)
->setNewValue($all_ccs);
$merged_comment = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent("\xE2\x97\x80 Merged tasks: {$task_names}."));
$editor->applyTransactions($task, array($add_ccs, $merged_comment));
return $response;
}
private function getStrings() {
switch ($this->type) {
case DifferentialPHIDTypeRevision::TYPECONST:
$noun = 'Revisions';
$selected = 'created';
break;
case ManiphestPHIDTypeTask::TYPECONST:
$noun = 'Tasks';
$selected = 'assigned';
break;
case PhabricatorRepositoryPHIDTypeCommit::TYPECONST:
$noun = 'Commits';
$selected = 'created';
break;
case PholioPHIDTypeMock::TYPECONST:
$noun = 'Mocks';
$selected = 'created';
break;
}
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_ATTACH:
$dialog_title = "Manage Attached {$noun}";
$header_text = "Currently Attached {$noun}";
$button_text = "Save {$noun}";
$instructions = null;
break;
case self::ACTION_MERGE:
$dialog_title = "Merge Duplicate Tasks";
$header_text = "Tasks To Merge";
$button_text = "Merge {$noun}";
$instructions =
"These tasks will be merged into the current task and then closed. ".
"The current task will grow stronger.";
break;
case self::ACTION_DEPENDENCIES:
$dialog_title = "Edit Dependencies";
$header_text = "Current Dependencies";
$button_text = "Save Dependencies";
$instructions = null;
break;
}
return array(
'target_plural_noun' => $noun,
'selected' => $selected,
'title' => $dialog_title,
'header' => $header_text,
'button' => $button_text,
'instructions' => $instructions,
);
}
private function getFilters(array $strings) {
if ($this->type == PholioPHIDTypeMock::TYPECONST) {
$filters = array(
'created' => 'Created By Me',
'all' => 'All '.$strings['target_plural_noun'],
);
} else {
$filters = array(
'assigned' => 'Assigned to Me',
'created' => 'Created By Me',
'open' => 'All Open '.$strings['target_plural_noun'],
'all' => 'All '.$strings['target_plural_noun'],
);
}
return $filters;
}
private function getEdgeType($src_type, $dst_type) {
$t_cmit = PhabricatorRepositoryPHIDTypeCommit::TYPECONST;
$t_task = ManiphestPHIDTypeTask::TYPECONST;
$t_drev = DifferentialPHIDTypeRevision::TYPECONST;
$t_mock = PholioPHIDTypeMock::TYPECONST;
$map = array(
$t_cmit => array(
$t_task => PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK,
),
$t_task => array(
$t_cmit => PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$t_drev => PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV,
$t_mock => PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK,
),
$t_drev => array(
$t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
$t_task => PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
),
$t_mock => array(
$t_task => PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK,
),
);
if (empty($map[$src_type][$dst_type])) {
return null;
}
return $map[$src_type][$dst_type];
}
private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) {
$cycle = $ex->getCycle();
$handles = $this->loadViewerHandles($cycle);
$names = array();
foreach ($cycle as $cycle_phid) {
$names[] = $handles[$cycle_phid]->getFullName();
}
$names = implode(" \xE2\x86\x92 ", $names);
throw new Exception(
"You can not create that dependency, because it would create a ".
"circular dependency: {$names}.");
}
}
diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
index 538a1f944e..9345607939 100644
--- a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
@@ -1,156 +1,156 @@
/**
* @provides javelin-behavior-maniphest-batch-editor
* @requires javelin-behavior
* javelin-dom
* javelin-util
* phabricator-prefab
* multirow-row-manager
* javelin-json
*/
JX.behavior('maniphest-batch-editor', function(config) {
var root = JX.$(config.root);
var editor_table = JX.DOM.find(root, 'table', 'maniphest-batch-actions');
var manager = new JX.MultirowRowManager(editor_table);
var action_rows = [];
addRow({});
function renderRow(data) {
var action_select = JX.Prefab.renderSelect(
{
'add_project': 'Add Projects',
'remove_project' : 'Remove Projects',
'priority': 'Change Priority',
- 'status': 'Open / Close',
+ 'status': 'Change Status',
'add_comment': 'Comment',
'assign': 'Assign',
'add_ccs' : 'Add CCs',
'remove_ccs' : 'Remove CCs'
});
var proj_tokenizer = build_tokenizer(config.sources.project);
var owner_tokenizer = build_tokenizer(config.sources.owner);
var cc_tokenizer = build_tokenizer(config.sources.cc);
var priority_select = JX.Prefab.renderSelect(config.priorityMap);
var status_select = JX.Prefab.renderSelect(config.statusMap);
var comment_input = JX.$N('input', {style: {width: '100%'}});
var cell = JX.$N('td', {className: 'batch-editor-input'});
var vfunc = null;
function update() {
switch (action_select.value) {
case 'add_project':
case 'remove_project':
JX.DOM.setContent(cell, proj_tokenizer.template);
vfunc = function() {
return JX.keys(proj_tokenizer.object.getTokens());
};
break;
case 'add_ccs':
case 'remove_ccs':
JX.DOM.setContent(cell, cc_tokenizer.template);
vfunc = function() {
return JX.keys(cc_tokenizer.object.getTokens());
};
break;
case 'assign':
JX.DOM.setContent(cell, owner_tokenizer.template);
vfunc = function() {
return JX.keys(owner_tokenizer.object.getTokens());
};
break;
case 'add_comment':
JX.DOM.setContent(cell, comment_input);
vfunc = function() {
return comment_input.value;
};
break;
case 'priority':
JX.DOM.setContent(cell, priority_select);
vfunc = function() { return priority_select.value; };
break;
case 'status':
JX.DOM.setContent(cell, status_select);
vfunc = function() { return status_select.value; };
break;
}
}
JX.DOM.listen(action_select, 'change', null, update);
update();
return {
nodes : [JX.$N('td', {}, action_select), cell],
dataCallback : function() {
return {
action: action_select.value,
value: vfunc()
};
}
};
}
function onaddaction(e) {
e.kill();
addRow({});
}
function addRow(info) {
var data = renderRow(info);
var row = manager.addRow(data.nodes);
var id = manager.getRowID(row);
action_rows[id] = data.dataCallback;
}
function onsubmit(e) {
var input = JX.$(config.input);
var actions = [];
for (var k in action_rows) {
actions.push(action_rows[k]());
}
input.value = JX.JSON.stringify(actions);
}
JX.DOM.listen(
root,
'click',
'add-action',
onaddaction);
JX.DOM.listen(
root,
'submit',
null,
onsubmit);
manager.listen(
'row-removed',
function(row_id) {
delete action_rows[row_id];
});
function build_tokenizer(tconfig) {
var template = JX.$N('div', JX.$H(config.tokenizerTemplate)).firstChild;
template.id = '';
var build_config = JX.copy({}, tconfig);
build_config.root = template;
var built = JX.Prefab.buildTokenizer(build_config);
built.tokenizer.start();
return {
object: built.tokenizer,
template: template
};
}
});