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(); + }); + } });