diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 2195089d24..c29c7ee9a1 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,368 +1,366 @@ getViewer(); $id = $request->getURIData('id'); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needSubscriberPHIDs(true) ->executeOne(); if (!$task) { return new Aphront404Response(); } $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($viewer) ->readFieldsFromStorage($task); $edit_engine = id(new ManiphestEditEngine()) ->setViewer($viewer) ->setTargetObject($task); $e_commit = ManiphestTaskHasCommitEdgeType::EDGECONST; $e_dep_on = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $e_dep_by = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $e_rev = ManiphestTaskHasRevisionEdgeType::EDGECONST; $e_mock = ManiphestTaskHasMockEdgeType::EDGECONST; $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes( array( $e_commit, $e_dep_on, $e_dep_by, $e_rev, $e_mock, )); $edges = idx($query->execute(), $phid); $phids = array_fill_keys($query->getDestinationPHIDs(), true); if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $phids = array_keys($phids); $handles = $viewer->loadHandles($phids); $timeline = $this->buildTransactionTimeline( $task, new ManiphestTransactionQuery()); $monogram = $task->getMonogram(); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($monogram) ->setBorder(true); $header = $this->buildHeaderView($task); $details = $this->buildPropertyView($task, $field_list, $edges, $handles); $description = $this->buildDescriptionView($task); $curtain = $this->buildCurtain($task, $edit_engine); $title = pht('%s %s', $monogram, $task->getTitle()); $comment_view = $edit_engine ->buildEditEngineCommentView($task); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $timeline, $comment_view, )) ->addPropertySection(pht('Description'), $description) ->addPropertySection(pht('Details'), $details); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs( array( $task->getPHID(), )) ->appendChild( array( $view, )); } private function buildHeaderView(ManiphestTask $task) { $view = id(new PHUIHeaderView()) ->setHeader($task->getTitle()) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); $priority_name = ManiphestTaskPriority::getTaskPriorityName( $task->getPriority()); $priority_color = ManiphestTaskPriority::getTaskPriorityColor( $task->getPriority()); $status = $task->getStatus(); $status_name = ManiphestTaskStatus::renderFullDescription( $status, $priority_name, $priority_color); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); $view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon( $task->getStatus()).' '.$priority_color); if (ManiphestTaskPoints::getIsEnabled()) { $points = $task->getPoints(); if ($points !== null) { $points_name = pht('%s %s', $task->getPoints(), ManiphestTaskPoints::getPointsLabel()); $tag = id(new PHUITagView()) ->setName($points_name) ->setShade('blue') ->setType(PHUITagView::TYPE_SHADE); $view->addTag($tag); } } return $view; } private function buildCurtain( ManiphestTask $task, PhabricatorEditEngine $edit_engine) { $viewer = $this->getViewer(); $id = $task->getID(); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $curtain = $this->newCurtainView($task); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/task/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $edit_config = $edit_engine->loadDefaultEditConfiguration(); $can_create = (bool)$edit_config; $can_reassign = $edit_engine->hasEditAccessToTransaction( ManiphestTransaction::TYPE_OWNER); if ($can_create) { $form_key = $edit_config->getIdentifier(); $edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) ->setQueryParam('parent', $id) ->setQueryParam('template', $id) ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $edit_uri = $this->getApplicationURI($edit_uri); } else { // TODO: This will usually give us a somewhat-reasonable error page, but // could be a bit cleaner. $edit_uri = "/task/edit/{$id}/"; $edit_uri = $this->getApplicationURI($edit_uri); } $task_submenu = array(); $task_submenu[] = id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($edit_uri) ->setIcon('fa-level-down') ->setDisabled(!$can_create) ->setWorkflow(!$can_create); $task_submenu[] = id(new PhabricatorActionView()) ->setName(pht('Edit Blocking Tasks')) ->setHref("/search/attach/{$phid}/TASK/blocks/") - ->setWorkflow(true) ->setIcon('fa-link') ->setDisabled(!$can_edit) ->setWorkflow(true); $task_submenu[] = id(new PhabricatorActionView()) ->setName(pht('Merge Duplicates In')) ->setHref("/search/attach/{$phid}/TASK/merge/") - ->setWorkflow(true) ->setIcon('fa-compress') ->setDisabled(!$can_edit) ->setWorkflow(true); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Related Tasks...')) ->setIcon('fa-anchor') ->setSubmenu($task_submenu)); $owner_phid = $task->getOwnerPHID(); $author_phid = $task->getAuthorPHID(); $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); if ($owner_phid) { $image_uri = $handles[$owner_phid]->getImageURI(); $image_href = $handles[$owner_phid]->getURI(); $owner = $viewer->renderHandle($owner_phid)->render(); $content = phutil_tag('strong', array(), $owner); $assigned_to = id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } else { $assigned_to = phutil_tag('em', array(), pht('None')); } $curtain->newPanel() ->setHeaderText(pht('Assigned To')) ->appendChild($assigned_to); $author_uri = $handles[$author_phid]->getImageURI(); $author_href = $handles[$author_phid]->getURI(); $author = $viewer->renderHandle($author_phid)->render(); $content = phutil_tag('strong', array(), $author); $date = phabricator_date($task->getDateCreated(), $viewer); $content = pht('%s, %s', $content, $date); $authored_by = id(new PHUIHeadThingView()) ->setImage($author_uri) ->setImageHref($author_href) ->setContent($content); $curtain->newPanel() ->setHeaderText(pht('Authored By')) ->appendChild($authored_by); return $curtain; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, $handles) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); $view->addProperty( pht('From Email'), phutil_tag( 'a', array( 'href' => 'mailto:'.$source.'?subject='.$subject, ), $source)); } $edge_types = array( ManiphestTaskDependedOnByTaskEdgeType::EDGECONST => pht('Blocks'), ManiphestTaskDependsOnTaskEdgeType::EDGECONST => pht('Blocked By'), ManiphestTaskHasRevisionEdgeType::EDGECONST => pht('Differential Revisions'), ManiphestTaskHasMockEdgeType::EDGECONST => pht('Pholio Mocks'), ); $revisions_commits = array(); $commit_phids = array_keys( $edges[ManiphestTaskHasCommitEdgeType::EDGECONST]); if ($commit_phids) { $commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST; $drev_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($commit_phids) ->withEdgeTypes(array($commit_drev)) ->execute(); foreach ($commit_phids as $phid) { $revisions_commits[$phid] = $handles->renderHandle($phid) ->setShowHovercard(true); $revision_phid = key($drev_edges[$phid][$commit_drev]); $revision_handle = $handles->getHandleIfExists($revision_phid); if ($revision_handle) { $task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST; unset($edges[$task_drev][$revision_phid]); $revisions_commits[$phid] = hsprintf( '%s / %s', $revision_handle->renderHovercardLink($revision_handle->getName()), $revisions_commits[$phid]); } } } foreach ($edge_types as $edge_type => $edge_name) { if ($edges[$edge_type]) { $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); $view->addProperty( $edge_name, $edge_handles->renderList()); } } if ($revisions_commits) { $view->addProperty( pht('Commits'), phutil_implode_html(phutil_tag('br'), $revisions_commits)); } $field_list->appendFieldsToPropertyList( $task, $viewer, $view); if ($view->hasAnyProperties()) { return $view; } return null; } private function buildDescriptionView(ManiphestTask $task) { $viewer = $this->getViewer(); $section = null; $description = $task->getDescription(); if (strlen($description)) { $section = new PHUIPropertyListView(); $section->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), id(new PHUIRemarkupView($viewer, $description)) ->setContextObject($task))); } return $section; } } diff --git a/src/applications/pholio/event/PholioActionMenuEventListener.php b/src/applications/pholio/event/PholioActionMenuEventListener.php index cfa6e39c32..0b563537fa 100644 --- a/src/applications/pholio/event/PholioActionMenuEventListener.php +++ b/src/applications/pholio/event/PholioActionMenuEventListener.php @@ -1,51 +1,50 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: $this->handleActionsEvent($event); break; } } private function handleActionsEvent(PhutilEvent $event) { $object = $event->getValue('object'); $actions = null; if ($object instanceof ManiphestTask) { $actions = $this->renderTaskItems($event); } $this->addActionMenuItems($event, $actions); } private function renderTaskItems(PhutilEvent $event) { if (!$this->canUseApplication($event->getUser())) { return; } $task = $event->getValue('object'); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $event->getUser(), $task, PhabricatorPolicyCapability::CAN_EDIT); return id(new PhabricatorActionView()) ->setName(pht('Edit Pholio Mocks')) ->setHref("/search/attach/{$phid}/MOCK/edge/") - ->setWorkflow(true) ->setIcon('fa-camera-retro') ->setDisabled(!$can_edit) ->setWorkflow(true); } } diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php index 2d00ac8bda..a3238cb50d 100644 --- a/src/applications/search/controller/PhabricatorSearchAttachController.php +++ b/src/applications/search/controller/PhabricatorSearchAttachController.php @@ -1,325 +1,330 @@ 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))); } }