Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index a4086281dd..6438c47d93 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,736 +1,749 @@
<?php
final class ManiphestTaskEditController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$response_type = $request->getStr('responseType', 'task');
$order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
$can_edit_assign = $this->hasApplicationCapability(
ManiphestEditAssignCapability::CAPABILITY);
$can_edit_policies = $this->hasApplicationCapability(
ManiphestEditPoliciesCapability::CAPABILITY);
$can_edit_priority = $this->hasApplicationCapability(
ManiphestEditPriorityCapability::CAPABILITY);
$can_edit_projects = $this->hasApplicationCapability(
ManiphestEditProjectsCapability::CAPABILITY);
$can_edit_status = $this->hasApplicationCapability(
ManiphestEditStatusCapability::CAPABILITY);
$parent_task = null;
$template_id = null;
if ($this->id) {
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($this->id))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
} else {
$task = ManiphestTask::initializeNewTask($user);
// We currently do not allow you to set the task status when creating
// a new task, although now that statuses are custom it might make
// sense.
$can_edit_status = false;
// These allow task creation with defaults.
if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title'));
if ($can_edit_projects) {
$projects = $request->getStr('projects');
if ($projects) {
$tokens = $request->getStrList('projects');
$type_project = PhabricatorProjectProjectPHIDType::TYPECONST;
foreach ($tokens as $key => $token) {
if (phid_get_type($token) == $type_project) {
// If this is formatted like a PHID, leave it as-is.
continue;
}
if (preg_match('/^#/', $token)) {
// If this already has a "#", leave it as-is.
continue;
}
// Add a "#" prefix.
$tokens[$key] = '#'.$token;
}
$default_projects = id(new PhabricatorObjectQuery())
->setViewer($user)
->withNames($tokens)
->execute();
$default_projects = mpull($default_projects, 'getPHID');
if ($default_projects) {
$task->attachProjectPHIDs($default_projects);
}
}
}
if ($can_edit_priority) {
$priority = $request->getInt('priority');
if ($priority !== null) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if (isset($priority_map[$priority])) {
$task->setPriority($priority);
}
}
}
$task->setDescription($request->getStr('description'));
if ($can_edit_assign) {
$assign = $request->getStr('assign');
if (strlen($assign)) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withUsernames(array($assign))
->executeOne();
if (!$assign_user) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withPHIDs(array($assign))
->executeOne();
}
if ($assign_user) {
$task->setOwnerPHID($assign_user->getPHID());
}
}
}
}
$template_id = $request->getInt('template');
// You can only have a parent task if you're creating a new task.
$parent_id = $request->getInt('parent');
if ($parent_id) {
$parent_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($parent_id))
->executeOne();
if (!$template_id) {
$template_id = $parent_id;
}
}
}
$errors = array();
$e_title = true;
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($user);
$field_list->readFieldsFromStorage($task);
$aux_fields = $field_list->getFields();
if ($request->isFormPost()) {
$changes = array();
$new_title = $request->getStr('title');
$new_desc = $request->getStr('description');
$new_status = $request->getStr('status');
if (!$task->getID()) {
$workflow = 'create';
} else {
$workflow = '';
}
$changes[ManiphestTransaction::TYPE_TITLE] = $new_title;
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc;
if ($can_edit_status) {
$changes[ManiphestTransaction::TYPE_STATUS] = $new_status;
} else if (!$task->getID()) {
// Create an initial status transaction for the burndown chart.
// TODO: We can probably remove this once Facts comes online.
$changes[ManiphestTransaction::TYPE_STATUS] = $task->getStatus();
}
$owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer);
if (!strlen($new_title)) {
$e_title = pht('Required');
$errors[] = pht('Title is required.');
}
$old_values = array();
foreach ($aux_fields as $aux_arr_key => $aux_field) {
// TODO: This should be buildFieldTransactionsFromRequest() once we
// switch to ApplicationTransactions properly.
$aux_old_value = $aux_field->getOldValueForApplicationTransactions();
$aux_field->readValueFromRequest($request);
$aux_new_value = $aux_field->getNewValueForApplicationTransactions();
// TODO: We're faking a call to the ApplicaitonTransaction validation
// logic here. We need valid objects to pass, but they aren't used
// in a meaningful way. For now, build User objects. Once the Maniphest
// objects exist, this will switch over automatically. This is a big
// hack but shouldn't be long for this world.
$placeholder_editor = new PhabricatorUserProfileEditor();
$field_errors = $aux_field->validateApplicationTransactions(
$placeholder_editor,
PhabricatorTransactions::TYPE_CUSTOMFIELD,
array(
id(new ManiphestTransaction())
->setOldValue($aux_old_value)
->setNewValue($aux_new_value),
));
foreach ($field_errors as $error) {
$errors[] = $error->getMessage();
}
$old_values[$aux_field->getFieldKey()] = $aux_old_value;
}
if ($errors) {
$task->setTitle($new_title);
$task->setDescription($new_desc);
$task->setPriority($request->getInt('priority'));
$task->setOwnerPHID($owner_phid);
$task->setCCPHIDs($request->getArr('cc'));
$task->attachProjectPHIDs($request->getArr('projects'));
} else {
if ($can_edit_priority) {
$changes[ManiphestTransaction::TYPE_PRIORITY] =
$request->getInt('priority');
}
if ($can_edit_assign) {
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
}
$changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc');
if ($can_edit_projects) {
$projects = $request->getArr('projects');
$changes[ManiphestTransaction::TYPE_PROJECTS] =
$projects;
$column_phid = $request->getStr('columnPHID');
// allow for putting a task in a project column at creation -only-
if (!$task->getID() && $column_phid && $projects) {
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs($projects)
->withPHIDs(array($column_phid))
->executeOne();
if ($column) {
$changes[ManiphestTransaction::TYPE_PROJECT_COLUMN] =
array(
'new' => array(
'projectPHID' => $column->getProjectPHID(),
'columnPHIDs' => array($column_phid)),
'old' => array(
'projectPHID' => $column->getProjectPHID(),
'columnPHIDs' => array()));
}
}
}
if ($can_edit_policies) {
$changes[PhabricatorTransactions::TYPE_VIEW_POLICY] =
$request->getStr('viewPolicy');
$changes[PhabricatorTransactions::TYPE_EDIT_POLICY] =
$request->getStr('editPolicy');
}
$template = new ManiphestTransaction();
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
if ($type == ManiphestTransaction::TYPE_PROJECT_COLUMN) {
$transaction->setNewValue($value['new']);
$transaction->setOldValue($value['old']);
} else if ($type == ManiphestTransaction::TYPE_PROJECTS) {
// TODO: Gross.
$project_type =
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$transaction
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($value),
));
} else {
$transaction->setNewValue($value);
}
$transactions[] = $transaction;
}
if ($aux_fields) {
foreach ($aux_fields as $aux_field) {
$transaction = clone $template;
$transaction->setTransactionType(
PhabricatorTransactions::TYPE_CUSTOMFIELD);
$aux_key = $aux_field->getFieldKey();
$transaction->setMetadataValue('customfield:key', $aux_key);
$old = idx($old_values, $aux_key);
$new = $aux_field->getNewValueForApplicationTransactions();
$transaction->setOldValue($old);
$transaction->setNewValue($new);
$transactions[] = $transaction;
}
}
if ($transactions) {
$is_new = !$task->getID();
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
if ($parent_task) {
// TODO: This should be transactional now.
id(new PhabricatorEdgeEditor())
->addEdge(
$parent_task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$task->getPHID())
->save();
$workflow = $parent_task->getID();
}
if ($request->isAjax()) {
switch ($response_type) {
case 'card':
$owner = null;
if ($task->getOwnerPHID()) {
$owner = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($task->getOwnerPHID()))
->executeOne();
}
$tasks = id(new ProjectBoardTaskCard())
->setViewer($user)
->setTask($task)
->setOwner($owner)
->setCanEdit(true)
->getItem();
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withPHIDs(array($request->getStr('columnPHID')))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($user)
->withColumns(array($column))
->execute();
$task_phids = mpull($positions, 'getObjectPHID');
$column_tasks = id(new ManiphestTaskQuery())
->setViewer($user)
->withPHIDs($task_phids)
->execute();
- $sort_map = mpull(
- $column_tasks,
- 'getPrioritySortVector',
- 'getPHID');
+ if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
+ // TODO: This is a little bit awkward, because PHP and JS use
+ // slightly different sort order parameters to achieve the same
+ // effect. It would be unify this a bit at some point.
+ $sort_map = array();
+ foreach ($positions as $position) {
+ $sort_map[$position->getObjectPHID()] = array(
+ -$position->getSequence(),
+ $position->getID(),
+ );
+ }
+ } else {
+ $sort_map = mpull(
+ $column_tasks,
+ 'getPrioritySortVector',
+ 'getPHID');
+ }
$data = array(
'sortMap' => $sort_map,
);
break;
case 'task':
default:
$tasks = $this->renderSingleTask($task);
$data = array();
break;
}
return id(new AphrontAjaxResponse())->setContent(
array(
'tasks' => $tasks,
'data' => $data,
));
}
$redirect_uri = '/T'.$task->getID();
if ($workflow) {
$redirect_uri .= '?workflow='.$workflow;
}
return id(new AphrontRedirectResponse())
->setURI($redirect_uri);
}
} else {
if (!$task->getID()) {
$task->setCCPHIDs(array(
$user->getPHID(),
));
if ($template_id) {
$template_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($template_id))
->executeOne();
if ($template_task) {
$task->setCCPHIDs($template_task->getCCPHIDs());
$task->attachProjectPHIDs($template_task->getProjectPHIDs());
$task->setOwnerPHID($template_task->getOwnerPHID());
$task->setPriority($template_task->getPriority());
$task->setViewPolicy($template_task->getViewPolicy());
$task->setEditPolicy($template_task->getEditPolicy());
$template_fields = PhabricatorCustomField::getObjectFields(
$template_task,
PhabricatorCustomField::ROLE_EDIT);
$fields = $template_fields->getFields();
foreach ($fields as $key => $field) {
if (!$field->shouldCopyWhenCreatingSimilarTask()) {
unset($fields[$key]);
}
if (empty($aux_fields[$key])) {
unset($fields[$key]);
}
}
if ($fields) {
id(new PhabricatorCustomFieldList($fields))
->setViewer($user)
->readFieldsFromStorage($template_task);
foreach ($fields as $key => $field) {
$aux_fields[$key]->setValueFromStorage(
$field->getValueForStorage());
}
}
}
}
}
}
$phids = array_merge(
array($task->getOwnerPHID()),
$task->getCCPHIDs(),
$task->getProjectPHIDs());
if ($parent_task) {
$phids[] = $parent_task->getPHID();
}
$phids = array_filter($phids);
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
}
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($task->getOwnerPHID()) {
$assigned_value = array($handles[$task->getOwnerPHID()]);
} else {
$assigned_value = array();
}
if ($task->getCCPHIDs()) {
$cc_value = array_select_keys($handles, $task->getCCPHIDs());
} else {
$cc_value = array();
}
if ($task->getProjectPHIDs()) {
$projects_value = array_select_keys($handles, $task->getProjectPHIDs());
} else {
$projects_value = array();
}
$cancel_id = nonempty($task->getID(), $template_id);
if ($cancel_id) {
$cancel_uri = '/T'.$cancel_id;
} else {
$cancel_uri = '/maniphest/';
}
if ($task->getID()) {
$button_name = pht('Save Task');
$header_name = pht('Edit Task');
} else if ($parent_task) {
$cancel_uri = '/T'.$parent_task->getID();
$button_name = pht('Create Task');
$header_name = pht('Create New Subtask');
} else {
$button_name = pht('Create Task');
$header_name = pht('Create New Task');
}
require_celerity_resource('maniphest-task-edit-css');
$project_tokenizer_id = celerity_generate_unique_node_id();
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('template', $template_id)
->addHiddenInput('responseType', $response_type)
->addHiddenInput('order', $order)
->addHiddenInput('columnPHID', $request->getStr('columnPHID'));
if ($parent_task) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Parent Task'))
->setValue($handles[$parent_task->getPHID()]->getFullName()))
->addHiddenInput('parent', $parent_task->getID());
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Title'))
->setName('title')
->setError($e_title)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle()));
if ($can_edit_status) {
// See T4819.
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$dup_status = ManiphestTaskStatus::getDuplicateStatus();
if ($task->getStatus() != $dup_status) {
unset($status_map[$dup_status]);
}
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setValue($task->getStatus())
->setOptions($status_map));
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($task)
->execute();
if ($can_edit_assign) {
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assigned To'))
->setName('assigned_to')
->setValue($assigned_value)
->setUser($user)
->setDatasource(new PhabricatorPeopleDatasource())
->setLimit(1));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CC'))
->setName('cc')
->setValue($cc_value)
->setUser($user)
->setDatasource(new PhabricatorMetaMTAMailableDatasource()));
if ($can_edit_priority) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setValue($task->getPriority()));
}
if ($can_edit_policies) {
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($task)
->setPolicies($policies)
->setName('viewPolicy'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($task)
->setPolicies($policies)
->setName('editPolicy'));
}
if ($can_edit_projects) {
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption(
javelin_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
pht('Create New Project')))
->setDatasource(new PhabricatorProjectDatasource()));
}
$field_list->appendFieldsToForm($form);
require_celerity_resource('aphront-error-view-css');
Javelin::initBehavior('project-create', array(
'tokenizerID' => $project_tokenizer_id,
));
$description_control = new PhabricatorRemarkupControl();
// "Upsell" creating tasks via email in create flows if the instance is
// configured for this awesomeness.
$email_create = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
if (!$task->getID() && $email_create) {
$email_hint = pht(
'You can also create tasks by sending an email to: %s',
phutil_tag('tt', array(), $email_create));
$description_control->setCaption($email_hint);
}
$description_control
->setLabel(pht('Description'))
->setName('description')
->setID('description-textarea')
->setValue($task->getDescription())
->setUser($user);
$form
->appendChild($description_control);
if ($request->isAjax()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_name)
->appendChild(
array(
$error_view,
$form->buildLayoutView(),
))
->addCancelButton($cancel_uri)
->addSubmitButton($button_name);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button_name));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header_name)
->setFormErrors($errors)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview'))
->setControlID('description-textarea')
->setPreviewURI($this->getApplicationURI('task/descriptionpreview/'));
if ($task->getID()) {
$page_objects = array($task->getPHID());
} else {
$page_objects = array();
}
$crumbs = $this->buildApplicationCrumbs();
if ($task->getID()) {
$crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID());
}
$crumbs->addTextCrumb($header_name);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
$preview,
),
array(
'title' => $header_name,
'pageObjects' => $page_objects,
));
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index f4069bf183..aa3e07b4f6 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,538 +1,648 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $heraldEmailPHIDs = array();
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = ManiphestTransaction::TYPE_PRIORITY;
$types[] = ManiphestTransaction::TYPE_STATUS;
$types[] = ManiphestTransaction::TYPE_TITLE;
$types[] = ManiphestTransaction::TYPE_DESCRIPTION;
$types[] = ManiphestTransaction::TYPE_OWNER;
$types[] = ManiphestTransaction::TYPE_CCS;
$types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
$types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
$types[] = ManiphestTransaction::TYPE_UNBLOCK;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getPriority();
case ManiphestTransaction::TYPE_STATUS:
if ($this->getIsNewObject()) {
return null;
}
return $object->getStatus();
case ManiphestTransaction::TYPE_TITLE:
if ($this->getIsNewObject()) {
return null;
}
return $object->getTitle();
case ManiphestTransaction::TYPE_DESCRIPTION:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDescription();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($object->getOwnerPHID(), null);
case ManiphestTransaction::TYPE_CCS:
return array_values(array_unique($object->getCCPHIDs()));
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// These are pre-populated.
return $xaction->getOldValue();
case ManiphestTransaction::TYPE_SUBPRIORITY:
return $object->getSubpriority();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return (int)$xaction->getNewValue();
case ManiphestTransaction::TYPE_CCS:
return array_values(array_unique($xaction->getNewValue()));
case ManiphestTransaction::TYPE_OWNER:
return nonempty($xaction->getNewValue(), null);
case ManiphestTransaction::TYPE_STATUS:
case ManiphestTransaction::TYPE_TITLE:
case ManiphestTransaction::TYPE_DESCRIPTION:
case ManiphestTransaction::TYPE_SUBPRIORITY:
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
case ManiphestTransaction::TYPE_UNBLOCK:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_CCS:
sort($old);
sort($new);
return ($old !== $new);
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$new_column_phids = $new['columnPHIDs'];
$old_column_phids = $old['columnPHIDs'];
sort($new_column_phids);
sort($old_column_phids);
return ($old !== $new);
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return $object->setPriority($xaction->getNewValue());
case ManiphestTransaction::TYPE_STATUS:
return $object->setStatus($xaction->getNewValue());
case ManiphestTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case ManiphestTransaction::TYPE_DESCRIPTION:
return $object->setDescription($xaction->getNewValue());
case ManiphestTransaction::TYPE_OWNER:
$phid = $xaction->getNewValue();
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($phid) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
return $object->setOwnerPHID($phid);
case ManiphestTransaction::TYPE_CCS:
return $object->setCCPHIDs($xaction->getNewValue());
case ManiphestTransaction::TYPE_SUBPRIORITY:
$data = $xaction->getNewValue();
$new_sub = $this->getNextSubpriority(
$data['newPriority'],
$data['newSubpriorityBase'],
$data['direction']);
$object->setSubpriority($new_sub);
return;
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// these do external (edge) updates
return;
}
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_SUBPRIORITY:
$data = $xaction->getNewValue();
$new_pri = $data['newPriority'];
if ($new_pri != $object->getPriority()) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($new_pri);
}
break;
default:
break;
}
return $xactions;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$board_phid = idx($xaction->getNewValue(), 'projectPHID');
if (!$board_phid) {
throw new Exception(
pht("Expected 'projectPHID' in column transaction."));
}
+ $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
$new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
if (count($new_phids) !== 1) {
throw new Exception(
pht("Expected exactly one 'columnPHIDs' in column transaction."));
}
+ $columns = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($this->requireActor())
+ ->withPHIDs($new_phids)
+ ->execute();
+ $columns = mpull($columns, null, 'getPHID');
+
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($this->requireActor())
->withObjectPHIDs(array($object->getPHID()))
->withBoardPHIDs(array($board_phid))
->execute();
+ $before_phid = idx($xaction->getNewValue(), 'beforePHID');
+ $after_phid = idx($xaction->getNewValue(), 'afterPHID');
+
+ if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) {
+ // If we are not moving the object between columns and also not
+ // reordering the position, this is a move on some other order
+ // (like priority). We can leave the positions untouched and just
+ // bail, there's no work to be done.
+ return;
+ }
+
+ // Otherwise, we're either moving between columns or adjusting the
+ // object's position in the "natural" ordering, so we do need to update
+ // some rows.
+
// Remove all existing column positions on the board.
foreach ($positions as $position) {
$position->delete();
}
- // Add the new column position.
+ // Add the new column positions.
foreach ($new_phids as $phid) {
- id(new PhabricatorProjectColumnPosition())
+ $column = idx($columns, $phid);
+ if (!$column) {
+ throw new Exception(
+ pht('No such column "%s" exists!', $phid));
+ }
+
+ // Load the other object positions in the column. Note that we must
+ // skip implicit column creation to avoid generating a new position
+ // if the target column is a backlog column.
+
+ $other_positions = id(new PhabricatorProjectColumnPositionQuery())
+ ->setViewer($this->requireActor())
+ ->withColumns(array($column))
+ ->withBoardPHIDs(array($board_phid))
+ ->setSkipImplicitCreate(true)
+ ->execute();
+ $other_positions = msort($other_positions, 'getOrderingKey');
+
+ // Set up the new position object. We're going to figure out the
+ // right sequence number and then persist this object with that
+ // sequence number.
+ $new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
- ->setColumnPHID($phid)
- ->setObjectPHID($object->getPHID())
- // TODO: Do real sequence stuff.
- ->setSequence(0)
- ->save();
+ ->setColumnPHID($column->getPHID())
+ ->setObjectPHID($object->getPHID());
+
+ $updates = array();
+ $sequence = 0;
+
+ // If we're just dropping this into the column without any specific
+ // position information, put it at the top.
+ if (!$before_phid && !$after_phid) {
+ $new_position->setSequence($sequence)->save();
+ $sequence++;
+ }
+
+ foreach ($other_positions as $position) {
+ $object_phid = $position->getObjectPHID();
+
+ // If this is the object we're moving before and we haven't
+ // saved yet, insert here.
+ if (($before_phid == $object_phid) && !$new_position->getID()) {
+ $new_position->setSequence($sequence)->save();
+ $sequence++;
+ }
+
+ // This object goes here in the sequence; we might need to update
+ // the row.
+ if ($sequence != $position->getSequence()) {
+ $updates[$position->getID()] = $sequence;
+ }
+ $sequence++;
+
+ // If this is the object we're moving after and we haven't saved
+ // yet, insert here.
+ if (($after_phid == $object_phid) && !$new_position->getID()) {
+ $new_position->setSequence($sequence)->save();
+ $sequence++;
+ }
+ }
+
+ // We should have found a place to put it.
+ if (!$new_position->getID()) {
+ throw new Exception(
+ pht('Unable to find a place to insert object on column!'));
+ }
+
+ // If we changed other objects' column positions, bulk reorder them.
+
+ if ($updates) {
+ $position = new PhabricatorProjectColumnPosition();
+ $conn_w = $position->establishConnection('w');
+
+ $pairs = array();
+ foreach ($updates as $id => $sequence) {
+ // This is ugly because MySQL gets upset with us if it is
+ // configured strictly and we attempt inserts which can't work.
+ // We'll never actually do these inserts since they'll always
+ // collide (triggering the ON DUPLICATE KEY logic), so we just
+ // provide dummy values in order to get there.
+
+ $pairs[] = qsprintf(
+ $conn_w,
+ '(%d, %d, "", "", "")',
+ $id,
+ $sequence);
+ }
+
+ queryfx(
+ $conn_w,
+ 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
+ VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
+ $position->getTableName(),
+ implode(', ', $pairs));
+ }
}
break;
default:
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$unblock_xactions = array();
$unblock_xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
// TODO: We should avoid notifiying users about these indirect
// changes if they are getting a notification about the current
// change, so you don't get a pile of extra notifications if you are
// subscribed to this task.
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, $unblock_xactions);
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = mfilter($xactions, 'shouldHide', true);
return $xactions;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getOwnerPHID(),
$this->requireActor()->getPHID(),
);
}
protected function getMailCC(PhabricatorLiskDAO $object) {
$phids = array();
foreach ($object->getCCPHIDs() as $phid) {
$phids[] = $phid;
}
foreach (parent::getMailCC($object) as $phid) {
$phids[] = $phid;
}
foreach ($this->heraldEmailPHIDs as $phid) {
$phids[] = $phid;
}
return $phids;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}")
->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addTextSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addTextSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
// TODO: Convert these to transactions. The way Maniphest deals with these
// transactions is currently unconventional and messy.
$save_again = false;
$cc_phids = $adapter->getCcPHIDs();
if ($cc_phids) {
$existing_cc = $object->getCCPHIDs();
$new_cc = array_unique(array_merge($cc_phids, $existing_cc));
$object->setCCPHIDs($new_cc);
$object->save();
}
$this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
$xactions = array();
$assign_phid = $adapter->getAssignPHID();
if ($assign_phid) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setNewValue($assign_phid);
}
$project_phids = $adapter->getProjectPHIDs();
if ($project_phids) {
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'+' => array_fuse($project_phids),
));
}
return $xactions;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestEditPriorityCapability::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestEditStatusCapability::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestEditAssignCapability::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = null;
if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$app_capability = ManiphestEditProjectsCapability::CAPABILITY;
break;
}
} else {
$app_capability = idx($app_capability_map, $transaction_type);
}
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_OWNER:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
continue;
}
}
return $copy;
}
private function getNextSubpriority($pri, $sub, $dir = '>') {
switch ($dir) {
case '>':
$order = 'ASC';
break;
case '<':
$order = 'DESC';
break;
default:
throw new Exception('$dir must be ">" or "<".');
break;
}
if ($sub === null) {
$base = 0;
} else {
$base = $sub;
}
if ($sub === null) {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d ORDER BY subpriority %Q LIMIT 1',
$pri,
$order);
if ($next) {
if ($dir == '>') {
return $next->getSubpriority() - ((double)(2 << 16));
} else {
return $next->getSubpriority() + ((double)(2 << 16));
}
}
} else {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d AND subpriority %Q %f ORDER BY subpriority %Q LIMIT 1',
$pri,
$dir,
$sub,
$order);
if ($next) {
return ($sub + $next->getSubpriority()) / 2;
}
}
if ($dir == '>') {
return $base + (double)(2 << 32);
} else {
return $base - (double)(2 << 32);
}
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index 4e297534ed..ac0ada0178 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -1,842 +1,852 @@
<?php
final class ManiphestTransaction
extends PhabricatorApplicationTransaction {
const TYPE_TITLE = 'title';
const TYPE_STATUS = 'status';
const TYPE_DESCRIPTION = 'description';
const TYPE_OWNER = 'reassign';
const TYPE_CCS = 'ccs';
const TYPE_PROJECTS = 'projects';
const TYPE_PRIORITY = 'priority';
const TYPE_EDGE = 'edge';
const TYPE_SUBPRIORITY = 'subpriority';
const TYPE_PROJECT_COLUMN = 'projectcolumn';
const TYPE_UNBLOCK = 'unblock';
// NOTE: this type is deprecated. Keep it around for legacy installs
// so any transactions render correctly.
const TYPE_ATTACH = 'attach';
public function getApplicationName() {
return 'maniphest';
}
public function getApplicationTransactionType() {
return ManiphestTaskPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new ManiphestTransactionComment();
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
case self::TYPE_PROJECT_COLUMN:
case self::TYPE_EDGE:
case self::TYPE_UNBLOCK:
return false;
}
return parent::shouldGenerateOldValue();
}
public function getRemarkupBlocks() {
$blocks = parent::getRemarkupBlocks();
switch ($this->getTransactionType()) {
case self::TYPE_DESCRIPTION:
$blocks[] = $this->getNewValue();
break;
}
return $blocks;
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$new = $this->getNewValue();
$old = $this->getOldValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
if ($new) {
$phids[] = $new;
}
if ($old) {
$phids[] = $old;
}
break;
case self::TYPE_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;
case self::TYPE_UNBLOCK:
foreach (array_keys($new) as $phid) {
$phids[] = $phid;
}
break;
}
return $phids;
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case self::TYPE_DESCRIPTION:
case self::TYPE_PRIORITY:
case self::TYPE_STATUS:
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case self::TYPE_SUBPRIORITY:
return true;
+ case self::TYPE_PROJECT_COLUMN:
+ $old_cols = idx($this->getOldValue(), 'columnPHIDs');
+ $new_cols = idx($this->getNewValue(), 'columnPHIDs');
+
+ $old_cols = array_values($old_cols);
+ $new_cols = array_values($new_cols);
+ sort($old_cols);
+ sort($new_cols);
+
+ return ($old_cols === $new_cols);
}
return parent::shouldHide();
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
return 1.4;
case self::TYPE_STATUS:
return 1.3;
case self::TYPE_OWNER:
return 1.2;
case self::TYPE_PRIORITY:
return 1.1;
}
return parent::getActionStrength();
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
if ($this->getAuthorPHID() == $new) {
return 'green';
} else if (!$new) {
return 'black';
} else if (!$old) {
return 'green';
} else {
return 'green';
}
case self::TYPE_STATUS:
$color = ManiphestTaskStatus::getStatusColor($new);
if ($color !== null) {
return $color;
}
if (ManiphestTaskStatus::isOpenStatus($new)) {
return 'green';
} else {
return 'black';
}
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:
if ($old === null) {
return pht('Created');
}
return pht('Retitled');
case self::TYPE_STATUS:
$action = ManiphestTaskStatus::getStatusActionName($new);
if ($action) {
return $action;
}
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
if ($new_closed && !$old_closed) {
return pht('Closed');
} else if (!$new_closed && $old_closed) {
return pht('Reopened');
} else {
return pht('Changed Status');
}
case self::TYPE_DESCRIPTION:
return pht('Edited');
case self::TYPE_OWNER:
if ($this->getAuthorPHID() == $new) {
return pht('Claimed');
} else if (!$new) {
return pht('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');
case self::TYPE_UNBLOCK:
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
if ($old_closed && !$new_closed) {
return pht('Block');
} else if (!$old_closed && $new_closed) {
return pht('Unblock');
} else {
return pht('Blocker');
}
}
return parent::getActionName();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_OWNER:
return 'fa-user';
case self::TYPE_CCS:
return 'fa-envelope';
case self::TYPE_TITLE:
if ($old === null) {
return 'fa-pencil';
}
return 'fa-pencil';
case self::TYPE_STATUS:
$action = ManiphestTaskStatus::getStatusIcon($new);
if ($action !== null) {
return $action;
}
if (ManiphestTaskStatus::isClosedStatus($new)) {
return 'fa-check';
} else {
return 'fa-pencil';
}
case self::TYPE_DESCRIPTION:
return 'fa-pencil';
case self::TYPE_PROJECTS:
return 'fa-briefcase';
case self::TYPE_PROJECT_COLUMN:
return 'fa-columns';
case self::TYPE_PRIORITY:
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'fa-arrow-right';
} else if ($old > $new) {
return 'fa-arrow-down';
} else {
return 'fa-arrow-up';
}
case self::TYPE_EDGE:
case self::TYPE_ATTACH:
return 'fa-thumb-tack';
case self::TYPE_UNBLOCK:
return 'fa-shield';
}
return parent::getIcon();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return pht(
'%s created this task.',
$this->renderHandleLink($author_phid));
}
return pht(
'%s changed the title from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_DESCRIPTION:
return pht(
'%s edited the task description.',
$this->renderHandleLink($author_phid));
case self::TYPE_STATUS:
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
$old_name = ManiphestTaskStatus::getTaskStatusName($old);
$new_name = ManiphestTaskStatus::getTaskStatusName($new);
if ($new_closed && !$old_closed) {
if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
return pht(
'%s closed this task as a duplicate.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s closed this task as "%s".',
$this->renderHandleLink($author_phid),
$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_UNBLOCK:
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($old_closed && !$new_closed) {
return pht(
'%s reopened blocking task %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$new_name);
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed blocking task %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$new_name);
} else {
return pht(
'%s changed the status of blocking task %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($blocker_phid),
$old_name,
$new_name);
}
case self::TYPE_OWNER:
if ($author_phid == $new) {
return pht(
'%s claimed this task.',
$this->renderHandleLink($author_phid));
} else if (!$new) {
return pht(
'%s 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:
if ($old === null) {
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
return pht(
'%s renamed %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old,
$new);
case self::TYPE_DESCRIPTION:
return pht(
'%s edited the description of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case self::TYPE_STATUS:
$old_closed = ManiphestTaskStatus::isClosedStatus($old);
$new_closed = ManiphestTaskStatus::isClosedStatus($new);
$old_name = ManiphestTaskStatus::getTaskStatusName($old);
$new_name = ManiphestTaskStatus::getTaskStatusName($new);
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));
} else {
return pht(
'%s closed %s as "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$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_UNBLOCK:
// TODO: We should probably not show these in feed; they're highly
// redundant. For now, just use the normal titles. Right now, we can't
// publish something to noficiations without also publishing it to feed.
// Fix that, then stop these from rendering in feed only.
break;
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 PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS;
break;
default:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER;
break;
}
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/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
index 25ffbeae37..66cb1b5de4 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -1,554 +1,571 @@
<?php
final class PhabricatorProjectBoardViewController
extends PhabricatorProjectBoardController {
private $id;
private $slug;
private $handles;
private $queryKey;
private $filter;
private $sortKey;
private $showHidden;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->slug = idx($data, 'slug');
$this->queryKey = idx($data, 'queryKey');
$this->filter = (bool)idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$show_hidden = $request->getBool('hidden');
$this->showHidden = $show_hidden;
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needImages(true);
if ($this->slug) {
$project->withSlugs(array($this->slug));
} else {
$project->withIDs(array($this->id));
}
$project = $project->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$this->id = $project->getID();
$sort_key = $request->getStr('order');
switch ($sort_key) {
case PhabricatorProjectColumn::ORDER_NATURAL:
case PhabricatorProjectColumn::ORDER_PRIORITY:
break;
default:
$sort_key = PhabricatorProjectColumn::DEFAULT_ORDER;
break;
}
$this->sortKey = $sort_key;
$column_query = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()));
if (!$show_hidden) {
$column_query->withStatuses(
array(PhabricatorProjectColumn::STATUS_ACTIVE));
}
$columns = $column_query->execute();
$columns = mpull($columns, null, 'getSequence');
if (empty($columns[0])) {
switch ($request->getStr('initialize-type')) {
case 'backlog-only':
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProjectPHID($project->getPHID())
->save();
$column->attachProject($project);
$columns[0] = $column;
unset($unguarded);
break;
case 'import':
return id(new AphrontRedirectResponse())
->setURI(
$this->getApplicationURI('board/'.$project->getID().'/import/'));
break;
default:
return $this->initializeWorkboardDialog($project);
break;
}
}
ksort($columns);
$board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
$engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer)
->setBaseURI($board_uri)
->setIsBoardView(true);
if ($request->isFormPost()) {
$saved = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved);
return id(new AphrontRedirectResponse())->setURI(
$this->getURIWithState(
$engine->getQueryResultsPageURI($saved->getQueryKey())));
}
$query_key = $this->queryKey;
if (!$query_key) {
$query_key = 'open';
}
$this->queryKey = $query_key;
$custom_query = null;
if ($engine->isBuiltinQuery($query_key)) {
$saved = $engine->buildSavedQueryFromBuiltin($query_key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
return new Aphront404Response();
}
$custom_query = $saved;
}
if ($this->filter) {
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$engine->buildSearchForm($filter_form, $saved);
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
$task_query = $engine->buildQueryFromSavedQuery($saved);
$tasks = $task_query
->addWithAllProjects(array($project->getPHID()))
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->setViewer($viewer)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
if ($tasks) {
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($viewer)
->withObjectPHIDs(mpull($tasks, 'getPHID'))
->withColumns($columns)
->execute();
$positions = mpull($positions, null, 'getObjectPHID');
} else {
$positions = array();
}
$task_map = array();
$default_phid = $columns[0]->getPHID();
foreach ($tasks as $task) {
$task_phid = $task->getPHID();
$column_phid = null;
if (isset($positions[$task_phid])) {
$column_phid = $positions[$task_phid]->getColumnPHID();
}
$column_phid = nonempty($column_phid, $default_phid);
$task_map[$column_phid][] = $task_phid;
}
+ // If we're showing the board in "natural" order, sort columns by their
+ // column positions.
+ if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) {
+ foreach ($task_map as $column_phid => $task_phids) {
+ $order = array();
+ foreach ($task_phids as $task_phid) {
+ if (isset($positions[$task_phid])) {
+ $order[$task_phid] = $positions[$task_phid]->getOrderingKey();
+ } else {
+ $order[$task_phid] = 0;
+ }
+ }
+ asort($order);
+ $task_map[$column_phid] = array_keys($order);
+ }
+ }
+
$task_can_edit_map = id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
->apply($tasks);
$board_id = celerity_generate_unique_node_id();
$board = id(new PHUIWorkboardView())
->setUser($viewer)
->setID($board_id);
$this->initBehavior(
'project-boards',
array(
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'createURI' => '/maniphest/task/create/',
'order' => $this->sortKey,
));
$this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
foreach ($columns as $column) {
$panel = id(new PHUIWorkpanelView())
->setHeader($column->getDisplayName())
->setHeaderColor($column->getHeaderColor());
$panel->setEditURI($board_uri.'column/'.$column->getID().'/');
$panel->setHeaderAction(id(new PHUIIconView())
->setIconFont('fa-plus')
->setHref('/maniphest/task/create/')
->addSigil('column-add-task')
->setMetadata(
array('columnPHID' => $column->getPHID())));
$cards = id(new PHUIObjectItemListView())
->setUser($viewer)
->setFlush(true)
->setAllowEmptyList(true)
->addSigil('project-column')
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
));
$task_phids = idx($task_map, $column->getPHID(), array());
foreach (array_select_keys($tasks, $task_phids) as $task) {
$owner = null;
if ($task->getOwnerPHID()) {
$owner = $this->handles[$task->getOwnerPHID()];
}
$can_edit = idx($task_can_edit_map, $task->getPHID(), false);
$cards->addItem(id(new ProjectBoardTaskCard())
->setViewer($viewer)
->setTask($task)
->setOwner($owner)
->setCanEdit($can_edit)
->getItem());
}
$panel->setCards($cards);
if (!$task_phids) {
$cards->addClass('project-column-empty');
}
$board->addPanel($panel);
}
Javelin::initBehavior(
'boards-dropdown',
array());
$sort_menu = $this->buildSortMenu(
$viewer,
$sort_key);
$filter_menu = $this->buildFilterMenu(
$viewer,
$custom_query,
$engine,
$query_key);
$manage_menu = $this->buildManageMenu($project, $show_hidden);
$header_link = phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('view/'.$project->getID().'/')
),
$project->getName());
$header = id(new PHUIHeaderView())
->setHeader($header_link)
->setUser($viewer)
->setNoBackground(true)
->setImage($project->getProfileImageURI())
->setImageURL($this->getApplicationURI('view/'.$project->getID().'/'))
->addActionLink($sort_menu)
->addActionLink($filter_menu)
->addActionLink($manage_menu)
->setPolicyObject($project);
$board_box = id(new PHUIBoxView())
->appendChild($board)
->addClass('project-board-wrapper');
return $this->buildApplicationPage(
array(
$header,
$board_box,
),
array(
'title' => pht('%s Board', $project->getName()),
));
}
private function buildSortMenu(
PhabricatorUser $viewer,
$sort_key) {
$sort_icon = id(new PHUIIconView())
->setIconFont('fa-sort-amount-asc bluegrey');
$named = array(
PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'),
PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'),
);
$base_uri = $this->getURIWithState();
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $sort_key);
if ($is_selected) {
$active_order = $name;
}
$item = id(new PhabricatorActionView())
->setIcon('fa-sort-amount-asc')
->setSelected($is_selected)
->setName($name);
$uri = $base_uri->alter('order', $key);
$item->setHref($uri);
$items[] = $item;
}
$sort_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$sort_menu->addAction($item);
}
$sort_button = id(new PHUIButtonView())
->setText(pht('Sort: %s', $active_order))
->setIcon($sort_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $sort_menu),
));
return $sort_button;
}
private function buildFilterMenu(
PhabricatorUser $viewer,
$custom_query,
PhabricatorApplicationSearchEngine $engine,
$query_key) {
$filter_icon = id(new PHUIIconView())
->setIconFont('fa-search-plus bluegrey');
$named = array(
'open' => pht('Open Tasks'),
'all' => pht('All Tasks'),
);
if ($viewer->isLoggedIn()) {
$named['assigned'] = pht('Assigned to Me');
}
if ($custom_query) {
$named[$custom_query->getQueryKey()] = pht('Custom Filter');
}
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $query_key);
if ($is_selected) {
$active_filter = $name;
}
$is_custom = false;
if ($custom_query) {
$is_custom = ($key == $custom_query->getQueryKey());
}
$item = id(new PhabricatorActionView())
->setIcon('fa-search')
->setSelected($is_selected)
->setName($name);
if ($is_custom) {
$uri = $this->getApplicationURI(
'board/'.$this->id.'/filter/query/'.$key.'/');
$item->setWorkflow(true);
} else {
$uri = $engine->getQueryResultsPageURI($key);
}
$uri = $this->getURIWithState($uri);
$item->setHref($uri);
$items[] = $item;
}
$items[] = id(new PhabricatorActionView())
->setIcon('fa-cog')
->setHref($this->getApplicationURI('board/'.$this->id.'/filter/'))
->setWorkflow(true)
->setName(pht('Advanced Filter...'));
$filter_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$filter_menu->addAction($item);
}
$filter_button = id(new PHUIButtonView())
->setText(pht('Filter: %s', $active_filter))
->setIcon($filter_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $filter_menu),
));
return $filter_button;
}
private function buildManageMenu(
PhabricatorProject $project,
$show_hidden) {
$request = $this->getRequest();
$viewer = $request->getUser();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$manage_icon = id(new PHUIIconView())
->setIconFont('fa-cog bluegrey');
$manage_items = array();
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-plus')
->setName(pht('Add Column'))
->setHref($this->getApplicationURI('board/'.$this->id.'/edit/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-exchange')
->setName(pht('Reorder Columns'))
->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/'))
->setDisabled(!$can_edit)
->setWorkflow(true);
if ($show_hidden) {
$hidden_uri = $this->getURIWithState()
->setQueryParam('hidden', null);
$hidden_icon = 'fa-eye-slash';
$hidden_text = pht('Hide Hidden Columns');
} else {
$hidden_uri = $this->getURIWithState()
->setQueryParam('hidden', 'true');
$hidden_icon = 'fa-eye';
$hidden_text = pht('Show Hidden Columns');
}
$manage_items[] = id(new PhabricatorActionView())
->setIcon($hidden_icon)
->setName($hidden_text)
->setHref($hidden_uri);
$manage_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($manage_items as $item) {
$manage_menu->addAction($item);
}
$manage_button = id(new PHUIButtonView())
->setText(pht('Manage Board'))
->setIcon($manage_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $manage_menu),
));
return $manage_button;
}
private function initializeWorkboardDialog(PhabricatorProject $project) {
$instructions = pht('This workboard has not been setup yet.');
$new_selector = id(new AphrontFormRadioButtonControl())
->setName('initialize-type')
->setValue('backlog-only')
->addButton(
'backlog-only',
pht('New Empty Board'),
pht('Create a new board with just a backlog column.'))
->addButton(
'import',
pht('Import Columns'),
pht('Import board columns from another project.'));
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('New Workboard'))
->addSubmitButton('Continue')
->addCancelButton($this->getApplicationURI('view/'.$project->getID().'/'))
->appendParagraph($instructions)
->appendChild($new_selector);
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
/**
* Add current state parameters (like order and the visibility of hidden
* columns) to a URI.
*
* This allows actions which toggle or adjust one piece of state to keep
* the rest of the board state persistent. If no URI is provided, this method
* starts with the request URI.
*
* @param string|null URI to add state parameters to.
* @return PhutilURI URI with state parameters.
*/
private function getURIWithState($base = null) {
if ($base === null) {
$base = $this->getRequest()->getRequestURI();
}
$base = new PhutilURI($base);
if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) {
$base->setQueryParam('order', $this->sortKey);
} else {
$base->setQueryParam('order', null);
}
$base->setQueryParam('hidden', $this->showHidden ? 'true' : null);
return $base;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php
index d6c6b44118..5302877093 100644
--- a/src/applications/project/controller/PhabricatorProjectMoveController.php
+++ b/src/applications/project/controller/PhabricatorProjectMoveController.php
@@ -1,165 +1,176 @@
<?php
final class PhabricatorProjectMoveController
extends PhabricatorProjectController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$column_phid = $request->getStr('columnPHID');
$object_phid = $request->getStr('objectPHID');
$after_phid = $request->getStr('afterPHID');
$before_phid = $request->getStr('beforePHID');
+ $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
+
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->withIDs(array($this->id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
->execute();
$columns = mpull($columns, null, 'getPHID');
$column = idx($columns, $column_phid);
if (!$column) {
// User is trying to drop this object into a nonexistent column, just kick
// them out.
return new Aphront404Response();
}
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($viewer)
->withColumns($columns)
->withObjectPHIDs(array($object_phid))
->execute();
$xactions = array();
+ if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
+ $order_params = array(
+ 'afterPHID' => $after_phid,
+ 'beforePHID' => $before_phid,
+ );
+ } else {
+ $order_params = array();
+ }
+
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN)
->setNewValue(
array(
'columnPHIDs' => array($column->getPHID()),
'projectPHID' => $column->getProjectPHID(),
- ))
+ ) + $order_params)
->setOldValue(
array(
'columnPHIDs' => mpull($positions, 'getColumnPHID'),
'projectPHID' => $column->getProjectPHID(),
));
$task_phids = array();
if ($after_phid) {
$task_phids[] = $after_phid;
}
if ($before_phid) {
$task_phids[] = $before_phid;
}
- if ($task_phids) {
+ if ($task_phids && ($order == PhabricatorProjectColumn::ORDER_PRIORITY)) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs($task_phids)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
if (count($tasks) != count($task_phids)) {
return new Aphront404Response();
}
$tasks = mpull($tasks, null, 'getPHID');
$a_task = idx($tasks, $after_phid);
$b_task = idx($tasks, $before_phid);
if ($a_task &&
(($a_task->getPriority() < $object->getPriority()) ||
($a_task->getPriority() == $object->getPriority() &&
$a_task->getSubpriority() >= $object->getSubpriority()))) {
$after_pri = $a_task->getPriority();
$after_sub = $a_task->getSubpriority();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue(array(
'newPriority' => $after_pri,
'newSubpriorityBase' => $after_sub,
'direction' => '>'));
} else if ($b_task &&
(($b_task->getPriority() > $object->getPriority()) ||
($b_task->getPriority() == $object->getPriority() &&
$b_task->getSubpriority() <= $object->getSubpriority()))) {
$before_pri = $b_task->getPriority();
$before_sub = $b_task->getSubpriority();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue(array(
'newPriority' => $before_pri,
'newSubpriorityBase' => $before_sub,
'direction' => '<'));
}
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($object, $xactions);
$owner = null;
if ($object->getOwnerPHID()) {
$owner = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($object->getOwnerPHID()))
->executeOne();
}
$card = id(new ProjectBoardTaskCard())
->setViewer($viewer)
->setTask($object)
->setOwner($owner)
->setCanEdit(true)
->getItem();
return id(new AphrontAjaxResponse())->setContent(
array('task' => $card));
}
}
diff --git a/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php b/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php
index 60e9b2de58..87bd87ffd3 100644
--- a/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php
+++ b/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php
@@ -1,290 +1,306 @@
<?php
final class PhabricatorProjectColumnPositionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $boardPHIDs;
private $objectPHIDs;
private $columns;
private $needColumns;
+ private $skipImplicitCreate;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withBoardPHIDs(array $board_phids) {
$this->boardPHIDs = $board_phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
/**
* Find objects in specific columns.
*
* NOTE: Using this method activates logic which constructs virtual
* column positions for objects not in any column, if you pass a default
* column. Normally these results are not returned.
*
* @param list<PhabricatorProjectColumn> Columns to look for objects in.
* @return this
*/
public function withColumns(array $columns) {
assert_instances_of($columns, 'PhabricatorProjectColumn');
$this->columns = $columns;
return $this;
}
public function needColumns($need_columns) {
$this->needColumns = true;
return $this;
}
+
+ /**
+ * Skip implicit creation of column positions which are implied but do not
+ * yet exist.
+ *
+ * This is primarily useful internally.
+ *
+ * @param bool True to skip implicit creation of column positions.
+ * @return this
+ */
+ public function setSkipImplicitCreate($skip) {
+ $this->skipImplicitCreate = $skip;
+ return $this;
+ }
+
// NOTE: For now, boards are always attached to projects. However, they might
// not be in the future. This generalization just anticipates a future where
// we let other types of objects (like users) have boards, or let boards
// contain other types of objects.
private function newPositionObject() {
return new PhabricatorProjectColumnPosition();
}
private function newColumnQuery() {
return new PhabricatorProjectColumnQuery();
}
private function getBoardMembershipEdgeTypes() {
return array(
PhabricatorProjectProjectHasObjectEdgeType::EDGECONST,
);
}
private function getBoardMembershipPHIDTypes() {
return array(
ManiphestTaskPHIDType::TYPECONST,
);
}
protected function loadPage() {
$table = $this->newPositionObject();
$conn_r = $table->establishConnection('r');
// We're going to find results by combining two queries: one query finds
// objects on a board column, while the other query finds objects not on
// any board column and virtually puts them on the default column.
$unions = array();
// First, find all the stuff that's actually on a column.
$unions[] = qsprintf(
$conn_r,
'SELECT * FROM %T %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r));
// If we have a default column, find all the stuff that's not in any
// column and put it in the default column.
$must_type_filter = false;
- if ($this->columns) {
+ if ($this->columns && !$this->skipImplicitCreate) {
$default_map = array();
foreach ($this->columns as $column) {
if ($column->isDefaultColumn()) {
$default_map[$column->getProjectPHID()] = $column->getPHID();
}
}
if ($default_map) {
$where = array();
// Find the edges attached to the boards we have default columns for.
$where[] = qsprintf(
$conn_r,
'e.src IN (%Ls)',
array_keys($default_map));
// Find only edges which describe a board relationship.
$where[] = qsprintf(
$conn_r,
'e.type IN (%Ld)',
$this->getBoardMembershipEdgeTypes());
if ($this->boardPHIDs !== null) {
// This should normally be redundant, but construct it anyway if
// the caller has told us to.
$where[] = qsprintf(
$conn_r,
'e.src IN (%Ls)',
$this->boardPHIDs);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'e.dst IN (%Ls)',
$this->objectPHIDs);
}
$where[] = qsprintf(
$conn_r,
'p.id IS NULL');
$where = $this->formatWhereClause($where);
$unions[] = qsprintf(
$conn_r,
'SELECT NULL id, e.src boardPHID, NULL columnPHID, e.dst objectPHID,
0 sequence
FROM %T e LEFT JOIN %T p
ON e.src = p.boardPHID AND e.dst = p.objectPHID
%Q',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$table->getTableName(),
$where);
$must_type_filter = true;
}
}
$data = queryfx_all(
$conn_r,
'%Q %Q %Q',
implode(' UNION ALL ', $unions),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
// If we've picked up objects not in any column, we need to filter out any
// matched objects which have the wrong edge type.
if ($must_type_filter) {
$allowed_types = array_fuse($this->getBoardMembershipPHIDTypes());
foreach ($data as $id => $row) {
if ($row['columnPHID'] === null) {
$object_phid = $row['objectPHID'];
if (empty($allowed_types[phid_get_type($object_phid)])) {
unset($data[$id]);
}
}
}
}
$positions = $table->loadAllFromArray($data);
// Find the implied positions which don't exist yet. If there are any,
// we're going to create them.
$create = array();
foreach ($positions as $position) {
if ($position->getColumnPHID() === null) {
$column_phid = idx($default_map, $position->getBoardPHID());
$position->setColumnPHID($column_phid);
$create[] = $position;
}
}
if ($create) {
// If we're adding several objects to a column, insert the column
// position objects in object ID order. This means that newly added
// objects float to the top, and when a group of newly added objects
// float up at the same time, the most recently created ones end up
// highest in the list.
$objects = id(new PhabricatorObjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(mpull($create, 'getObjectPHID'))
->execute();
$objects = mpull($objects, null, 'getPHID');
$objects = msort($objects, 'getID');
$create = mgroup($create, 'getObjectPHID');
$create = array_select_keys($create, array_keys($objects)) + $create;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach ($create as $object_phid => $create_positions) {
foreach ($create_positions as $create_position) {
$create_position->save();
}
}
unset($unguarded);
}
return $positions;
}
protected function willFilterPage(array $page) {
if ($this->needColumns) {
$column_phids = mpull($page, 'getColumnPHID');
$columns = $this->newColumnQuery()
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($column_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
foreach ($page as $key => $position) {
$column = idx($columns, $position->getColumnPHID());
if (!$column) {
unset($page[$key]);
continue;
}
$position->attachColumn($column);
}
}
return $page;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->boardPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'boardPHID IN (%Ls)',
$this->boardPHIDs);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->columns !== null) {
$where[] = qsprintf(
$conn_r,
'columnPHID IN (%Ls)',
mpull($this->columns, 'getPHID'));
}
// NOTE: Explicitly not building the paging clause here, since it won't
// work with the UNION.
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php
index 1b229efdfb..0f84cb532f 100644
--- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php
+++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php
@@ -1,51 +1,62 @@
<?php
final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO
implements PhabricatorPolicyInterface {
protected $boardPHID;
protected $columnPHID;
protected $objectPHID;
protected $sequence;
private $column = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function getColumn() {
return $this->assertAttached($this->column);
}
public function attachColumn(PhabricatorProjectColumn $column) {
$this->column = $column;
return $this;
}
+ public function getOrderingKey() {
+ // Low sequence numbers go above high sequence numbers.
+ // High position IDs go above low position IDs.
+ // Broadly, this makes newly added stuff float to the top.
+
+ return sprintf(
+ '~%012d%012d',
+ $this->getSequence(),
+ ((1 << 31) - $this->getID()));
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 14, 2:08 AM (23 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
336711
Default Alt Text
(106 KB)

Event Timeline