diff --git a/src/applications/nuance/controller/NuanceItemViewController.php b/src/applications/nuance/controller/NuanceItemViewController.php index 091ade2d6b..7ef5d06682 100644 --- a/src/applications/nuance/controller/NuanceItemViewController.php +++ b/src/applications/nuance/controller/NuanceItemViewController.php @@ -1,122 +1,126 @@ getViewer(); $id = $request->getURIData('id'); $item = id(new NuanceItemQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$item) { return new Aphront404Response(); } $title = pht('Item %d', $item->getID()); $name = $item->getDisplayName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Items'), $this->getApplicationURI('item/')); $crumbs->addTextCrumb($title); $crumbs->setBorder(true); $curtain = $this->buildCurtain($item); $content = $this->buildContent($item); $commands = $this->buildCommands($item); $timeline = $this->buildTransactionTimeline( $item, new NuanceItemTransactionQuery()); $main = array( $commands, $content, $timeline, ); $header = id(new PHUIHeaderView()) ->setHeader($name); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn($main); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } private function buildCurtain(NuanceItem $item) { $viewer = $this->getViewer(); $id = $item->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $item, PhabricatorPolicyCapability::CAN_EDIT); $curtain = $this->newCurtainView($item); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Manage Item')) ->setIcon('fa-cogs') ->setHref($this->getApplicationURI("item/manage/{$id}/"))); $impl = $item->getImplementation(); $impl->setViewer($viewer); foreach ($impl->getItemActions($item) as $action) { $curtain->addAction($action); } + foreach ($impl->getItemCurtainPanels($item) as $panel) { + $curtain->addPanel($panel); + } + return $curtain; } private function buildContent(NuanceItem $item) { $viewer = $this->getViewer(); $impl = $item->getImplementation(); $impl->setViewer($viewer); return $impl->buildItemView($item); } private function buildCommands(NuanceItem $item) { $viewer = $this->getViewer(); $commands = id(new NuanceItemCommandQuery()) ->setViewer($viewer) ->withItemPHIDs(array($item->getPHID())) ->execute(); $commands = msort($commands, 'getID'); if (!$commands) { return null; } $rows = array(); foreach ($commands as $command) { $rows[] = array( $command->getCommand(), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Command'), )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Pending Commands')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); } } diff --git a/src/applications/nuance/github/NuanceGitHubRawEvent.php b/src/applications/nuance/github/NuanceGitHubRawEvent.php index 1283fb43b7..4da2bb8e46 100644 --- a/src/applications/nuance/github/NuanceGitHubRawEvent.php +++ b/src/applications/nuance/github/NuanceGitHubRawEvent.php @@ -1,380 +1,386 @@ type = $type; $event->raw = $raw; return $event; } public function getRepositoryFullName() { return $this->getRepositoryFullRawName(); } public function isIssueEvent() { if ($this->isPullRequestEvent()) { return false; } if ($this->type == self::TYPE_ISSUE) { return true; } switch ($this->getIssueRawKind()) { case 'IssuesEvent': return true; case 'IssueCommentEvent': if (!$this->getRawPullRequestData()) { return true; } break; } return false; } public function isPullRequestEvent() { if ($this->type == self::TYPE_ISSUE) { // TODO: This is wrong, some of these are pull events. return false; } $raw = $this->raw; switch ($this->getIssueRawKind()) { case 'PullRequestEvent': return true; case 'IssueCommentEvent': if ($this->getRawPullRequestData()) { return true; } break; } return false; } public function getIssueNumber() { if (!$this->isIssueEvent()) { return null; } return $this->getRawIssueNumber(); } public function getPullRequestNumber() { if (!$this->isPullRequestEvent()) { return null; } return $this->getRawIssueNumber(); } public function getID() { $raw = $this->raw; $id = idx($raw, 'id'); if ($id) { return (int)$id; } return null; } public function getComment() { - return 'TODO: Actually extract comment text.'; + if (!$this->isIssueEvent() && !$this->isPullRequestEvent()) { + return null; + } + + $raw = $this->raw; + + return idxv($raw, array('payload', 'comment', 'body')); } public function getURI() { $raw = $this->raw; if ($this->isIssueEvent() || $this->isPullRequestEvent()) { if ($this->type == self::TYPE_ISSUE) { $uri = idxv($raw, array('issue', 'html_url')); $uri = $uri.'#event-'.$this->getID(); } else { // The format of pull request events varies so we need to fish around // a bit to find the correct URI. $uri = idxv($raw, array('payload', 'pull_request', 'html_url')); $need_anchor = true; // For comments, we get a different anchor to link to the comment. In // this case, the URI comes with an anchor already. if (!$uri) { $uri = idxv($raw, array('payload', 'comment', 'html_url')); $need_anchor = false; } if (!$uri) { $uri = idxv($raw, array('payload', 'issue', 'html_url')); $need_anchor = true; } if ($need_anchor) { $uri = $uri.'#event-'.$this->getID(); } } } else { switch ($this->getIssueRawKind()) { case 'CreateEvent': $ref = idxv($raw, array('payload', 'ref')); $repo = $this->getRepositoryFullRawName(); return "https://github.com/{$repo}/commits/{$ref}"; case 'PushEvent': // These don't really have a URI since there may be multiple commits // involved and GitHub doesn't bundle the push as an object on its // own. Just try to find the URI for the log. The API also does // not return any HTML URI for these events. $head = idxv($raw, array('payload', 'head')); if ($head === null) { return null; } $repo = $this->getRepositoryFullRawName(); return "https://github.com/{$repo}/commits/{$head}"; case 'WatchEvent': // These have no reasonable URI. return null; default: return null; } } return $uri; } private function getRepositoryFullRawName() { $raw = $this->raw; $full = idxv($raw, array('repo', 'name')); if (strlen($full)) { return $full; } // For issue events, the repository is not identified explicitly in the // response body. Parse it out of the URI. $matches = null; $ok = preg_match( '(/repos/((?:[^/]+)/(?:[^/]+))/issues/events/)', idx($raw, 'url'), $matches); if ($ok) { return $matches[1]; } return null; } private function getIssueRawKind() { $raw = $this->raw; return idxv($raw, array('type')); } private function getRawIssueNumber() { $raw = $this->raw; if ($this->type == self::TYPE_ISSUE) { return idxv($raw, array('issue', 'number')); } if ($this->type == self::TYPE_REPOSITORY) { $issue_number = idxv($raw, array('payload', 'issue', 'number')); if ($issue_number) { return $issue_number; } $pull_number = idxv($raw, array('payload', 'number')); if ($pull_number) { return $pull_number; } } return null; } private function getRawPullRequestData() { $raw = $this->raw; return idxv($raw, array('payload', 'issue', 'pull_request')); } public function getEventFullTitle() { switch ($this->type) { case self::TYPE_ISSUE: $title = $this->getRawIssueEventTitle(); break; case self::TYPE_REPOSITORY: $title = $this->getRawRepositoryEventTitle(); break; default: $title = pht('Unknown Event Type ("%s")', $this->type); break; } return pht( 'GitHub %s %s (%s)', $this->getRepositoryFullRawName(), $this->getTargetObjectName(), $title); } private function getTargetObjectName() { if ($this->isPullRequestEvent()) { $number = $this->getRawIssueNumber(); return pht('Pull Request #%d', $number); } else if ($this->isIssueEvent()) { $number = $this->getRawIssueNumber(); return pht('Issue #%d', $number); } else if ($this->type == self::TYPE_REPOSITORY) { $raw = $this->raw; $type = idx($raw, 'type'); switch ($type) { case 'CreateEvent': $ref = idxv($raw, array('payload', 'ref')); $ref_type = idxv($raw, array('payload', 'ref_type')); switch ($ref_type) { case 'branch': return pht('Branch %s', $ref); case 'tag': return pht('Tag %s', $ref); default: return pht('Ref %s', $ref); } break; case 'PushEvent': $ref = idxv($raw, array('payload', 'ref')); if (preg_match('(^refs/heads/)', $ref)) { return pht('Branch %s', substr($ref, strlen('refs/heads/'))); } else { return pht('Ref %s', $ref); } break; case 'WatchEvent': $actor = idxv($raw, array('actor', 'login')); return pht('User %s', $actor); } return pht('Unknown Object'); } else { return pht('Unknown Object'); } } private function getRawIssueEventTitle() { $raw = $this->raw; $action = idxv($raw, array('event')); switch ($action) { case 'assigned': $assignee = idxv($raw, array('assignee', 'login')); $title = pht('Assigned: %s', $assignee); break; case 'closed': $title = pht('Closed'); break; case 'demilestoned': $milestone = idxv($raw, array('milestone', 'title')); $title = pht('Removed Milestone: %s', $milestone); break; case 'labeled': $label = idxv($raw, array('label', 'name')); $title = pht('Added Label: %s', $label); break; case 'locked': $title = pht('Locked'); break; case 'milestoned': $milestone = idxv($raw, array('milestone', 'title')); $title = pht('Added Milestone: %s', $milestone); break; case 'renamed': $title = pht('Renamed'); break; case 'reopened': $title = pht('Reopened'); break; case 'unassigned': $assignee = idxv($raw, array('assignee', 'login')); $title = pht('Unassigned: %s', $assignee); break; case 'unlabeled': $label = idxv($raw, array('label', 'name')); $title = pht('Removed Label: %s', $label); break; case 'unlocked': $title = pht('Unlocked'); break; default: $title = pht('"%s"', $action); break; } return $title; } private function getRawRepositoryEventTitle() { $raw = $this->raw; $type = idx($raw, 'type'); switch ($type) { case 'CreateEvent': return pht('Created'); case 'PushEvent': $head = idxv($raw, array('payload', 'head')); $head = substr($head, 0, 12); return pht('Pushed: %s', $head); case 'IssuesEvent': $action = idxv($raw, array('payload', 'action')); switch ($action) { case 'closed': return pht('Closed'); case 'opened': return pht('Created'); case 'reopened': return pht('Reopened'); default: return pht('"%s"', $action); } break; case 'IssueCommentEvent': $action = idxv($raw, array('payload', 'action')); switch ($action) { case 'created': return pht('Comment'); default: return pht('"%s"', $action); } break; case 'PullRequestEvent': $action = idxv($raw, array('payload', 'action')); switch ($action) { case 'opened': return pht('Created'); default: return pht('"%s"', $action); } break; case 'WatchEvent': return pht('Watched'); } return pht('"%s"', $type); } } diff --git a/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php b/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php index f5e2119141..5bdc3f34aa 100644 --- a/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php +++ b/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php @@ -1,111 +1,112 @@ readTestCases($path); foreach ($cases as $name => $info) { $input = $info['input']; $expect = $info['expect']; $event = NuanceGitHubRawEvent::newEvent( NuanceGitHubRawEvent::TYPE_ISSUE, $input); $this->assertGitHubRawEventParse($expect, $event, $name); } } public function testRepositoryEvents() { $path = dirname(__FILE__).'/repositoryevents/'; $cases = $this->readTestCases($path); foreach ($cases as $name => $info) { $input = $info['input']; $expect = $info['expect']; $event = NuanceGitHubRawEvent::newEvent( NuanceGitHubRawEvent::TYPE_REPOSITORY, $input); $this->assertGitHubRawEventParse($expect, $event, $name); } } private function assertGitHubRawEventParse( array $expect, NuanceGitHubRawEvent $event, $name) { $actual = array( 'repository.name.full' => $event->getRepositoryFullName(), 'is.issue' => $event->isIssueEvent(), 'is.pull' => $event->isPullRequestEvent(), 'issue.number' => $event->getIssueNumber(), 'pull.number' => $event->getPullRequestNumber(), 'id' => $event->getID(), 'uri' => $event->getURI(), 'title.full' => $event->getEventFullTitle(), + 'comment' => $event->getComment(), ); // Only verify the keys which are actually present in the test. This // allows tests to specify only relevant keys. $actual = array_select_keys($actual, array_keys($expect)); ksort($expect); ksort($actual); $this->assertEqual($expect, $actual, $name); } private function readTestCases($path) { $files = Filesystem::listDirectory($path, $include_hidden = false); $tests = array(); foreach ($files as $file) { $data = Filesystem::readFile($path.$file); $parts = preg_split('/^~{5,}$/m', $data); if (count($parts) < 2) { throw new Exception( pht( 'Expected test file "%s" to contain an input section in JSON, '. 'then an expected result section in JSON, with the two sections '. 'separated by a line of "~~~~~", but the divider is not present '. 'in the file.', $file)); } else if (count($parts) > 2) { throw new Exception( pht( 'Expected test file "%s" to contain exactly two sections, '. 'but it has more than two sections.')); } list($input, $expect) = $parts; try { $input = phutil_json_decode($input); $expect = phutil_json_decode($expect); } catch (Exception $ex) { throw new PhutilProxyException( pht( 'Exception while decoding test data for test "%s".', $file), $ex); } $tests[$file] = array( 'input' => $input, 'expect' => $expect, ); } return $tests; } } diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt index 71abbceac4..1991bb568e 100644 --- a/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt @@ -1,164 +1,165 @@ { "id": "3740938746", "type": "IssueCommentEvent", "actor": { "id": 102631, "login": "epriestley", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "avatar_url": "https://avatars.githubusercontent.com/u/102631?" }, "repo": { "id": 14627834, "name": "epriestley/poems", "url": "https://api.github.com/repos/epriestley/poems" }, "payload": { "action": "created", "issue": { "url": "https://api.github.com/repos/epriestley/poems/issues/2", "repository_url": "https://api.github.com/repos/epriestley/poems", "labels_url": "https://api.github.com/repos/epriestley/poems/issues/2/labels{/name}", "comments_url": "https://api.github.com/repos/epriestley/poems/issues/2/comments", "events_url": "https://api.github.com/repos/epriestley/poems/issues/2/events", "html_url": "https://github.com/epriestley/poems/pull/2", "id": 139568860, "number": 2, "title": "Please Merge Quack2 into Feature", "user": { "login": "epriestley", "id": 102631, "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "html_url": "https://github.com/epriestley", "followers_url": "https://api.github.com/users/epriestley/followers", "following_url": "https://api.github.com/users/epriestley/following{/other_user}", "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", "organizations_url": "https://api.github.com/users/epriestley/orgs", "repos_url": "https://api.github.com/users/epriestley/repos", "events_url": "https://api.github.com/users/epriestley/events{/privacy}", "received_events_url": "https://api.github.com/users/epriestley/received_events", "type": "User", "site_admin": false }, "labels": [ { "url": "https://api.github.com/repos/epriestley/poems/labels/bug", "name": "bug", "color": "fc2929" } ], "state": "open", "locked": false, "assignee": { "login": "epriestley", "id": 102631, "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "html_url": "https://github.com/epriestley", "followers_url": "https://api.github.com/users/epriestley/followers", "following_url": "https://api.github.com/users/epriestley/following{/other_user}", "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", "organizations_url": "https://api.github.com/users/epriestley/orgs", "repos_url": "https://api.github.com/users/epriestley/repos", "events_url": "https://api.github.com/users/epriestley/events{/privacy}", "received_events_url": "https://api.github.com/users/epriestley/received_events", "type": "User", "site_admin": false }, "milestone": { "url": "https://api.github.com/repos/epriestley/poems/milestones/1", "html_url": "https://github.com/epriestley/poems/milestones/b", "labels_url": "https://api.github.com/repos/epriestley/poems/milestones/1/labels", "id": 1633589, "number": 1, "title": "b", "description": null, "creator": { "login": "epriestley", "id": 102631, "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "html_url": "https://github.com/epriestley", "followers_url": "https://api.github.com/users/epriestley/followers", "following_url": "https://api.github.com/users/epriestley/following{/other_user}", "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", "organizations_url": "https://api.github.com/users/epriestley/orgs", "repos_url": "https://api.github.com/users/epriestley/repos", "events_url": "https://api.github.com/users/epriestley/events{/privacy}", "received_events_url": "https://api.github.com/users/epriestley/received_events", "type": "User", "site_admin": false }, "open_issues": 1, "closed_issues": 0, "state": "open", "created_at": "2016-03-09T12:42:50Z", "updated_at": "2016-03-09T12:52:41Z", "due_on": null, "closed_at": null }, "comments": 1, "created_at": "2016-03-09T12:52:31Z", "updated_at": "2016-03-09T12:53:06Z", "closed_at": null, "pull_request": { "url": "https://api.github.com/repos/epriestley/poems/pulls/2", "html_url": "https://github.com/epriestley/poems/pull/2", "diff_url": "https://github.com/epriestley/poems/pull/2.diff", "patch_url": "https://github.com/epriestley/poems/pull/2.patch" }, "body": "" }, "comment": { "url": "https://api.github.com/repos/epriestley/poems/issues/comments/194282800", "html_url": "https://github.com/epriestley/poems/pull/2#issuecomment-194282800", "issue_url": "https://api.github.com/repos/epriestley/poems/issues/2", "id": 194282800, "user": { "login": "epriestley", "id": 102631, "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "html_url": "https://github.com/epriestley", "followers_url": "https://api.github.com/users/epriestley/followers", "following_url": "https://api.github.com/users/epriestley/following{/other_user}", "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", "organizations_url": "https://api.github.com/users/epriestley/orgs", "repos_url": "https://api.github.com/users/epriestley/repos", "events_url": "https://api.github.com/users/epriestley/events{/privacy}", "received_events_url": "https://api.github.com/users/epriestley/received_events", "type": "User", "site_admin": false }, "created_at": "2016-03-09T12:53:06Z", "updated_at": "2016-03-09T12:53:06Z", "body": "wub wub" } }, "public": true, "created_at": "2016-03-09T12:53:06Z" } ~~~~~ { "repository.name.full": "epriestley/poems", "is.issue": false, "is.pull": true, "issue.number": null, "pull.number": 2, "id": 3740938746, "uri": "https://github.com/epriestley/poems/pull/2#issuecomment-194282800", - "title.full": "GitHub epriestley/poems Pull Request #2 (Comment)" + "title.full": "GitHub epriestley/poems Pull Request #2 (Comment)", + "comment": "wub wub" } diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt index a1ca094045..d22fd86e25 100644 --- a/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt @@ -1,101 +1,102 @@ { "id": "3733510485", "type": "IssueCommentEvent", "actor": { "id": 102631, "login": "epriestley", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "avatar_url": "https://avatars.githubusercontent.com/u/102631?" }, "repo": { "id": 14627834, "name": "epriestley/poems", "url": "https://api.github.com/repos/epriestley/poems" }, "payload": { "action": "created", "issue": { "url": "https://api.github.com/repos/epriestley/poems/issues/1", "repository_url": "https://api.github.com/repos/epriestley/poems", "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", "html_url": "https://github.com/epriestley/poems/issues/1", "id": 139138813, "number": 1, "title": "Enforce haiku in commit messages", "user": { "login": "epriestley", "id": 102631, "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "html_url": "https://github.com/epriestley", "followers_url": "https://api.github.com/users/epriestley/followers", "following_url": "https://api.github.com/users/epriestley/following{/other_user}", "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", "organizations_url": "https://api.github.com/users/epriestley/orgs", "repos_url": "https://api.github.com/users/epriestley/repos", "events_url": "https://api.github.com/users/epriestley/events{/privacy}", "received_events_url": "https://api.github.com/users/epriestley/received_events", "type": "User", "site_admin": false }, "labels": [ ], "state": "open", "locked": false, "assignee": null, "milestone": null, "comments": 1, "created_at": "2016-03-08T00:41:08Z", "updated_at": "2016-03-08T00:41:22Z", "closed_at": null, "body": "OK" }, "comment": { "url": "https://api.github.com/repos/epriestley/poems/issues/comments/193528669", "html_url": "https://github.com/epriestley/poems/issues/1#issuecomment-193528669", "issue_url": "https://api.github.com/repos/epriestley/poems/issues/1", "id": 193528669, "user": { "login": "epriestley", "id": 102631, "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", "gravatar_id": "", "url": "https://api.github.com/users/epriestley", "html_url": "https://github.com/epriestley", "followers_url": "https://api.github.com/users/epriestley/followers", "following_url": "https://api.github.com/users/epriestley/following{/other_user}", "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", "organizations_url": "https://api.github.com/users/epriestley/orgs", "repos_url": "https://api.github.com/users/epriestley/repos", "events_url": "https://api.github.com/users/epriestley/events{/privacy}", "received_events_url": "https://api.github.com/users/epriestley/received_events", "type": "User", "site_admin": false }, "created_at": "2016-03-08T00:41:22Z", "updated_at": "2016-03-08T00:41:22Z", "body": "comment on issue" } }, "public": true, "created_at": "2016-03-08T00:41:22Z" } ~~~~~ { "repository.name.full": "epriestley/poems", "is.issue": true, "is.pull": false, "issue.number": 1, "id": 3733510485, "uri": "https://github.com/epriestley/poems/issues/1#issuecomment-193528669", - "title.full": "GitHub epriestley/poems Issue #1 (Comment)" + "title.full": "GitHub epriestley/poems Issue #1 (Comment)", + "comment": "comment on issue" } diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php index 75f44e8964..617d75e493 100644 --- a/src/applications/nuance/item/NuanceGitHubEventItemType.php +++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php @@ -1,347 +1,370 @@ newRawEvent($item)->getEventFullTitle(); } public function canUpdateItems() { return true; } protected function updateItemFromSource(NuanceItem $item) { $viewer = $this->getViewer(); $is_dirty = false; // TODO: Link up the requestor, etc. $is_dirty = false; $xobj = $this->reloadExternalObject($item); if ($xobj) { $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); $is_dirty = true; } if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) { $item->setStatus(NuanceItem::STATUS_ROUTING); $is_dirty = true; } if ($is_dirty) { $item->save(); } } private function getDoorkeeperRef(NuanceItem $item) { $raw = $this->newRawEvent($item); $full_repository = $raw->getRepositoryFullName(); if (!strlen($full_repository)) { return null; } if ($raw->isIssueEvent()) { $ref_type = DoorkeeperBridgeGitHubIssue::OBJTYPE_GITHUB_ISSUE; $issue_number = $raw->getIssueNumber(); $full_ref = "{$full_repository}#{$issue_number}"; } else { return null; } return id(new DoorkeeperObjectRef()) ->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB) ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB) ->setObjectType($ref_type) ->setObjectID($full_ref); } private function reloadExternalObject(NuanceItem $item, $local = false) { $ref = $this->getDoorkeeperRef($item); if (!$ref) { return null; } $source = $item->getSource(); $token = $source->getSourceProperty('github.token'); $token = new PhutilOpaqueEnvelope($token); $viewer = $this->getViewer(); $ref = id(new DoorkeeperImportEngine()) ->setViewer($viewer) ->setRefs(array($ref)) ->setThrowOnMissingLink(true) ->setContextProperty('github.token', $token) ->needLocalOnly($local) ->executeOne(); if ($ref->getSyncFailed()) { $xobj = null; } else { $xobj = $ref->getExternalObject(); } if ($xobj) { $this->externalObject = $xobj; } return $xobj; } private function getExternalObject(NuanceItem $item) { if ($this->externalObject === null) { $xobj = $this->reloadExternalObject($item, $local = true); if ($xobj) { $this->externalObject = $xobj; } else { $this->externalObject = false; } } if ($this->externalObject) { return $this->externalObject; } return null; } private function newRawEvent(NuanceItem $item) { $type = $item->getItemProperty('api.type'); $raw = $item->getItemProperty('api.raw', array()); return NuanceGitHubRawEvent::newEvent($type, $raw); } public function getItemActions(NuanceItem $item) { $actions = array(); $xobj = $this->getExternalObject($item); if ($xobj) { $actions[] = $this->newItemAction($item, 'reload') ->setName(pht('Reload from GitHub')) ->setIcon('fa-refresh') ->setWorkflow(true) ->setRenderAsForm(true); } $actions[] = $this->newItemAction($item, 'sync') ->setName(pht('Import to Maniphest')) ->setIcon('fa-anchor') ->setWorkflow(true) ->setRenderAsForm(true); $actions[] = $this->newItemAction($item, 'raw') ->setName(pht('View Raw Event')) ->setWorkflow(true) ->setIcon('fa-code'); return $actions; } + public function getItemCurtainPanels(NuanceItem $item) { + $viewer = $this->getViewer(); + + $panels = array(); + + $xobj = $this->getExternalObject($item); + if ($xobj) { + $xobj_phid = $xobj->getPHID(); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withBridgedObjectPHIDs(array($xobj_phid)) + ->executeOne(); + if ($task) { + $panels[] = $this->newCurtainPanel($item) + ->setHeaderText(pht('Imported As')) + ->appendChild($viewer->renderHandle($task->getPHID())); + } + } + + return $panels; + } + protected function handleAction(NuanceItem $item, $action) { $viewer = $this->getViewer(); $controller = $this->getController(); switch ($action) { case 'raw': $raw = array( 'api.type' => $item->getItemProperty('api.type'), 'api.raw' => $item->getItemProperty('api.raw'), ); $raw_output = id(new PhutilJSON())->encodeFormatted($raw); $raw_box = id(new AphrontFormTextAreaControl()) ->setCustomClass('PhabricatorMonospaced') ->setLabel(pht('Raw Event')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($raw_output); $form = id(new AphrontFormView()) ->appendChild($raw_box); return $controller->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('GitHub Raw Event')) ->appendForm($form) ->addCancelButton($item->getURI(), pht('Done')); case 'sync': case 'reload': $item->issueCommand($viewer->getPHID(), $action); return id(new AphrontRedirectResponse())->setURI($item->getURI()); } return null; } protected function newItemView(NuanceItem $item) { $content = array(); $content[] = $this->newGitHubEventItemPropertyBox($item); return $content; } private function newGitHubEventItemPropertyBox($item) { $viewer = $this->getViewer(); $property_list = id(new PHUIPropertyListView()) ->setViewer($viewer); $event = $this->newRawEvent($item); $property_list->addProperty( pht('GitHub Event ID'), $event->getID()); $event_uri = $event->getURI(); if ($event_uri && PhabricatorEnv::isValidRemoteURIForLink($event_uri)) { $event_uri = phutil_tag( 'a', array( 'href' => $event_uri, ), $event_uri); } if ($event_uri) { $property_list->addProperty( pht('GitHub Event URI'), $event_uri); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Event Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($property_list); } protected function handleCommand( NuanceItem $item, NuanceItemCommand $command) { $action = $command->getCommand(); switch ($action) { case 'sync': return $this->syncItem($item, $command); case 'reload': $this->reloadExternalObject($item); return true; } return null; } private function syncItem( NuanceItem $item, NuanceItemCommand $command) { $xobj_phid = $item->getItemProperty('doorkeeper.xobj.phid'); if (!$xobj_phid) { throw new Exception( pht( 'Unable to sync: no external object PHID.')); } // TODO: Write some kind of marker to prevent double-synchronization. $viewer = $this->getViewer(); $xobj = id(new DoorkeeperExternalObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($xobj_phid)) ->executeOne(); if (!$xobj) { throw new Exception( pht( 'Unable to sync: failed to load object "%s".', $xobj_phid)); } $nuance_phid = id(new PhabricatorNuanceApplication())->getPHID(); $xactions = array(); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withBridgedObjectPHIDs(array($xobj_phid)) ->executeOne(); if (!$task) { $task = ManiphestTask::initializeNewTask($viewer) ->setAuthorPHID($nuance_phid) ->setBridgedObjectPHID($xobj_phid); $title = $xobj->getProperty('task.title'); if (!strlen($title)) { $title = pht('Nuance Item %d Task', $item->getID()); } $description = $xobj->getProperty('task.description'); $created = $xobj->getProperty('task.created'); $state = $xobj->getProperty('task.state'); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_TITLE) ->setNewValue($title) ->setDateCreated($created); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) ->setNewValue($description) ->setDateCreated($created); $task->setDateCreated($created); // TODO: Synchronize state. } $event = $this->newRawEvent($item); $comment = $event->getComment(); if (strlen($comment)) { $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent($comment)); } // TODO: Preserve the item's original source. $source = PhabricatorContentSource::newForSource( PhabricatorDaemonContentSource::SOURCECONST); // TODO: This should really be the external source. $acting_phid = $nuance_phid; $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) ->setActingAsPHID($acting_phid) ->setContentSource($source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = $editor->applyTransactions($task, $xactions); return array( 'objectPHID' => $task->getPHID(), 'xactionPHIDs' => mpull($xactions, 'getPHID'), ); } } diff --git a/src/applications/nuance/item/NuanceItemType.php b/src/applications/nuance/item/NuanceItemType.php index d4187bf418..a1186c6ddd 100644 --- a/src/applications/nuance/item/NuanceItemType.php +++ b/src/applications/nuance/item/NuanceItemType.php @@ -1,139 +1,147 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } public function canUpdateItems() { return false; } final public function buildItemView(NuanceItem $item) { return $this->newItemView($item); } protected function newItemView(NuanceItem $item) { return null; } public function getItemTypeDisplayIcon() { return null; } public function getItemActions(NuanceItem $item) { return array(); } + public function getItemCurtainPanels(NuanceItem $item) { + return array(); + } + abstract public function getItemTypeDisplayName(); abstract public function getItemDisplayName(NuanceItem $item); final public function updateItem(NuanceItem $item) { if (!$this->canUpdateItems()) { throw new Exception( pht( 'This item type ("%s", of class "%s") can not update items.', $this->getItemTypeConstant(), get_class($this))); } $this->updateItemFromSource($item); } protected function updateItemFromSource(NuanceItem $item) { throw new PhutilMethodNotImplementedException(); } final public function getItemTypeConstant() { return $this->getPhobjectClassConstant('ITEMTYPE', 64); } final public static function getAllItemTypes() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getItemTypeConstant') ->execute(); } final protected function newItemAction(NuanceItem $item, $key) { $id = $item->getID(); $action_uri = "/nuance/item/action/{$id}/{$key}/"; return id(new PhabricatorActionView()) ->setHref($action_uri); } + final protected function newCurtainPanel(NuanceItem $item) { + return id(new PHUICurtainPanelView()); + } + final public function buildActionResponse(NuanceItem $item, $action) { $response = $this->handleAction($item, $action); if ($response === null) { return new Aphront404Response(); } return $response; } protected function handleAction(NuanceItem $item, $action) { return null; } final public function applyCommand( NuanceItem $item, NuanceItemCommand $command) { $result = $this->handleCommand($item, $command); if ($result === null) { return; } $xaction = id(new NuanceItemTransaction()) ->setTransactionType(NuanceItemTransaction::TYPE_COMMAND) ->setNewValue( array( 'command' => $command->getCommand(), 'parameters' => $command->getParameters(), 'result' => $result, )); $viewer = $this->getViewer(); // TODO: Maybe preserve the actor's original content source? $source = PhabricatorContentSource::newForSource( PhabricatorDaemonContentSource::SOURCECONST); $editor = id(new NuanceItemEditor()) ->setActor($viewer) ->setActingAsPHID($command->getAuthorPHID()) ->setContentSource($source) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, array($xaction)); } protected function handleCommand( NuanceItem $item, NuanceItemCommand $command) { return null; } }