Remove the artifact plugin (#303)
Given that we are undergoing a major world-changing refactor (#190), all outstanding code needs to be refactored to use the new conventions. We don't actually use the Artifact Plugin yet, and reading the code, it needs non-trivial rewrites to be in sync with the new world. Rather than maintain it now, I am deleting it; we can regain the context when the time is ripe to setup and integrate the plugin. Test plan: Travis passes. `yarn start` produces no references to the artifact editor.
This commit is contained in:
parent
f0fcf02791
commit
c68b78f959
|
@ -3,12 +3,10 @@
|
|||
import React from "react";
|
||||
import {BrowserRouter as Router, Route, NavLink} from "react-router-dom";
|
||||
|
||||
import ArtifactEditor from "../plugins/artifact/editor/App";
|
||||
import CredExplorer from "./credExplorer/App";
|
||||
|
||||
export default class App extends React.Component<{}> {
|
||||
render() {
|
||||
const ARTIFACT_EDITOR_ROUTE = "/plugins/artifact/editor";
|
||||
const CRED_EXPLORER_ROUTE = "/explorer";
|
||||
return (
|
||||
<Router>
|
||||
|
@ -21,16 +19,12 @@ export default class App extends React.Component<{}> {
|
|||
<li>
|
||||
<NavLink to={CRED_EXPLORER_ROUTE}>Cred Explorer</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to={ARTIFACT_EDITOR_ROUTE}>Artifact Editor</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<hr />
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path={CRED_EXPLORER_ROUTE} component={CredExplorer} />
|
||||
<Route path={ARTIFACT_EDITOR_ROUTE} component={ArtifactEditor} />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import type {Address} from "../../core/address";
|
||||
import type {Graph} from "../../core/graph";
|
||||
|
||||
export const ARTIFACT_PLUGIN_NAME = "sourcecred/artifact-beta";
|
||||
|
||||
export const ARTIFACT_NODE_TYPE = "ARTIFACT";
|
||||
export type ArtifactNodePayload = {|
|
||||
+name: string,
|
||||
+description: string,
|
||||
|};
|
||||
|
||||
export type NodePayload = ArtifactNodePayload;
|
||||
|
||||
export const INCLUDES_EDGE_TYPE = "INCLUDES";
|
||||
export type IncludesEdgePayload = {|
|
||||
+weight: number, // non-negative
|
||||
|};
|
||||
|
||||
export type EdgePayload = IncludesEdgePayload;
|
||||
|
||||
const NON_SLUG_CHARACTER: RegExp = /[^a-z]/g;
|
||||
|
||||
export function artifactAddress(
|
||||
graph: Graph,
|
||||
repoOwner: string,
|
||||
repoName: string,
|
||||
artifactName: string
|
||||
): Address {
|
||||
const baseName = artifactName.toLowerCase().replace(NON_SLUG_CHARACTER, "-");
|
||||
const baseId = `${repoOwner}/${repoName}/${baseName}`;
|
||||
function address(id) {
|
||||
return {
|
||||
pluginName: ARTIFACT_PLUGIN_NAME,
|
||||
id,
|
||||
type: ARTIFACT_NODE_TYPE,
|
||||
};
|
||||
}
|
||||
let id = baseId;
|
||||
for (let i = 0; graph.node(address(id)) != null; i++) {
|
||||
id = baseId + "-" + i;
|
||||
}
|
||||
return address(id);
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import {Graph} from "../../core/graph";
|
||||
import {artifactAddress} from "./artifactPlugin";
|
||||
|
||||
describe("artifactPlugin", () => {
|
||||
describe("artifactAddress", () => {
|
||||
it("repositoryName included in id", () => {
|
||||
const a = artifactAddress(
|
||||
new Graph(),
|
||||
"not-sourcecred",
|
||||
"not-artifact-plugin",
|
||||
"Sample artifact!"
|
||||
);
|
||||
expect(a.id.startsWith("not-sourcecred/not-artifact-plugin")).toBe(true);
|
||||
});
|
||||
|
||||
it("slugifies the artifact name", () => {
|
||||
const a = artifactAddress(
|
||||
new Graph(),
|
||||
"not-sourcecred",
|
||||
"not-artifact-plugin",
|
||||
"Sample artifact!"
|
||||
);
|
||||
expect(a.id).toEqual(
|
||||
"not-sourcecred/not-artifact-plugin/sample-artifact-"
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves collisions", () => {
|
||||
const g = new Graph();
|
||||
const ids = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a = artifactAddress(
|
||||
g,
|
||||
"not-sourcecred",
|
||||
"not-artifact-plugin",
|
||||
"Sample artifact!"
|
||||
);
|
||||
ids.push(a.id);
|
||||
g.addNode({
|
||||
address: a,
|
||||
payload: {name: "Sample artifact!", description: ""},
|
||||
});
|
||||
}
|
||||
expect(ids).toEqual([
|
||||
"not-sourcecred/not-artifact-plugin/sample-artifact-",
|
||||
"not-sourcecred/not-artifact-plugin/sample-artifact--0",
|
||||
"not-sourcecred/not-artifact-plugin/sample-artifact--1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {StyleSheet, css} from "aphrodite/no-important";
|
||||
|
||||
import "./pluginAdapter";
|
||||
|
||||
import type {Graph, Node} from "../../../core/graph";
|
||||
import type {NodePayload as ArtifactNodePayload} from "../artifactPlugin";
|
||||
import type {Settings} from "./SettingsConfig";
|
||||
import {ArtifactGraphEditor} from "./ArtifactGraphEditor";
|
||||
import {ContributionList} from "./ContributionList";
|
||||
import {GithubGraphFetcher} from "./GithubGraphFetcher";
|
||||
import {SettingsConfig, defaultSettings} from "./SettingsConfig";
|
||||
import standardAdapterSet from "./standardAdapterSet";
|
||||
|
||||
type Props = {};
|
||||
type State = {
|
||||
artifacts: Node<ArtifactNodePayload>[],
|
||||
githubGraph: ?Graph,
|
||||
artifactGraph: ?Graph,
|
||||
settings: Settings,
|
||||
};
|
||||
|
||||
export default class App extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
artifacts: [],
|
||||
githubGraph: null,
|
||||
artifactGraph: null,
|
||||
settings: defaultSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<header className={css(styles.header)}>
|
||||
<h1>Artifact editor</h1>
|
||||
</header>
|
||||
<SettingsConfig
|
||||
onChange={(settings) => {
|
||||
this.setState({settings});
|
||||
}}
|
||||
/>
|
||||
<GithubGraphFetcher
|
||||
settings={this.state.settings}
|
||||
onCreateGraph={(githubGraph) => {
|
||||
this.setState({githubGraph});
|
||||
}}
|
||||
/>
|
||||
<ArtifactGraphEditor
|
||||
settings={this.state.settings}
|
||||
onChange={(artifactGraph) => {
|
||||
this.setState({artifactGraph});
|
||||
}}
|
||||
/>
|
||||
<ContributionList
|
||||
graph={this.state.githubGraph}
|
||||
adapters={standardAdapterSet}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
color: "#f0f",
|
||||
},
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
require("../../../app/testUtil").configureAphrodite();
|
||||
|
||||
// Check that PropTypes check out.
|
||||
it("renders without crashing", () => {
|
||||
const div = document.createElement("div");
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
|
@ -1,124 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type {Node} from "../../../core/graph";
|
||||
import type {Settings} from "./SettingsConfig";
|
||||
import type {NodePayload} from "../artifactPlugin";
|
||||
import {Graph} from "../../../core/graph";
|
||||
import {artifactAddress} from "../artifactPlugin";
|
||||
|
||||
type Props = {
|
||||
settings: Settings,
|
||||
onChange: (Graph) => void,
|
||||
};
|
||||
type State = {
|
||||
graph: Graph,
|
||||
artifactInProgressName: string,
|
||||
};
|
||||
|
||||
export class ArtifactGraphEditor extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
graph: new Graph(),
|
||||
artifactInProgressName: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onChange(this.state.graph);
|
||||
}
|
||||
|
||||
addArtifact(name: string): void {
|
||||
this.setState(
|
||||
(state) => {
|
||||
const node: Node<NodePayload> = {
|
||||
address: artifactAddress(
|
||||
state.graph,
|
||||
this.props.settings.repoOwner,
|
||||
this.props.settings.repoName,
|
||||
name
|
||||
),
|
||||
payload: {name, description: ""},
|
||||
};
|
||||
return {graph: state.graph.copy().addNode(node)};
|
||||
},
|
||||
() => {
|
||||
this.props.onChange(this.state.graph);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateArtifactDescription(
|
||||
oldArtifactNode: Node<NodePayload>,
|
||||
newDescription: string
|
||||
): void {
|
||||
this.setState(
|
||||
(state) => ({
|
||||
graph: state.graph
|
||||
.copy()
|
||||
.removeNode(oldArtifactNode.address)
|
||||
.addNode({
|
||||
address: oldArtifactNode.address,
|
||||
payload: {
|
||||
name: oldArtifactNode.payload.name,
|
||||
description: newDescription,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
() => {
|
||||
this.props.onChange(this.state.graph);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Artifacts</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artifact</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.graph.nodes().map((x) => (
|
||||
<tr key={x.address.id}>
|
||||
<td>{x.payload.name}</td>
|
||||
<td>
|
||||
<textarea
|
||||
key={`description-${x.address.id}`}
|
||||
value={x.payload.description}
|
||||
onChange={(e) => {
|
||||
this.updateArtifactDescription(x, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<input
|
||||
value={this.state.artifactInProgressName}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
this.setState({
|
||||
artifactInProgressName: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.addArtifact(this.state.artifactInProgressName);
|
||||
this.setState({artifactInProgressName: ""});
|
||||
}}
|
||||
>
|
||||
Add artifact
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {shallow} from "enzyme";
|
||||
|
||||
import {Graph} from "../../../core/graph";
|
||||
import {ArtifactGraphEditor} from "./ArtifactGraphEditor";
|
||||
import {artifactAddress} from "../artifactPlugin";
|
||||
|
||||
require("../../../app/testUtil").configureAphrodite();
|
||||
require("../../../app/testUtil").configureEnzyme();
|
||||
|
||||
describe("ArtifactGraphEditor", () => {
|
||||
function createComponent(onChange) {
|
||||
return (
|
||||
<ArtifactGraphEditor
|
||||
settings={{
|
||||
githubApiToken: "123youdontneedme",
|
||||
repoOwner: "sourcecred",
|
||||
repoName: "artifact-tests",
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
it("invokes its callback after mounting, not construction", () => {
|
||||
const onChange = jest.fn();
|
||||
const component = createComponent(onChange);
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
shallow(component);
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("adds an artifact to the list", () => {
|
||||
const onChange = jest.fn();
|
||||
const element = shallow(createComponent(onChange));
|
||||
expect(onChange).toHaveBeenLastCalledWith(new Graph());
|
||||
element
|
||||
.find("input")
|
||||
.simulate("change", {target: {value: "Root artifact!"}});
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
element.find("button").simulate("click");
|
||||
expect(
|
||||
element.find("td").filterWhere((x) => x.text() === "Root artifact!")
|
||||
).toHaveLength(1);
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
expect(onChange).toHaveBeenLastCalledWith(
|
||||
new Graph().addNode({
|
||||
address: artifactAddress(
|
||||
new Graph(),
|
||||
"sourcecred",
|
||||
"artifact-tests",
|
||||
"Root artifact!"
|
||||
),
|
||||
payload: {
|
||||
name: "Root artifact!",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("modifies an artifact's description", () => {
|
||||
const onChange = jest.fn();
|
||||
const element = shallow(createComponent(onChange));
|
||||
element
|
||||
.find("input")
|
||||
.simulate("change", {target: {value: "Root artifact!"}});
|
||||
element.find("button").simulate("click");
|
||||
element
|
||||
.find("tr textarea")
|
||||
.simulate("change", {target: {value: "for garlic, carrots, etc."}});
|
||||
expect(onChange).toHaveBeenLastCalledWith(
|
||||
new Graph().addNode({
|
||||
address: artifactAddress(
|
||||
new Graph(),
|
||||
"sourcecred",
|
||||
"artifact-tests",
|
||||
"Root artifact!"
|
||||
),
|
||||
payload: {
|
||||
name: "Root artifact!",
|
||||
description: "for garlic, carrots, etc.",
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not mutate the graph passed to its callback", () => {
|
||||
const onChange = jest.fn();
|
||||
const element = shallow(createComponent(onChange));
|
||||
const g1 = onChange.mock.calls[0][0];
|
||||
const g1Copy = g1.copy();
|
||||
element
|
||||
.find("input")
|
||||
.simulate("change", {target: {value: "Root artifact!"}});
|
||||
element.find("button").simulate("click");
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
const g2 = onChange.mock.calls[1][0];
|
||||
const g2Copy = g2.copy();
|
||||
expect(g1.equals(g1Copy)).toBe(true);
|
||||
expect(g1.equals(g2)).toBe(false);
|
||||
element
|
||||
.find("tr textarea")
|
||||
.simulate("change", {target: {value: "for garlic, carrots, etc."}});
|
||||
expect(onChange).toHaveBeenCalledTimes(3);
|
||||
const g3 = onChange.mock.calls[2][0];
|
||||
expect(g1.equals(g1Copy)).toBe(true);
|
||||
expect(g2.equals(g2Copy)).toBe(true);
|
||||
expect(g3.equals(g2)).toBe(false);
|
||||
});
|
||||
});
|
|
@ -1,141 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type {Node} from "../../../core/graph";
|
||||
import {AdapterSet} from "./adapterSet";
|
||||
import {Graph} from "../../../core/graph";
|
||||
|
||||
type Props = {
|
||||
graph: ?Graph,
|
||||
adapters: AdapterSet,
|
||||
};
|
||||
type State = {
|
||||
typeFilter: ?{|
|
||||
+pluginName: string,
|
||||
+type: string,
|
||||
|},
|
||||
};
|
||||
|
||||
export class ContributionList extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
typeFilter: null,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Contributions</h2>
|
||||
{this.renderFilterSelect()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderFilterSelect() {
|
||||
if (this.props.graph == null) {
|
||||
return null;
|
||||
}
|
||||
const graph: Graph = this.props.graph;
|
||||
const typesByPlugin: {[pluginName: string]: Set<string>} = {};
|
||||
graph.nodes().forEach((node) => {
|
||||
const adapter = this.props.adapters.getAdapter(node);
|
||||
if (adapter == null) {
|
||||
return;
|
||||
}
|
||||
if (!typesByPlugin[adapter.pluginName]) {
|
||||
typesByPlugin[adapter.pluginName] = new Set();
|
||||
}
|
||||
typesByPlugin[adapter.pluginName].add(node.address.type);
|
||||
});
|
||||
function optionGroup(pluginName: string) {
|
||||
const header = (
|
||||
<option key={pluginName} disabled style={{fontWeight: "bold"}}>
|
||||
{pluginName}
|
||||
</option>
|
||||
);
|
||||
const entries = Array.from(typesByPlugin[pluginName])
|
||||
.sort()
|
||||
.map((type) => (
|
||||
<option key={type} value={JSON.stringify({pluginName, type})}>
|
||||
{"\u2003" + type}
|
||||
</option>
|
||||
));
|
||||
return [header, ...entries];
|
||||
}
|
||||
return (
|
||||
<label>
|
||||
Filter by contribution type:{" "}
|
||||
<select
|
||||
value={JSON.stringify(this.state.typeFilter)}
|
||||
onChange={(e) => {
|
||||
this.setState({typeFilter: JSON.parse(e.target.value)});
|
||||
}}
|
||||
>
|
||||
<option value={JSON.stringify(null)}>Show all</option>
|
||||
{Object.keys(typesByPlugin)
|
||||
.sort()
|
||||
.map(optionGroup)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
if (this.props.graph == null) {
|
||||
return <div>(no graph)</div>;
|
||||
} else {
|
||||
const graph: Graph = this.props.graph;
|
||||
const {typeFilter} = this.state;
|
||||
const shouldDisplay: (node: Node<any>) => boolean = typeFilter
|
||||
? (node) => {
|
||||
const adapter = this.props.adapters.getAdapter(node);
|
||||
return (
|
||||
!!adapter &&
|
||||
adapter.pluginName === typeFilter.pluginName &&
|
||||
node.address.type === typeFilter.type
|
||||
);
|
||||
}
|
||||
: (_) => true;
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Artifact</th>
|
||||
<th>Weight</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.graph.nodes().map((node) => {
|
||||
if (!shouldDisplay(node)) {
|
||||
return null;
|
||||
}
|
||||
const adapter = this.props.adapters.getAdapter(node);
|
||||
if (adapter == null) {
|
||||
return (
|
||||
<tr key={JSON.stringify(node.address)}>
|
||||
<td colSpan={3}>
|
||||
<i>unknown</i> (plugin: {node.address.pluginName})
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tr key={JSON.stringify(node.address)}>
|
||||
<td>{adapter.extractTitle(graph, node)}</td>
|
||||
<td>[TODO]</td>
|
||||
<td>[TODO]</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import type {ReactWrapper} from "enzyme";
|
||||
import React from "react";
|
||||
import {shallow} from "enzyme";
|
||||
import enzymeToJSON from "enzyme-to-json";
|
||||
|
||||
import type {Address} from "../../../core/address";
|
||||
import type {Node} from "../../../core/graph";
|
||||
import type {PluginAdapter} from "./pluginAdapter";
|
||||
import {AdapterSet} from "./adapterSet";
|
||||
import {ContributionList} from "./ContributionList";
|
||||
import {Graph} from "../../../core/graph";
|
||||
|
||||
require("../../../app/testUtil").configureAphrodite();
|
||||
require("../../../app/testUtil").configureEnzyme();
|
||||
|
||||
function createTestData(): * {
|
||||
type PayloadA = number;
|
||||
type PayloadB = boolean;
|
||||
type PayloadC = string;
|
||||
|
||||
const PLUGIN_A = "sourcecred/example-plugin-a";
|
||||
const PLUGIN_B = "sourcecred/example-plugin-b";
|
||||
const PLUGIN_C = "sourcecred/example-plugin-c";
|
||||
|
||||
function makeAddress(
|
||||
pluginName: typeof PLUGIN_A | typeof PLUGIN_B | typeof PLUGIN_C,
|
||||
type: string,
|
||||
id: string
|
||||
): Address {
|
||||
return {
|
||||
pluginName,
|
||||
id,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
const nodeA1 = () => ({
|
||||
address: makeAddress(PLUGIN_A, "small", "one"),
|
||||
payload: (111: PayloadA),
|
||||
});
|
||||
const nodeA2 = () => ({
|
||||
address: makeAddress(PLUGIN_A, "small", "two"),
|
||||
payload: (234: PayloadA),
|
||||
});
|
||||
const nodeA3 = () => ({
|
||||
address: makeAddress(PLUGIN_A, "big", "three"),
|
||||
payload: (616: PayloadA),
|
||||
});
|
||||
const nodeB4 = () => ({
|
||||
address: makeAddress(PLUGIN_B, "very true", "four"),
|
||||
payload: (true: PayloadB),
|
||||
});
|
||||
const nodeC5 = () => ({
|
||||
address: makeAddress(PLUGIN_C, "ctype", "five"),
|
||||
payload: ("I have no adapter :-(": PayloadC),
|
||||
});
|
||||
const edgeA1A2 = () => ({
|
||||
address: makeAddress(PLUGIN_A, "atype", "one-to-two"),
|
||||
payload: null,
|
||||
src: nodeA1().address,
|
||||
dst: nodeA2().address,
|
||||
});
|
||||
const edgeB4A3 = () => ({
|
||||
address: makeAddress(PLUGIN_C, "ctype", "four-to-three"),
|
||||
payload: null,
|
||||
src: nodeB4().address,
|
||||
dst: nodeA3().address,
|
||||
});
|
||||
|
||||
const graph: () => Graph = () =>
|
||||
new Graph()
|
||||
.addNode(nodeA1())
|
||||
.addNode(nodeA2())
|
||||
.addNode(nodeA3())
|
||||
.addNode(nodeB4())
|
||||
.addNode(nodeC5())
|
||||
.addEdge(edgeA1A2())
|
||||
.addEdge(edgeB4A3());
|
||||
|
||||
const adapterA: () => PluginAdapter<PayloadA> = () => ({
|
||||
pluginName: PLUGIN_A,
|
||||
renderer: class RendererA extends React.Component<{
|
||||
graph: Graph,
|
||||
node: Node<PayloadA>,
|
||||
}> {
|
||||
render() {
|
||||
const {graph, node} = this.props;
|
||||
const neighborCount = graph.neighborhood(node.address, {
|
||||
direction: "OUT",
|
||||
}).length;
|
||||
return (
|
||||
<span>
|
||||
<tt>{node.address.id}</tt> has neighbor count{" "}
|
||||
<strong>{neighborCount}</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
extractTitle(graph: Graph, node: Node<PayloadA>) {
|
||||
return `the number ${String(node.payload)}`;
|
||||
},
|
||||
});
|
||||
|
||||
const adapterB: () => PluginAdapter<PayloadB> = () => ({
|
||||
pluginName: PLUGIN_B,
|
||||
renderer: class RendererB extends React.Component<{
|
||||
graph: Graph,
|
||||
node: Node<PayloadB>,
|
||||
}> {
|
||||
render() {
|
||||
const {node} = this.props;
|
||||
return (
|
||||
<span>
|
||||
Node <em>{node.address.id}</em>: <strong>{node.payload}</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
extractTitle(graph: Graph, node: Node<PayloadB>) {
|
||||
return String(node.payload).toUpperCase() + "!";
|
||||
},
|
||||
});
|
||||
|
||||
const adapters: () => AdapterSet = () => {
|
||||
const result = new AdapterSet();
|
||||
result.addAdapter(adapterA());
|
||||
result.addAdapter(adapterB());
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
PLUGIN_A,
|
||||
PLUGIN_B,
|
||||
PLUGIN_C,
|
||||
nodeA1,
|
||||
nodeA2,
|
||||
nodeA3,
|
||||
nodeB4,
|
||||
nodeC5,
|
||||
edgeA1A2,
|
||||
edgeB4A3,
|
||||
graph,
|
||||
adapterA,
|
||||
adapterB,
|
||||
adapters,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ContributionList", () => {
|
||||
// Render a contribution list with the above test data.
|
||||
function render() {
|
||||
const data = createTestData();
|
||||
const result = shallow(
|
||||
<ContributionList graph={data.graph()} adapters={data.adapters()} />
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Select the unique <option> whose text matches the given patern.
|
||||
function simulateSelect(container: ReactWrapper, pattern: RegExp): void {
|
||||
const targetOption = container
|
||||
.find("option")
|
||||
.filterWhere((x) => pattern.test(x.text()));
|
||||
expect(targetOption).toHaveLength(1);
|
||||
container
|
||||
.find("select")
|
||||
.simulate("change", {target: {value: targetOption.prop("value")}});
|
||||
}
|
||||
|
||||
it("renders some test data in the default state", () => {
|
||||
const result = render();
|
||||
expect(enzymeToJSON(result)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("updates the node table when a filter is selected", () => {
|
||||
const result = render();
|
||||
simulateSelect(result, /small/);
|
||||
expect(enzymeToJSON(result)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("resets the node table when a filter is deselected", () => {
|
||||
const result = render();
|
||||
const originalHtml = result.html();
|
||||
simulateSelect(result, /big/);
|
||||
const intermediateHtml = result.html();
|
||||
simulateSelect(result, /Show all/);
|
||||
const finalHtml = result.html();
|
||||
expect(finalHtml).toEqual(originalHtml);
|
||||
expect(finalHtml).not.toEqual(intermediateHtml);
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type {Graph} from "../../../core/graph";
|
||||
import type {Settings} from "./SettingsConfig";
|
||||
import fetchGithubRepo from "../../github/fetchGithubRepo";
|
||||
import {parse} from "../../github/parser";
|
||||
|
||||
type Props = {
|
||||
settings: Settings,
|
||||
onCreateGraph: (graph: Graph) => void,
|
||||
};
|
||||
|
||||
export class GithubGraphFetcher extends React.Component<Props> {
|
||||
render() {
|
||||
const {settings} = this.props;
|
||||
const haveSettings =
|
||||
!!settings.githubApiToken && !!settings.repoOwner && !!settings.repoName;
|
||||
return (
|
||||
<button onClick={() => this.fetchGraph()} disabled={!haveSettings}>
|
||||
Fetch GitHub graph
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
fetchGraph() {
|
||||
const {repoOwner, repoName, githubApiToken} = this.props.settings;
|
||||
fetchGithubRepo(repoOwner, repoName, githubApiToken)
|
||||
.then((json) => {
|
||||
return Promise.resolve(parse(json));
|
||||
})
|
||||
.then((graph) => {
|
||||
this.props.onCreateGraph(graph);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import LocalStore from "../../../app/LocalStore";
|
||||
|
||||
export default new LocalStore({version: "1", keyPrefix: "artifact-editor"});
|
|
@ -1,89 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import LocalStore from "./LocalStore";
|
||||
|
||||
export type Settings = {
|
||||
githubApiToken: string,
|
||||
repoOwner: string,
|
||||
repoName: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onChange: (Settings) => void,
|
||||
};
|
||||
type State = Settings;
|
||||
|
||||
const LOCAL_STORE_SETTINGS_KEY = "SettingsConfig.settings";
|
||||
|
||||
export function defaultSettings() {
|
||||
return {
|
||||
githubApiToken: "",
|
||||
repoOwner: "",
|
||||
repoName: "",
|
||||
};
|
||||
}
|
||||
|
||||
export class SettingsConfig extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = defaultSettings();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState(LocalStore.get(LOCAL_STORE_SETTINGS_KEY, this.state), () => {
|
||||
this.props.onChange(this.state);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
API token{" "}
|
||||
<input
|
||||
value={this.state.githubApiToken}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
this.setState(
|
||||
{githubApiToken: value},
|
||||
this._updateSettings.bind(this)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Repository owner{" "}
|
||||
<input
|
||||
value={this.state.repoOwner}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
this.setState(
|
||||
{repoOwner: value},
|
||||
this._updateSettings.bind(this)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
Repository name{" "}
|
||||
<input
|
||||
value={this.state.repoName}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
this.setState({repoName: value}, this._updateSettings.bind(this));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_updateSettings() {
|
||||
LocalStore.set(LOCAL_STORE_SETTINGS_KEY, this.state);
|
||||
this.props.onChange(this.state);
|
||||
}
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ContributionList renders some test data in the default state 1`] = `
|
||||
<div>
|
||||
<h2>
|
||||
Contributions
|
||||
</h2>
|
||||
<label>
|
||||
Filter by contribution type:
|
||||
|
||||
<select
|
||||
onChange={[Function]}
|
||||
value="null"
|
||||
>
|
||||
<option
|
||||
value="null"
|
||||
>
|
||||
Show all
|
||||
</option>
|
||||
<option
|
||||
disabled={true}
|
||||
key="sourcecred/example-plugin-a"
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
>
|
||||
sourcecred/example-plugin-a
|
||||
</option>
|
||||
<option
|
||||
key="big"
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"big\\"}"
|
||||
>
|
||||
big
|
||||
</option>
|
||||
<option
|
||||
key="small"
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
small
|
||||
</option>
|
||||
<option
|
||||
disabled={true}
|
||||
key="sourcecred/example-plugin-b"
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
>
|
||||
sourcecred/example-plugin-b
|
||||
</option>
|
||||
<option
|
||||
key="very true"
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-b\\",\\"type\\":\\"very true\\"}"
|
||||
>
|
||||
very true
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Title
|
||||
</th>
|
||||
<th>
|
||||
Artifact
|
||||
</th>
|
||||
<th>
|
||||
Weight
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"one\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
<td>
|
||||
the number 111
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"two\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
<td>
|
||||
the number 234
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"three\\",\\"type\\":\\"big\\"}"
|
||||
>
|
||||
<td>
|
||||
the number 616
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-b\\",\\"id\\":\\"four\\",\\"type\\":\\"very true\\"}"
|
||||
>
|
||||
<td>
|
||||
TRUE!
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-c\\",\\"id\\":\\"five\\",\\"type\\":\\"ctype\\"}"
|
||||
>
|
||||
<td
|
||||
colSpan={3}
|
||||
>
|
||||
<i>
|
||||
unknown
|
||||
</i>
|
||||
(plugin:
|
||||
sourcecred/example-plugin-c
|
||||
)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ContributionList updates the node table when a filter is selected 1`] = `
|
||||
<div>
|
||||
<h2>
|
||||
Contributions
|
||||
</h2>
|
||||
<label>
|
||||
Filter by contribution type:
|
||||
|
||||
<select
|
||||
onChange={[Function]}
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
<option
|
||||
value="null"
|
||||
>
|
||||
Show all
|
||||
</option>
|
||||
<option
|
||||
disabled={true}
|
||||
key="sourcecred/example-plugin-a"
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
>
|
||||
sourcecred/example-plugin-a
|
||||
</option>
|
||||
<option
|
||||
key="big"
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"big\\"}"
|
||||
>
|
||||
big
|
||||
</option>
|
||||
<option
|
||||
key="small"
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
small
|
||||
</option>
|
||||
<option
|
||||
disabled={true}
|
||||
key="sourcecred/example-plugin-b"
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
>
|
||||
sourcecred/example-plugin-b
|
||||
</option>
|
||||
<option
|
||||
key="very true"
|
||||
value="{\\"pluginName\\":\\"sourcecred/example-plugin-b\\",\\"type\\":\\"very true\\"}"
|
||||
>
|
||||
very true
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Title
|
||||
</th>
|
||||
<th>
|
||||
Artifact
|
||||
</th>
|
||||
<th>
|
||||
Weight
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"one\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
<td>
|
||||
the number 111
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"two\\",\\"type\\":\\"small\\"}"
|
||||
>
|
||||
<td>
|
||||
the number 234
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
<td>
|
||||
[TODO]
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
|
@ -1,20 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import type {Node} from "../../../core/graph";
|
||||
import type {PluginAdapter} from "./pluginAdapter";
|
||||
|
||||
export class AdapterSet {
|
||||
adapters: {[pluginName: string]: PluginAdapter<any>};
|
||||
|
||||
constructor() {
|
||||
this.adapters = {};
|
||||
}
|
||||
|
||||
addAdapter(adapter: PluginAdapter<any>): void {
|
||||
this.adapters[adapter.pluginName] = adapter;
|
||||
}
|
||||
|
||||
getAdapter<NP>(node: Node<NP>): ?PluginAdapter<NP> {
|
||||
return this.adapters[node.address.pluginName];
|
||||
}
|
||||
}
|
|
@ -1,420 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`githubPluginAdapter operates on the example repo 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "https://github.com/decentralion",
|
||||
"payload": Object {
|
||||
"login": "decentralion",
|
||||
"subtype": "USER",
|
||||
"url": "https://github.com/decentralion",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
AUTHOR
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "decentralion",
|
||||
"type": "AUTHOR",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github",
|
||||
"payload": Object {
|
||||
"name": "example-github",
|
||||
"owner": "sourcecred",
|
||||
"url": "https://github.com/sourcecred/example-github",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
REPOSITORY
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "sourcecred/example-github",
|
||||
"type": "REPOSITORY",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/1",
|
||||
"payload": Object {
|
||||
"body": "This is just an example issue.",
|
||||
"number": 1,
|
||||
"title": "An example issue.",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/1",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
ISSUE
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#1: An example issue.",
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2",
|
||||
"payload": Object {
|
||||
"body": "This issue references another issue, namely #1",
|
||||
"number": 2,
|
||||
"title": "A referencing issue.",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
ISSUE
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#2: A referencing issue.",
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768703",
|
||||
"payload": Object {
|
||||
"body": "It should also be possible to reference by exact url: https://github.com/sourcecred/example-github/issues/6",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768703",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768850",
|
||||
"payload": Object {
|
||||
"body": "We might also reference individual comments directly.
|
||||
https://github.com/sourcecred/example-github/issues/6#issuecomment-373768538",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768850",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576185",
|
||||
"payload": Object {
|
||||
"body": "Here's a PR by direct url: https://github.com/sourcecred/example-github/pull/5",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576185",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576220",
|
||||
"payload": Object {
|
||||
"body": "a PR review by url: https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576220",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576248",
|
||||
"payload": Object {
|
||||
"body": "a PR Review Comment by url: https://github.com/sourcecred/example-github/pull/5#discussion_r171460198",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576248",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576273",
|
||||
"payload": Object {
|
||||
"body": "a user by url: https://github.com/wchargin",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576273",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576920",
|
||||
"payload": Object {
|
||||
"body": "Here are several references:
|
||||
#1
|
||||
#2
|
||||
#3
|
||||
|
||||
https://github.com/sourcecred/example-github/pull/5#discussion_r171460198
|
||||
https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899
|
||||
",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576920",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576936",
|
||||
"payload": Object {
|
||||
"body": "This comment has no references.",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576936",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #2: A referencing issue.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/4",
|
||||
"payload": Object {
|
||||
"body": "Alas, its life as an open issue had only just begun.",
|
||||
"number": 4,
|
||||
"title": "A closed pull request",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/4",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
ISSUE
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#4: A closed pull request",
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/6",
|
||||
"payload": Object {
|
||||
"body": "This issue shall shortly have a few comments.",
|
||||
"number": 6,
|
||||
"title": "An issue with comments",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/6",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
ISSUE
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#6: An issue with comments",
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768442",
|
||||
"payload": Object {
|
||||
"body": "A wild COMMENT appeared!",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768442",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #6: An issue with comments",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768538",
|
||||
"payload": Object {
|
||||
"body": "And the maintainer said, \\"Let there be comments!\\"",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768538",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #6: An issue with comments",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316",
|
||||
"payload": Object {
|
||||
"body": "This comment references an #2, which itself references an issue. This comment is thus allows us to test that in-references are not included when requesting a Post's references.",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #6: An issue with comments",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/7",
|
||||
"payload": Object {
|
||||
"body": "Deal with this, naive string display algorithms!!!!!",
|
||||
"number": 7,
|
||||
"title": "An issue with an extremely long title, which even has a VerySuperFragicalisticialiManyCharacterUberLongTriplePlusGood word in it, and should really be truncated intelligently or something",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/7",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
ISSUE
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#7: An issue with an extremely long title, which even has a VerySuperFragicalisticialiManyCharacterUberLongTriplePlusGood word in it, and should really be truncated intelligently or something",
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/issues/8",
|
||||
"payload": Object {
|
||||
"body": "Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️
|
||||
Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️",
|
||||
"number": 8,
|
||||
"title": "Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️",
|
||||
"url": "https://github.com/sourcecred/example-github/issues/8",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
ISSUE
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#8: Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️",
|
||||
"type": "ISSUE",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/3",
|
||||
"payload": Object {
|
||||
"body": "Oh look, it's a pull request.",
|
||||
"number": 3,
|
||||
"title": "Add README, merge via PR.",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/3",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
PULL_REQUEST
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#3: Add README, merge via PR.",
|
||||
"type": "PULL_REQUEST",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222",
|
||||
"payload": Object {
|
||||
"body": "It seems apropos to reference something from a pull request comment... eg: #2 ",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on #3: Add README, merge via PR.",
|
||||
"type": "COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/5",
|
||||
"payload": Object {
|
||||
"body": "@wchargin could you please do the following:
|
||||
- add a commit comment
|
||||
- add a review comment requesting some trivial change
|
||||
- i'll change it
|
||||
- then approve the pr",
|
||||
"number": 5,
|
||||
"title": "This pull request will be more contentious. I can feel it...",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/5",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
PULL_REQUEST
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#5: This pull request will be more contentious. I can feel it...",
|
||||
"type": "PULL_REQUEST",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/5#discussion_r171460198",
|
||||
"payload": Object {
|
||||
"body": "seems a bit capricious",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/5#discussion_r171460198",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
PULL_REQUEST_REVIEW_COMMENT
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "comment on review of #5: This pull request will be more contentious. I can feel it...",
|
||||
"type": "PULL_REQUEST_REVIEW_COMMENT",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
|
||||
"payload": Object {
|
||||
"body": "hmmm.jpg",
|
||||
"state": "CHANGES_REQUESTED",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
PULL_REQUEST_REVIEW
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "review of #5: This pull request will be more contentious. I can feel it...",
|
||||
"type": "PULL_REQUEST_REVIEW",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100314038",
|
||||
"payload": Object {
|
||||
"body": "I'm sold",
|
||||
"state": "APPROVED",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100314038",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
PULL_REQUEST_REVIEW
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "review of #5: This pull request will be more contentious. I can feel it...",
|
||||
"type": "PULL_REQUEST_REVIEW",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/sourcecred/example-github/pull/9",
|
||||
"payload": Object {
|
||||
"body": "Nominally paired with @wchargin",
|
||||
"number": 9,
|
||||
"title": "An unmerged pull request",
|
||||
"url": "https://github.com/sourcecred/example-github/pull/9",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
PULL_REQUEST
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "#9: An unmerged pull request",
|
||||
"type": "PULL_REQUEST",
|
||||
},
|
||||
Object {
|
||||
"id": "https://github.com/wchargin",
|
||||
"payload": Object {
|
||||
"login": "wchargin",
|
||||
"subtype": "USER",
|
||||
"url": "https://github.com/wchargin",
|
||||
},
|
||||
"rendered": <div>
|
||||
type:
|
||||
AUTHOR
|
||||
(details to be implemented)
|
||||
</div>,
|
||||
"title": "wchargin",
|
||||
"type": "AUTHOR",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -1,108 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {Graph} from "../../../../core/graph";
|
||||
import type {Node} from "../../../../core/graph";
|
||||
import type {
|
||||
NodePayload,
|
||||
NodeType,
|
||||
RepositoryNodePayload,
|
||||
IssueNodePayload,
|
||||
PullRequestNodePayload,
|
||||
CommentNodePayload,
|
||||
PullRequestReviewCommentNodePayload,
|
||||
PullRequestReviewNodePayload,
|
||||
AuthorNodePayload,
|
||||
} from "../../../github/types";
|
||||
import type {PluginAdapter} from "../pluginAdapter";
|
||||
import {PLUGIN_NAME} from "../../../github/pluginName";
|
||||
import {CONTAINS_EDGE_TYPE} from "../../../github/types";
|
||||
|
||||
const adapter: PluginAdapter<NodePayload> = {
|
||||
pluginName: PLUGIN_NAME,
|
||||
|
||||
renderer: class GithubNodeRenderer extends React.Component<{
|
||||
graph: Graph,
|
||||
node: Node<NodePayload>,
|
||||
}> {
|
||||
render() {
|
||||
const type = this.props.node.address.type;
|
||||
return <div>type: {type} (details to be implemented)</div>;
|
||||
}
|
||||
},
|
||||
|
||||
extractTitle(graph: *, node: Node<NodePayload>): string {
|
||||
// NOTE: If the graph is malformed such that there are containment
|
||||
// cycles, then this function may blow the stack or fail to
|
||||
// terminate. (If necessary, we can fix this by tracking all
|
||||
// previously queried IDs.)
|
||||
function extractParentTitles(node: Node<NodePayload>): string[] {
|
||||
return graph
|
||||
.neighborhood(node.address, {
|
||||
direction: "IN",
|
||||
edgeType: CONTAINS_EDGE_TYPE,
|
||||
})
|
||||
.map(({neighbor}) => {
|
||||
return adapter.extractTitle(graph, graph.node(neighbor));
|
||||
});
|
||||
}
|
||||
function extractRepositoryTitle(node: Node<RepositoryNodePayload>) {
|
||||
return `${node.payload.owner}/${node.payload.name}`;
|
||||
}
|
||||
function extractIssueOrPrTitle(
|
||||
node: Node<IssueNodePayload | PullRequestNodePayload>
|
||||
) {
|
||||
return `#${node.payload.number}: ${node.payload.title}`;
|
||||
}
|
||||
function extractCommentTitle(
|
||||
kind: string,
|
||||
node: Node<CommentNodePayload | PullRequestReviewCommentNodePayload>
|
||||
) {
|
||||
const parentTitles = extractParentTitles(node);
|
||||
if (parentTitles.length === 0) {
|
||||
// Should never happen.
|
||||
return "comment (orphaned)";
|
||||
} else {
|
||||
// Should just be one parent.
|
||||
return `comment on ${parentTitles.join(" and ")}`;
|
||||
}
|
||||
}
|
||||
function extractPRReviewTitle(node: Node<PullRequestReviewNodePayload>) {
|
||||
const parentTitles = extractParentTitles(node);
|
||||
if (parentTitles.length === 0) {
|
||||
// Should never happen.
|
||||
return "pull request review (orphaned)";
|
||||
} else {
|
||||
// Should just be one parent.
|
||||
return `review of ${parentTitles.join(" and ")}`;
|
||||
}
|
||||
}
|
||||
function extractAuthorTitle(node: Node<AuthorNodePayload>) {
|
||||
return node.payload.login;
|
||||
}
|
||||
const anyNode: Node<any> = node;
|
||||
const type: NodeType = (node.address.type: any);
|
||||
switch (type) {
|
||||
case "REPOSITORY":
|
||||
return extractRepositoryTitle(anyNode);
|
||||
case "ISSUE":
|
||||
case "PULL_REQUEST":
|
||||
return extractIssueOrPrTitle(anyNode);
|
||||
case "COMMENT":
|
||||
return extractCommentTitle("comment", anyNode);
|
||||
case "PULL_REQUEST_REVIEW_COMMENT":
|
||||
return extractCommentTitle("review comment", anyNode);
|
||||
case "PULL_REQUEST_REVIEW":
|
||||
return extractPRReviewTitle(anyNode);
|
||||
case "AUTHOR":
|
||||
return extractAuthorTitle(anyNode);
|
||||
default:
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
(type: empty);
|
||||
throw new Error(`unknown node type: ${node.address.type}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default adapter;
|
|
@ -1,36 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {shallow} from "enzyme";
|
||||
import enzymeToJSON from "enzyme-to-json";
|
||||
import stringify from "json-stable-stringify";
|
||||
|
||||
import {parse} from "../../../github/parser";
|
||||
import exampleRepoData from "../../../github/demoData/example-github.json";
|
||||
import adapter from "./githubPluginAdapter";
|
||||
|
||||
require("../../../../app/testUtil").configureEnzyme();
|
||||
|
||||
describe("githubPluginAdapter", () => {
|
||||
it("operates on the example repo", () => {
|
||||
const graph = parse(exampleRepoData);
|
||||
|
||||
const result = graph
|
||||
.nodes()
|
||||
.map((node) => ({
|
||||
id: node.address.id,
|
||||
payload: node.payload,
|
||||
type: node.address.type,
|
||||
title: adapter.extractTitle(graph, node),
|
||||
rendered: enzymeToJSON(
|
||||
shallow(<adapter.renderer graph={graph} node={node} />)
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const ka = stringify(a.id);
|
||||
const kb = stringify(b.id);
|
||||
return ka > kb ? 1 : ka < kb ? -1 : 0;
|
||||
});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import type {Graph, Node} from "../../../core/graph";
|
||||
import type {ComponentType} from "react";
|
||||
|
||||
export interface PluginAdapter<-NodePayload> {
|
||||
pluginName: string;
|
||||
renderer: $Subtype<ComponentType<{graph: Graph, node: Node<NodePayload>}>>;
|
||||
extractTitle(graph: Graph, node: Node<NodePayload>): string;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import {AdapterSet} from "./adapterSet";
|
||||
import githubPluginAdapter from "./adapters/githubPluginAdapter";
|
||||
|
||||
const adapterSet = new AdapterSet();
|
||||
adapterSet.addAdapter(githubPluginAdapter);
|
||||
|
||||
export default adapterSet;
|
Loading…
Reference in New Issue