diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3394,6 +3394,7 @@ 'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php', 'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php', 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', + 'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php', 'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php', 'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php', 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', @@ -8213,6 +8214,7 @@ 'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchOrderField' => 'PhabricatorSearchField', 'PhabricatorSearchRelationship' => 'Phobject', + 'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchResultBucket' => 'Phobject', 'PhabricatorSearchResultBucketGroup' => 'Phobject', 'PhabricatorSearchResultView' => 'AphrontView', diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php --- a/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php @@ -24,4 +24,20 @@ return false; } + public function canRelateObjects($src, $dst) { + return ($dst instanceof PhabricatorRepositoryCommit); + } + + public function getDialogTitleText() { + return pht('Edit Related Commits'); + } + + public function getDialogHeaderText() { + return pht('Current Commits'); + } + + public function getDialogButtonText() { + return pht('Save Related Commits'); + } + } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php --- a/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php @@ -17,4 +17,20 @@ return 'fa-camera-retro'; } + public function canRelateObjects($src, $dst) { + return ($dst instanceof PholioMock); + } + + public function getDialogTitleText() { + return pht('Edit Related Mocks'); + } + + public function getDialogHeaderText() { + return pht('Current Mocks'); + } + + public function getDialogButtonText() { + return pht('Save Related Mocks'); + } + } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php --- a/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php @@ -17,4 +17,20 @@ return 'fa-cog'; } + public function canRelateObjects($src, $dst) { + return ($dst instanceof DifferentialRevision); + } + + public function getDialogTitleText() { + return pht('Edit Related Revisions'); + } + + public function getDialogHeaderText() { + return pht('Current Revisions'); + } + + public function getDialogButtonText() { + return pht('Save Related Revisions'); + } + } diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -41,6 +41,8 @@ 'delete/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorSearchDeleteController', 'order/(?P[^/]+)/' => 'PhabricatorSearchOrderController', + 'rel/(?P[^/]+)/(?P[^/]+)/' + => 'PhabricatorSearchRelationshipController', ), ); } diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipController.php b/src/applications/search/controller/PhabricatorSearchRelationshipController.php new file mode 100644 --- /dev/null +++ b/src/applications/search/controller/PhabricatorSearchRelationshipController.php @@ -0,0 +1,198 @@ +getViewer(); + + $phid = $request->getURIData('sourcePHID'); + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $list = PhabricatorObjectRelationshipList::newForObject( + $viewer, + $object); + + $relationship_key = $request->getURIData('relationshipKey'); + $relationship = $list->getRelationship($relationship_key); + if (!$relationship) { + return new Aphront404Response(); + } + + $src_phid = $object->getPHID(); + $edge_type = $relationship->getEdgeConstant(); + + $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $src_phid, + $edge_type); + + $all_phids = $dst_phids; + $all_phids[] = $src_phid; + + $handles = $viewer->loadHandles($all_phids); + $src_handle = $handles[$src_phid]; + + $done_uri = $src_handle->getURI(); + + if ($request->isFormPost()) { + $phids = explode(';', $request->getStr('phids')); + $phids = array_filter($phids); + $phids = array_values($phids); + + // TODO: Embed these in the form instead, to gracefully resolve + // concurrent edits like we do for subscribers and projects. + $old_phids = $dst_phids; + + $add_phids = $phids; + $rem_phids = array_diff($old_phids, $add_phids); + + if ($add_phids) { + $dst_objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->setRaisePolicyExceptions(true) + ->execute(); + $dst_objects = mpull($dst_objects, null, 'getPHID'); + } else { + $dst_objects = array(); + } + + try { + foreach ($add_phids as $add_phid) { + $dst_object = idx($dst_objects, $add_phid); + if (!$dst_object) { + throw new Exception( + pht( + 'You can not create a relationship to object "%s" because '. + 'the object does not exist or could not be loaded.', + $add_phid)); + } + + if (!$relationship->canRelateObjects($object, $dst_object)) { + throw new Exception( + pht( + 'You can not create a relationship (of type "%s") to object '. + '"%s" because it is not the right type of object for this '. + 'relationship.', + $relationship->getRelationshipConstant(), + $add_phid)); + } + } + } catch (Exception $ex) { + return $this->newUnrelatableObjectResponse($ex, $done_uri); + } + + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $xactions = array(); + $xactions[] = $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edge_type) + ->setNewValue(array( + '+' => array_fuse($add_phids), + '-' => array_fuse($rem_phids), + )); + + try { + $editor->applyTransactions($object, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } catch (PhabricatorEdgeCycleException $ex) { + return $this->newGraphCycleResponse($ex, $done_uri); + } + } + + $handles = iterator_to_array($handles); + $handles = array_select_keys($handles, $dst_phids); + + // TODO: These are hard-coded for now. + $filters = array( + 'assigned' => pht('Assigned to Me'), + 'created' => pht('Created By Me'), + 'open' => pht('All Open Objects'), + 'all' => pht('All Objects'), + ); + + $dialog_title = $relationship->getDialogTitleText(); + $dialog_header = $relationship->getDialogHeaderText(); + $dialog_button = $relationship->getDialogButtonText(); + $dialog_instructions = $relationship->getDialogInstructionsText(); + + // TODO: Remove this, this is just legacy support. + $legacy_kinds = array( + ManiphestTaskHasCommitEdgeType::EDGECONST => 'CMIT', + ManiphestTaskHasMockEdgeType::EDGECONST => 'MOCK', + ManiphestTaskHasRevisionEdgeType::EDGECONST => 'DREV', + ); + + $edge_type = $relationship->getEdgeConstant(); + $legacy_kind = idx($legacy_kinds, $edge_type); + if (!$legacy_kind) { + throw new Exception( + pht('Only specific legacy relationships are supported!')); + } + + return id(new PhabricatorObjectSelectorDialog()) + ->setUser($viewer) + ->setHandles($handles) + ->setFilters($filters) + ->setSelectedFilter('created') + ->setExcluded($phid) + ->setCancelURI($done_uri) + ->setSearchURI("/search/select/{$legacy_kind}/edge/") + ->setTitle($dialog_title) + ->setHeader($dialog_header) + ->setButtonText($dialog_button) + ->setInstructions($dialog_instructions) + ->buildDialog(); + } + + private function newGraphCycleResponse( + PhabricatorEdgeCycleException $ex, + $done_uri) { + + $viewer = $this->getViewer(); + $cycle = $ex->getCycle(); + + $handles = $this->loadViewerHandles($cycle); + $names = array(); + foreach ($cycle as $cycle_phid) { + $names[] = $handles[$cycle_phid]->getFullName(); + } + + $message = pht( + 'You can not create that relationship because it would create a '. + 'circular dependency: %s.', + implode(" \xE2\x86\x92 ", $names)); + + return $this->newDialog() + ->setTitle(pht('Circular Dependency')) + ->appendParagraph($message) + ->addCancelButton($done_uri); + } + + private function newUnrelatableObjectResponse(Exception $ex, $done_uri) { + $message = $ex->getMessage(); + + return $this->newDialog() + ->setTitle(pht('Invalid Relationship')) + ->appendParagraph($message) + ->addCancelButton($done_uri); + } + +} diff --git a/src/applications/search/relationship/PhabricatorObjectRelationship.php b/src/applications/search/relationship/PhabricatorObjectRelationship.php --- a/src/applications/search/relationship/PhabricatorObjectRelationship.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationship.php @@ -24,6 +24,16 @@ abstract protected function getActionName(); abstract protected function getActionIcon(); + abstract public function canRelateObjects($src, $dst); + + abstract public function getDialogTitleText(); + abstract public function getDialogHeaderText(); + abstract public function getDialogButtonText(); + + public function getDialogInstructionsText() { + return null; + } + public function shouldAppearInActionMenu() { return true; } @@ -58,24 +68,8 @@ private function getActionURI($object) { $phid = $object->getPHID(); - - // TODO: Remove this, this is just legacy support for the current - // controller until a new one gets built. - $legacy_kinds = array( - ManiphestTaskHasCommitEdgeType::EDGECONST => 'CMIT', - ManiphestTaskHasMockEdgeType::EDGECONST => 'MOCK', - ManiphestTaskHasRevisionEdgeType::EDGECONST => 'DREV', - ); - - $edge_type = $this->getEdgeConstant(); - $legacy_kind = idx($legacy_kinds, $edge_type); - if (!$legacy_kind) { - throw new Exception( - pht( - 'Only specific legacy relationships are supported!')); - } - - return "/search/attach/{$phid}/{$legacy_kind}/"; + $type = $this->getRelationshipConstant(); + return "/search/rel/{$type}/{$phid}/"; } } diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php --- a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php @@ -71,6 +71,10 @@ ->setSubmenu($actions); } + public function getRelationship($key) { + return idx($this->relationships, $key); + } + public static function newForObject(PhabricatorUser $viewer, $object) { $relationships = PhabricatorObjectRelationship::getAllRelationships();