diff --git a/CHANGELOG.md b/CHANGELOG.md
index b452086..69ec6ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- Hyperlink to GitHub entities (#860)
- Add GitHub reactions to the graph (#846)
- Detect references to commits (#833)
- Detect references in commit messages (#829)
diff --git a/src/app/adapters/pluginAdapter.js b/src/app/adapters/pluginAdapter.js
index 5c3088c..532cbb1 100644
--- a/src/app/adapters/pluginAdapter.js
+++ b/src/app/adapters/pluginAdapter.js
@@ -1,5 +1,6 @@
// @flow
+import {type Node as ReactNode} from "react";
import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph";
import type {Assets} from "../assets";
import type {Repo} from "../../core/repo";
@@ -30,6 +31,6 @@ export interface StaticPluginAdapter {
export interface DynamicPluginAdapter {
graph(): Graph;
- nodeDescription(NodeAddressT): string;
+ nodeDescription(NodeAddressT): ReactNode;
static (): StaticPluginAdapter;
}
diff --git a/src/app/credExplorer/pagerankTable/shared.js b/src/app/credExplorer/pagerankTable/shared.js
index 28ddf00..6d3fe5d 100644
--- a/src/app/credExplorer/pagerankTable/shared.js
+++ b/src/app/credExplorer/pagerankTable/shared.js
@@ -14,7 +14,7 @@ import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerank
export function nodeDescription(
address: NodeAddressT,
adapters: DynamicAdapterSet
-): string {
+): ReactNode {
const adapter = adapters.adapterMatchingNode(address);
try {
return adapter.nodeDescription(address);
diff --git a/src/plugins/github/__snapshots__/render.test.js.snap b/src/plugins/github/__snapshots__/render.test.js.snap
index 7cd00cb..fb782e0 100644
--- a/src/plugins/github/__snapshots__/render.test.js.snap
+++ b/src/plugins/github/__snapshots__/render.test.js.snap
@@ -1,13 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`plugins/github/render descriptions are as expected 1`] = `
-Object {
- "comment": "comment by @wchargin on review by @wchargin of #5 (+1/−0): This pull request will be more contentious. I can feel it...",
- "commit": "commit 0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
- "issue": "#2: A referencing issue.",
- "pull": "#5 (+1/−0): This pull request will be more contentious. I can feel it...",
- "repo": "sourcecred/example-github",
- "review": "review by @wchargin of #5 (+1/−0): This pull request will be more contentious. I can feel it...",
- "userlike": "@wchargin",
-}
+exports[`plugins/github/render renders the right description for a comment 1`] = `
+
+
+ comment
+
+ on
+
+
+ review
+
+ on
+
+
+ #5
+
+
+
+ (
+
+ +1
+
+ /
+
+ −0
+
+ )
+
+ : This pull request will be more contentious. I can feel it...
+
+
+
+`;
+
+exports[`plugins/github/render renders the right description for a commit 1`] = `
+
+ Commit
+
+ 0a22334
+
+
+`;
+
+exports[`plugins/github/render renders the right description for a issue 1`] = `
+
+
+ #2
+
+ : A referencing issue.
+
+`;
+
+exports[`plugins/github/render renders the right description for a pull 1`] = `
+
+
+ #5
+
+
+
+ (
+
+ +1
+
+ /
+
+ −0
+
+ )
+
+ : This pull request will be more contentious. I can feel it...
+
+`;
+
+exports[`plugins/github/render renders the right description for a repo 1`] = `
+
+ sourcecred/example-github
+
+`;
+
+exports[`plugins/github/render renders the right description for a review 1`] = `
+
+
+ review
+
+ on
+
+
+ #5
+
+
+
+ (
+
+ +1
+
+ /
+
+ −0
+
+ )
+
+ : This pull request will be more contentious. I can feel it...
+
+
+`;
+
+exports[`plugins/github/render renders the right description for a userlike 1`] = `
+
+ @wchargin
+
`;
diff --git a/src/plugins/github/render.js b/src/plugins/github/render.js
index 62d9d72..1a611f8 100644
--- a/src/plugins/github/render.js
+++ b/src/plugins/github/render.js
@@ -1,31 +1,96 @@
// @flow
+import React, {type Node as ReactNode} from "react";
import * as R from "./relationalView";
-export function description(e: R.Entity) {
- const withAuthors = (x: R.AuthoredEntity) => {
- const authors = Array.from(x.authors());
- if (authors.length === 0) {
- // ghost author - probably a deleted account
- return "";
- }
- return "by " + authors.map((x) => description(x)).join(" & ") + " ";
- };
+function EntityUrl(props) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+function repo(x: R.Repo) {
+ return (
+
+ {x.owner()}/{x.name()}
+
+ );
+}
+
+function hyperlinkedNumber(x: R.Issue | R.Pull) {
+ return #{x.number()};
+}
+
+function issue(x: R.Issue) {
+ return (
+
+ {hyperlinkedNumber(x)}: {x.title()}
+
+ );
+}
+
+function pull(x: R.Pull) {
+ const additions = +{x.additions()};
+ const deletions = −{x.deletions()};
+ const diff = (
+
+ ({additions}/{deletions})
+
+ );
+ return (
+
+ {hyperlinkedNumber(x)} {diff}: {x.title()}
+
+ );
+}
+
+function review(x: R.Review) {
+ const leader = review;
+ return (
+
+ {leader} on {description(x.parent())}
+
+ );
+}
+
+function comment(x: R.Comment) {
+ const leader = comment;
+ return (
+
+ {leader} on {description(x.parent())}
+
+ );
+}
+
+function userlike(x: R.Userlike) {
+ return @{x.login()};
+}
+
+// The commit type is included for completeness's sake and to
+// satisfy the typechecker, but won't ever be seen in the frontend
+// because the commit has a Git plugin prefix and will therefore by
+// handled by the git plugin adapter
+function commit(x: R.Commit) {
+ // TODO(@wchargin): Ensure this hash is unambiguous
+ const shortHash = x.address().hash.slice(0, 7);
+ return (
+
+ Commit {shortHash}
+
+ );
+}
+
+export function description(e: R.Entity): ReactNode {
const handlers = {
- repo: (x) => `${x.owner()}/${x.name()}`,
- issue: (x) => `#${x.number()}: ${x.title()}`,
- pull: (x) => {
- const diff = `+${x.additions()}/\u2212${x.deletions()}`;
- return `#${x.number()} (${diff}): ${x.title()}`;
- },
- review: (x) => `review ${withAuthors(x)}of ${description(x.parent())}`,
- comment: (x) => `comment ${withAuthors(x)}on ${description(x.parent())}`,
- // The commit type is included for completeness's sake and to
- // satisfy the typechecker, but won't ever be seen in the frontend
- // because the commit has a Git plugin prefix and will therefore by
- // handled by the git plugin adapter
- commit: (x) => `commit ${x.address().hash}`,
- userlike: (x) => `@${x.login()}`,
+ repo,
+ issue,
+ pull,
+ review,
+ comment,
+ commit,
+ userlike,
};
return R.match(handlers, e);
}
diff --git a/src/plugins/github/render.test.js b/src/plugins/github/render.test.js
index e074581..2e7da9b 100644
--- a/src/plugins/github/render.test.js
+++ b/src/plugins/github/render.test.js
@@ -1,16 +1,19 @@
// @flow
+import {render} from "enzyme";
import {exampleEntities} from "./example/example";
import {description} from "./render";
+import enzymeToJSON from "enzyme-to-json";
+
+require("../../app/testUtil").configureEnzyme();
describe("plugins/github/render", () => {
- it("descriptions are as expected", () => {
- const examples = exampleEntities();
- const withDescriptions = {};
- for (const name of Object.keys(exampleEntities())) {
+ const examples = exampleEntities();
+ for (const name of Object.keys(examples)) {
+ it(`renders the right description for a ${name}`, () => {
const entity = examples[name];
- withDescriptions[name] = description(entity);
- }
- expect(withDescriptions).toMatchSnapshot();
- });
+ const renderedEntity = render(description(entity));
+ expect(enzymeToJSON(renderedEntity)).toMatchSnapshot();
+ });
+ }
});