mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-18 21:02:41 +01:00
Convert Maniphest merge operations to modern Relationship code
Summary: Ref T4788. Fixes T7820. This updates the "Merge Duplicates In" interaction, and adds a "Close as Duplicate" action. These are the last interactions that were using the old code, so it removes that code. Merges are now recorded as real edges, so we can show them in the UI later on (originally from T9390, etc). Also provides more general support for relationships which need EDIT permission, not-undoable relationships like merges, preventing relating an object to itself, and relationship side effects like merges. Finally, fixes a couple of behaviors around typing an exact object name (like `T123`) to find the related object. Test Plan: - Merged tasks into the current task. - Closed the current task as a duplicate of another task. - Edited other relationships. - Searched for tasks, commits, etc., by object monogram. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788, T7820 Differential Revision: https://secure.phabricator.com/D16196
This commit is contained in:
parent
4f8d07594e
commit
2a7545a452
14 changed files with 336 additions and 440 deletions
|
@ -1420,6 +1420,7 @@ phutil_register_library_map(array(
|
|||
'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php',
|
||||
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
|
||||
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
|
||||
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
|
||||
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
|
||||
'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
|
||||
'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
|
||||
|
@ -1430,6 +1431,7 @@ phutil_register_library_map(array(
|
|||
'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php',
|
||||
'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
|
||||
'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php',
|
||||
'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php',
|
||||
'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
|
||||
'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php',
|
||||
'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php',
|
||||
|
@ -1438,10 +1440,12 @@ phutil_register_library_map(array(
|
|||
'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php',
|
||||
'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php',
|
||||
'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php',
|
||||
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php',
|
||||
'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
|
||||
'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php',
|
||||
'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
|
||||
'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
|
||||
'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php',
|
||||
'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php',
|
||||
'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php',
|
||||
'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php',
|
||||
|
@ -3372,7 +3376,6 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php',
|
||||
'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php',
|
||||
'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
|
||||
'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php',
|
||||
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
|
||||
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
|
||||
'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php',
|
||||
|
@ -3415,7 +3418,6 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
|
||||
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
|
||||
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
|
||||
'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php',
|
||||
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
|
||||
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
|
||||
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
|
||||
|
@ -5929,6 +5931,7 @@ phutil_register_library_map(array(
|
|||
'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField',
|
||||
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
|
||||
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
|
||||
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
|
||||
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||
'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
|
||||
'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
|
||||
|
@ -5939,6 +5942,7 @@ phutil_register_library_map(array(
|
|||
'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine',
|
||||
'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
|
||||
'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship',
|
||||
'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType',
|
||||
'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
|
||||
'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship',
|
||||
'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship',
|
||||
|
@ -5947,10 +5951,12 @@ phutil_register_library_map(array(
|
|||
'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship',
|
||||
'ManiphestTaskHeraldField' => 'HeraldField',
|
||||
'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup',
|
||||
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType',
|
||||
'ManiphestTaskListController' => 'ManiphestController',
|
||||
'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
||||
'ManiphestTaskListView' => 'ManiphestView',
|
||||
'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
|
||||
'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship',
|
||||
'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||
'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver',
|
||||
'ManiphestTaskPHIDType' => 'PhabricatorPHIDType',
|
||||
|
@ -8210,7 +8216,6 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
|
||||
'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController',
|
||||
'PhabricatorSearchBaseController' => 'PhabricatorController',
|
||||
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
|
||||
'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
|
@ -8253,7 +8258,6 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchResultView' => 'AphrontView',
|
||||
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
||||
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
|
||||
'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController',
|
||||
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
|
||||
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
|
||||
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
|
||||
|
|
|
@ -201,6 +201,8 @@ final class ManiphestTaskDetailController extends ManiphestController {
|
|||
|
||||
$parent_key = ManiphestTaskHasParentRelationship::RELATIONSHIPKEY;
|
||||
$subtask_key = ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY;
|
||||
$merge_key = ManiphestTaskMergeInRelationship::RELATIONSHIPKEY;
|
||||
$close_key = ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY;
|
||||
|
||||
$task_submenu[] = $relationship_list->getRelationship($parent_key)
|
||||
->newAction($task);
|
||||
|
@ -208,12 +210,11 @@ final class ManiphestTaskDetailController extends ManiphestController {
|
|||
$task_submenu[] = $relationship_list->getRelationship($subtask_key)
|
||||
->newAction($task);
|
||||
|
||||
$task_submenu[] = id(new PhabricatorActionView())
|
||||
->setName(pht('Merge Duplicates In'))
|
||||
->setHref("/search/attach/{$phid}/TASK/merge/")
|
||||
->setIcon('fa-compress')
|
||||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(true);
|
||||
$task_submenu[] = $relationship_list->getRelationship($merge_key)
|
||||
->newAction($task);
|
||||
|
||||
$task_submenu[] = $relationship_list->getRelationship($close_key)
|
||||
->newAction($task);
|
||||
|
||||
$curtain->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
final class ManiphestTaskHasDuplicateTaskEdgeType
|
||||
extends PhabricatorEdgeType {
|
||||
|
||||
const EDGECONST = 62;
|
||||
|
||||
public function getInverseEdgeConstant() {
|
||||
return ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST;
|
||||
}
|
||||
|
||||
public function shouldWriteInverseTransactions() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
final class ManiphestTaskIsDuplicateOfTaskEdgeType
|
||||
extends PhabricatorEdgeType {
|
||||
|
||||
const EDGECONST = 63;
|
||||
|
||||
public function getInverseEdgeConstant() {
|
||||
return ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST;
|
||||
}
|
||||
|
||||
public function shouldWriteInverseTransactions() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
final class ManiphestTaskCloseAsDuplicateRelationship
|
||||
extends ManiphestTaskRelationship {
|
||||
|
||||
const RELATIONSHIPKEY = 'task.close-as-duplicate';
|
||||
|
||||
public function getEdgeConstant() {
|
||||
return ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST;
|
||||
}
|
||||
|
||||
protected function getActionName() {
|
||||
return pht('Close As Duplicate');
|
||||
}
|
||||
|
||||
protected function getActionIcon() {
|
||||
return 'fa-times';
|
||||
}
|
||||
|
||||
public function canRelateObjects($src, $dst) {
|
||||
return ($dst instanceof ManiphestTask);
|
||||
}
|
||||
|
||||
public function shouldAppearInActionMenu() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDialogTitleText() {
|
||||
return pht('Close As Duplicate');
|
||||
}
|
||||
|
||||
public function getDialogHeaderText() {
|
||||
return pht('Close This Task As a Duplicate Of');
|
||||
}
|
||||
|
||||
public function getDialogButtonText() {
|
||||
return pht('Merge Into Selected Task');
|
||||
}
|
||||
|
||||
protected function newRelationshipSource() {
|
||||
return new ManiphestTaskRelationshipSource();
|
||||
}
|
||||
|
||||
public function getRequiredRelationshipCapabilities() {
|
||||
return array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
);
|
||||
}
|
||||
|
||||
public function canUndoRelationship() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function willUpdateRelationships($object, array $add, array $rem) {
|
||||
|
||||
// TODO: Communicate this in the UI before users hit this error.
|
||||
if (count($add) > 1) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'A task can only be closed as a duplicate of exactly one other '.
|
||||
'task.'));
|
||||
}
|
||||
|
||||
$task = head($add);
|
||||
return $this->newMergeIntoTransactions($task);
|
||||
}
|
||||
|
||||
public function didUpdateRelationships($object, array $add, array $rem) {
|
||||
$viewer = $this->getViewer();
|
||||
$content_source = $this->getContentSource();
|
||||
|
||||
$task = head($add);
|
||||
$xactions = $this->newMergeFromTransactions(array($object));
|
||||
|
||||
$task->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSource($content_source)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true)
|
||||
->applyTransactions($task, $xactions);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
final class ManiphestTaskMergeInRelationship
|
||||
extends ManiphestTaskRelationship {
|
||||
|
||||
const RELATIONSHIPKEY = 'task.merge-in';
|
||||
|
||||
public function getEdgeConstant() {
|
||||
return ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST;
|
||||
}
|
||||
|
||||
protected function getActionName() {
|
||||
return pht('Merge Duplicates In');
|
||||
}
|
||||
|
||||
protected function getActionIcon() {
|
||||
return 'fa-compress';
|
||||
}
|
||||
|
||||
public function canRelateObjects($src, $dst) {
|
||||
return ($dst instanceof ManiphestTask);
|
||||
}
|
||||
|
||||
public function shouldAppearInActionMenu() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDialogTitleText() {
|
||||
return pht('Merge Duplicates Into This Task');
|
||||
}
|
||||
|
||||
public function getDialogHeaderText() {
|
||||
return pht('Tasks to Close and Merge');
|
||||
}
|
||||
|
||||
public function getDialogButtonText() {
|
||||
return pht('Close and Merge Selected Tasks');
|
||||
}
|
||||
|
||||
protected function newRelationshipSource() {
|
||||
return new ManiphestTaskRelationshipSource();
|
||||
}
|
||||
|
||||
public function getRequiredRelationshipCapabilities() {
|
||||
return array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
);
|
||||
}
|
||||
|
||||
public function canUndoRelationship() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function willUpdateRelationships($object, array $add, array $rem) {
|
||||
return $this->newMergeFromTransactions($add);
|
||||
}
|
||||
|
||||
public function didUpdateRelationships($object, array $add, array $rem) {
|
||||
$viewer = $this->getViewer();
|
||||
$content_source = $this->getContentSource();
|
||||
|
||||
foreach ($add as $task) {
|
||||
$xactions = $this->newMergeIntoTransactions($object);
|
||||
|
||||
$task->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSource($content_source)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true)
|
||||
->applyTransactions($task, $xactions);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -16,4 +16,52 @@ abstract class ManiphestTaskRelationship
|
|||
return ($object instanceof ManiphestTask);
|
||||
}
|
||||
|
||||
protected function newMergeIntoTransactions(ManiphestTask $task) {
|
||||
return array(
|
||||
id(new ManiphestTransaction())
|
||||
->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO)
|
||||
->setNewValue($task->getPHID()),
|
||||
);
|
||||
}
|
||||
|
||||
protected function newMergeFromTransactions(array $tasks) {
|
||||
$xactions = array();
|
||||
|
||||
$subscriber_phids = $this->loadMergeSubscriberPHIDs($tasks);
|
||||
|
||||
$xactions[] = id(new ManiphestTransaction())
|
||||
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
|
||||
->setNewValue(array('+' => $subscriber_phids));
|
||||
|
||||
$xactions[] = id(new ManiphestTransaction())
|
||||
->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM)
|
||||
->setNewValue(mpull($tasks, 'getPHID'));
|
||||
|
||||
return $xactions;
|
||||
}
|
||||
|
||||
private function loadMergeSubscriberPHIDs(array $tasks) {
|
||||
$phids = array();
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$phids[] = $task->getAuthorPHID();
|
||||
$phids[] = $task->getOwnerPHID();
|
||||
}
|
||||
|
||||
$subscribers = id(new PhabricatorSubscribersQuery())
|
||||
->withObjectPHIDs(mpull($tasks, 'getPHID'))
|
||||
->execute();
|
||||
|
||||
foreach ($subscribers as $phid => $subscriber_list) {
|
||||
foreach ($subscriber_list as $subscriber) {
|
||||
$phids[] = $subscriber;
|
||||
}
|
||||
}
|
||||
|
||||
$phids = array_unique($phids);
|
||||
$phids = array_filter($phids);
|
||||
|
||||
return $phids;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,10 +30,6 @@ final class PhabricatorSearchApplication extends PhabricatorApplication {
|
|||
return array(
|
||||
'/search/' => array(
|
||||
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSearchController',
|
||||
'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?'
|
||||
=> 'PhabricatorSearchAttachController',
|
||||
'select/(?P<type>\w+)/(?:(?P<action>\w+)/)?'
|
||||
=> 'PhabricatorSearchSelectController',
|
||||
'index/(?P<phid>[^/]+)/' => 'PhabricatorSearchIndexController',
|
||||
'hovercard/'
|
||||
=> 'PhabricatorSearchHovercardController',
|
||||
|
|
|
@ -1,330 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSearchAttachController
|
||||
extends PhabricatorSearchBaseController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$user = $request->getUser();
|
||||
$phid = $request->getURIData('phid');
|
||||
$attach_type = $request->getURIData('type');
|
||||
$action = $request->getURIData('action', self::ACTION_ATTACH);
|
||||
|
||||
$handle = id(new PhabricatorHandleQuery())
|
||||
->setViewer($user)
|
||||
->withPHIDs(array($phid))
|
||||
->executeOne();
|
||||
|
||||
$object_type = $handle->getType();
|
||||
|
||||
$object = id(new PhabricatorObjectQuery())
|
||||
->setViewer($user)
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->withPHIDs(array($phid))
|
||||
->executeOne();
|
||||
|
||||
if (!$object) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$edge_type = null;
|
||||
switch ($action) {
|
||||
case self::ACTION_EDGE:
|
||||
case self::ACTION_DEPENDENCIES:
|
||||
case self::ACTION_BLOCKS:
|
||||
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) {
|
||||
if (!$object instanceof PhabricatorApplicationTransactionInterface) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected object ("%s") to implement interface "%s".',
|
||||
get_class($object),
|
||||
'PhabricatorApplicationTransactionInterface'));
|
||||
}
|
||||
|
||||
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||
$phid,
|
||||
$edge_type);
|
||||
$add_phids = $phids;
|
||||
$rem_phids = array_diff($old_phids, $add_phids);
|
||||
|
||||
$txn_editor = $object->getApplicationTransactionEditor()
|
||||
->setActor($user)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true);
|
||||
|
||||
$txn_template = $object->getApplicationTransactionTemplate()
|
||||
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
|
||||
->setMetadataValue('edge:type', $edge_type)
|
||||
->setNewValue(array(
|
||||
'+' => array_fuse($add_phids),
|
||||
'-' => array_fuse($rem_phids),
|
||||
));
|
||||
|
||||
try {
|
||||
$txn_editor->applyTransactions(
|
||||
$object->getApplicationTransactionObject(),
|
||||
array($txn_template));
|
||||
} 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(
|
||||
$phid,
|
||||
$edge_type);
|
||||
} else {
|
||||
// This is a merge.
|
||||
$phids = array();
|
||||
}
|
||||
}
|
||||
|
||||
$strings = $this->getStrings($attach_type, $action);
|
||||
|
||||
$handles = $this->loadViewerHandles($phids);
|
||||
|
||||
$obj_dialog = new PhabricatorObjectSelectorDialog();
|
||||
$obj_dialog
|
||||
->setUser($user)
|
||||
->setHandles($handles)
|
||||
->setFilters($this->getFilters($strings, $attach_type))
|
||||
->setSelectedFilter($strings['selected'])
|
||||
->setExcluded($phid)
|
||||
->setCancelURI($handle->getURI())
|
||||
->setSearchURI('/search/select/'.$attach_type.'/'.$action.'/')
|
||||
->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)
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->withPHIDs(array_keys($phids))
|
||||
->needSubscriberPHIDs(true)
|
||||
->needProjectPHIDs(true)
|
||||
->execute();
|
||||
|
||||
if (empty($targets)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$editor = id(new ManiphestTransactionEditor())
|
||||
->setActor($user)
|
||||
->setContentSourceFromRequest($this->getRequest())
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true);
|
||||
|
||||
$cc_vector = array();
|
||||
// since we loaded this via a generic object query, go ahead and get the
|
||||
// attach the subscriber and project phids now
|
||||
$task->attachSubscriberPHIDs(
|
||||
PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID()));
|
||||
$task->attachProjectPHIDs(
|
||||
PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(),
|
||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST));
|
||||
|
||||
$cc_vector[] = $task->getSubscriberPHIDs();
|
||||
foreach ($targets as $target) {
|
||||
$cc_vector[] = $target->getSubscriberPHIDs();
|
||||
$cc_vector[] = array(
|
||||
$target->getAuthorPHID(),
|
||||
$target->getOwnerPHID(),
|
||||
);
|
||||
|
||||
$merged_into_txn = id(new ManiphestTransaction())
|
||||
->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO)
|
||||
->setNewValue($task->getPHID());
|
||||
|
||||
$editor->applyTransactions(
|
||||
$target,
|
||||
array($merged_into_txn));
|
||||
|
||||
}
|
||||
$all_ccs = array_mergev($cc_vector);
|
||||
$all_ccs = array_filter($all_ccs);
|
||||
$all_ccs = array_unique($all_ccs);
|
||||
|
||||
$add_ccs = id(new ManiphestTransaction())
|
||||
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
|
||||
->setNewValue(array('=' => $all_ccs));
|
||||
|
||||
$merged_from_txn = id(new ManiphestTransaction())
|
||||
->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM)
|
||||
->setNewValue(mpull($targets, 'getPHID'));
|
||||
|
||||
$editor->applyTransactions(
|
||||
$task,
|
||||
array($add_ccs, $merged_from_txn));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getStrings($attach_type, $action) {
|
||||
switch ($attach_type) {
|
||||
case DifferentialRevisionPHIDType::TYPECONST:
|
||||
$noun = pht('Revisions');
|
||||
$selected = pht('created');
|
||||
break;
|
||||
case ManiphestTaskPHIDType::TYPECONST:
|
||||
$noun = pht('Tasks');
|
||||
$selected = pht('assigned');
|
||||
break;
|
||||
case PhabricatorRepositoryCommitPHIDType::TYPECONST:
|
||||
$noun = pht('Commits');
|
||||
$selected = pht('created');
|
||||
break;
|
||||
case PholioMockPHIDType::TYPECONST:
|
||||
$noun = pht('Mocks');
|
||||
$selected = pht('created');
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case self::ACTION_EDGE:
|
||||
case self::ACTION_ATTACH:
|
||||
$dialog_title = pht('Manage Attached %s', $noun);
|
||||
$header_text = pht('Currently Attached %s', $noun);
|
||||
$button_text = pht('Save %s', $noun);
|
||||
$instructions = null;
|
||||
break;
|
||||
case self::ACTION_MERGE:
|
||||
$dialog_title = pht('Merge Duplicate Tasks');
|
||||
$header_text = pht('Tasks To Merge');
|
||||
$button_text = pht('Merge %s', $noun);
|
||||
$instructions = pht(
|
||||
'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 = pht('Edit Dependencies');
|
||||
$header_text = pht('Current Dependencies');
|
||||
$button_text = pht('Save Dependencies');
|
||||
$instructions = null;
|
||||
break;
|
||||
case self::ACTION_BLOCKS:
|
||||
$dialog_title = pht('Edit Blocking Tasks');
|
||||
$header_text = pht('Current Blocking Tasks');
|
||||
$button_text = pht('Save Blocking Tasks');
|
||||
$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, $attach_type) {
|
||||
if ($attach_type == PholioMockPHIDType::TYPECONST) {
|
||||
$filters = array(
|
||||
'created' => pht('Created By Me'),
|
||||
'all' => pht('All %s', $strings['target_plural_noun']),
|
||||
);
|
||||
} else {
|
||||
$filters = array(
|
||||
'assigned' => pht('Assigned to Me'),
|
||||
'created' => pht('Created By Me'),
|
||||
'open' => pht('All Open %s', $strings['target_plural_noun']),
|
||||
'all' => pht('All %s', $strings['target_plural_noun']),
|
||||
);
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function getEdgeType($src_type, $dst_type) {
|
||||
$t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST;
|
||||
$t_task = ManiphestTaskPHIDType::TYPECONST;
|
||||
$t_drev = DifferentialRevisionPHIDType::TYPECONST;
|
||||
$t_mock = PholioMockPHIDType::TYPECONST;
|
||||
|
||||
$map = array(
|
||||
$t_cmit => array(
|
||||
$t_task => DiffusionCommitHasTaskEdgeType::EDGECONST,
|
||||
),
|
||||
$t_task => array(
|
||||
$t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST,
|
||||
$t_task => ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
|
||||
$t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST,
|
||||
$t_mock => ManiphestTaskHasMockEdgeType::EDGECONST,
|
||||
),
|
||||
$t_drev => array(
|
||||
$t_drev => DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
|
||||
$t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST,
|
||||
),
|
||||
$t_mock => array(
|
||||
$t_task => PholioMockHasTaskEdgeType::EDGECONST,
|
||||
),
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
throw new Exception(
|
||||
pht(
|
||||
'You can not create that dependency, because it would create a '.
|
||||
'circular dependency: %s.',
|
||||
implode(" \xE2\x86\x92 ", $names)));
|
||||
}
|
||||
|
||||
}
|
|
@ -19,9 +19,16 @@ final class PhabricatorSearchRelationshipController
|
|||
$src_phid = $object->getPHID();
|
||||
$edge_type = $relationship->getEdgeConstant();
|
||||
|
||||
$dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||
$src_phid,
|
||||
$edge_type);
|
||||
// If this is a normal relationship, users can remove related objects. If
|
||||
// it's a special relationship like a merge, we can't undo it, so we won't
|
||||
// prefill the current related objects.
|
||||
if ($relationship->canUndoRelationship()) {
|
||||
$dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||
$src_phid,
|
||||
$edge_type);
|
||||
} else {
|
||||
$dst_phids = array();
|
||||
}
|
||||
|
||||
$all_phids = $dst_phids;
|
||||
$all_phids[] = $src_phid;
|
||||
|
@ -44,12 +51,16 @@ final class PhabricatorSearchRelationshipController
|
|||
// relationships at the same time don't race and overwrite one another.
|
||||
$add_phids = array_diff($phids, $initial_phids);
|
||||
$rem_phids = array_diff($initial_phids, $phids);
|
||||
$all_phids = array_merge($add_phids, $rem_phids);
|
||||
|
||||
if ($add_phids) {
|
||||
$capabilities = $relationship->getRequiredRelationshipCapabilities();
|
||||
|
||||
if ($all_phids) {
|
||||
$dst_objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs($phids)
|
||||
->withPHIDs($all_phids)
|
||||
->setRaisePolicyExceptions(true)
|
||||
->requireCapabilities($capabilities)
|
||||
->execute();
|
||||
$dst_objects = mpull($dst_objects, null, 'getPHID');
|
||||
} else {
|
||||
|
@ -67,6 +78,14 @@ final class PhabricatorSearchRelationshipController
|
|||
$add_phid));
|
||||
}
|
||||
|
||||
if ($add_phid == $src_phid) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'You can not create a relationship to object "%s" because '.
|
||||
'objects can not be related to themselves.',
|
||||
$add_phid));
|
||||
}
|
||||
|
||||
if (!$relationship->canRelateObjects($object, $dst_object)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
|
@ -81,9 +100,12 @@ final class PhabricatorSearchRelationshipController
|
|||
return $this->newUnrelatableObjectResponse($ex, $done_uri);
|
||||
}
|
||||
|
||||
$content_source = PhabricatorContentSource::newFromRequest($request);
|
||||
$relationship->setContentSource($content_source);
|
||||
|
||||
$editor = $object->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContentSource($content_source)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true);
|
||||
|
||||
|
@ -96,9 +118,29 @@ final class PhabricatorSearchRelationshipController
|
|||
'-' => array_fuse($rem_phids),
|
||||
));
|
||||
|
||||
$add_objects = array_select_keys($dst_objects, $add_phids);
|
||||
$rem_objects = array_select_keys($dst_objects, $rem_phids);
|
||||
|
||||
if ($add_objects || $rem_objects) {
|
||||
$more_xactions = $relationship->willUpdateRelationships(
|
||||
$object,
|
||||
$add_objects,
|
||||
$rem_objects);
|
||||
foreach ($more_xactions as $xaction) {
|
||||
$xactions[] = $xaction;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$editor->applyTransactions($object, $xactions);
|
||||
|
||||
if ($add_objects || $rem_objects) {
|
||||
$relationship->didUpdateRelationships(
|
||||
$object,
|
||||
$add_objects,
|
||||
$rem_objects);
|
||||
}
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($done_uri);
|
||||
} catch (PhabricatorEdgeCycleException $ex) {
|
||||
return $this->newGraphCycleResponse($ex, $done_uri);
|
||||
|
|
|
@ -58,7 +58,7 @@ final class PhabricatorSearchRelationshipSourceController
|
|||
->execute();
|
||||
|
||||
$phids = array_fill_keys(mpull($results, 'getPHID'), true);
|
||||
$phids += $this->queryObjectNames($query_str, $capabilities);
|
||||
$phids = $this->queryObjectNames($query, $capabilities) + $phids;
|
||||
|
||||
$phids = array_keys($phids);
|
||||
$handles = $viewer->loadHandles($phids);
|
||||
|
@ -72,15 +72,21 @@ final class PhabricatorSearchRelationshipSourceController
|
|||
return id(new AphrontAjaxResponse())->setContent($data);
|
||||
}
|
||||
|
||||
private function queryObjectNames($query, $capabilities) {
|
||||
private function queryObjectNames(
|
||||
PhabricatorSavedQuery $query,
|
||||
array $capabilities) {
|
||||
|
||||
$request = $this->getRequest();
|
||||
$viewer = $request->getUser();
|
||||
|
||||
$types = $query->getParameter('types');
|
||||
$match = $query->getParameter('query');
|
||||
|
||||
$objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($viewer)
|
||||
->requireCapabilities($capabilities)
|
||||
->withTypes(array($request->getURIData('type')))
|
||||
->withNames(array($query))
|
||||
->withTypes($query->getParameter('types'))
|
||||
->withNames(array($match))
|
||||
->execute();
|
||||
|
||||
return mpull($objects, 'getPHID');
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSearchSelectController
|
||||
extends PhabricatorSearchBaseController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$user = $request->getUser();
|
||||
$type = $request->getURIData('type');
|
||||
$action = $request->getURIData('action');
|
||||
|
||||
$query = new PhabricatorSavedQuery();
|
||||
$query_str = $request->getStr('query');
|
||||
|
||||
$query->setEngineClassName('PhabricatorSearchApplicationSearchEngine');
|
||||
$query->setParameter('query', $query_str);
|
||||
$query->setParameter('types', array($type));
|
||||
|
||||
$status_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
|
||||
|
||||
switch ($request->getStr('filter')) {
|
||||
case 'assigned':
|
||||
$query->setParameter('ownerPHIDs', array($user->getPHID()));
|
||||
$query->setParameter('statuses', array($status_open));
|
||||
break;
|
||||
case 'created';
|
||||
$query->setParameter('authorPHIDs', array($user->getPHID()));
|
||||
// TODO - if / when we allow pholio mocks to be archived, etc
|
||||
// update this
|
||||
if ($type != PholioMockPHIDType::TYPECONST) {
|
||||
$query->setParameter('statuses', array($status_open));
|
||||
}
|
||||
break;
|
||||
case 'open':
|
||||
$query->setParameter('statuses', array($status_open));
|
||||
break;
|
||||
}
|
||||
|
||||
$query->setParameter('excludePHIDs', array($request->getStr('exclude')));
|
||||
|
||||
$capabilities = array(PhabricatorPolicyCapability::CAN_VIEW);
|
||||
switch ($action) {
|
||||
case self::ACTION_MERGE:
|
||||
$capabilities[] = PhabricatorPolicyCapability::CAN_EDIT;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$results = id(new PhabricatorSearchDocumentQuery())
|
||||
->setViewer($user)
|
||||
->requireObjectCapabilities($capabilities)
|
||||
->withSavedQuery($query)
|
||||
->setOffset(0)
|
||||
->setLimit(100)
|
||||
->execute();
|
||||
|
||||
$phids = array_fill_keys(mpull($results, 'getPHID'), true);
|
||||
$phids += $this->queryObjectNames($query_str, $capabilities);
|
||||
|
||||
$phids = array_keys($phids);
|
||||
$handles = $this->loadViewerHandles($phids);
|
||||
|
||||
$data = array();
|
||||
foreach ($handles as $handle) {
|
||||
$view = new PhabricatorHandleObjectSelectorDataView($handle);
|
||||
$data[] = $view->renderData();
|
||||
}
|
||||
|
||||
return id(new AphrontAjaxResponse())->setContent($data);
|
||||
}
|
||||
|
||||
private function queryObjectNames($query, $capabilities) {
|
||||
$request = $this->getRequest();
|
||||
$viewer = $request->getUser();
|
||||
|
||||
$objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($viewer)
|
||||
->requireCapabilities($capabilities)
|
||||
->withTypes(array($request->getURIData('type')))
|
||||
->withNames(array($query))
|
||||
->execute();
|
||||
|
||||
return mpull($objects, 'getPHID');
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
abstract class PhabricatorObjectRelationship extends Phobject {
|
||||
|
||||
private $viewer;
|
||||
private $contentSource;
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
|
@ -13,6 +14,15 @@ abstract class PhabricatorObjectRelationship extends Phobject {
|
|||
return $this->viewer;
|
||||
}
|
||||
|
||||
public function setContentSource(PhabricatorContentSource $content_source) {
|
||||
$this->contentSource = $content_source;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContentSource() {
|
||||
return $this->contentSource;
|
||||
}
|
||||
|
||||
final public function getRelationshipConstant() {
|
||||
return $this->getPhobjectClassConstant('RELATIONSHIPKEY');
|
||||
}
|
||||
|
@ -94,4 +104,16 @@ abstract class PhabricatorObjectRelationship extends Phobject {
|
|||
return "/search/rel/{$type}/{$phid}/";
|
||||
}
|
||||
|
||||
public function canUndoRelationship() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function willUpdateRelationships($object, array $add, array $rem) {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function didUpdateRelationships($object, array $add, array $rem) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -609,6 +609,8 @@ abstract class PhabricatorApplicationTransaction
|
|||
$edge_type = $this->getMetadataValue('edge:type');
|
||||
switch ($edge_type) {
|
||||
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
|
||||
case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
|
||||
case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
|
||||
return true;
|
||||
break;
|
||||
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
|
||||
|
|
Loading…
Reference in a new issue