├── .babelrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TAGS ├── app ├── CaptionDisplaySettings.js ├── EdgeDisplaySettings.js ├── NodeDisplaySettings.js ├── actions.js ├── components │ ├── AddCaptionForm.jsx │ ├── AddConnectedNodesForm.jsx │ ├── AddEdgeForm.jsx │ ├── AddNodeForm.jsx │ ├── AddNodeInput.jsx │ ├── AddNodeResult.jsx │ ├── AnnotationsTracker.jsx │ ├── BaseComponent.jsx │ ├── Caption.jsx │ ├── ChangeColorInput.jsx │ ├── DeleteSelectedButton.jsx │ ├── Edge.jsx │ ├── EdgeArrowSelector.jsx │ ├── EdgeDashSelector.jsx │ ├── EdgeDropdown.jsx │ ├── EditButton.jsx │ ├── EditButtons.jsx │ ├── EditTools.jsx │ ├── Editor.jsx │ ├── EmbeddedGraphAnnotation.jsx │ ├── EmbeddedGraphAnnotations.jsx │ ├── EmbeddedNavBar.jsx │ ├── EmbeddedNavButtons.jsx │ ├── Graph.jsx │ ├── GraphAnnotation.jsx │ ├── GraphAnnotationForm.jsx │ ├── GraphAnnotationList.jsx │ ├── GraphAnnotationListItem.jsx │ ├── GraphAnnotations.jsx │ ├── GraphByLine.jsx │ ├── GraphHeader.jsx │ ├── GraphLinks.jsx │ ├── GraphNavButtons.jsx │ ├── GraphSettingsForm.jsx │ ├── GraphTitle.jsx │ ├── GraphTitleForm.jsx │ ├── HelpButton.jsx │ ├── HelpScreen.jsx │ ├── LayoutButtons.jsx │ ├── Node.jsx │ ├── NodeCircle.jsx │ ├── NodeLabel.jsx │ ├── Root.jsx │ ├── SaveButton.jsx │ ├── SettingsButton.jsx │ ├── UndoButtons.jsx │ ├── UpdateCaptionForm.jsx │ ├── UpdateEdgeForm.jsx │ ├── UpdateNodeForm.jsx │ ├── ZoomButtons.jsx │ └── __tests__ │ │ ├── AnnotationsTracker-test.jsx │ │ ├── Caption-test.jsx │ │ ├── ChangeColorInput-test.jsx │ │ ├── DeleteSelectedButton-test.js │ │ ├── Edge-test.jsx │ │ ├── EdgeArrowSelector-test.jsx │ │ ├── EdgeDashSelector-test.jsx │ │ ├── EdgeDropdown-test.jsx │ │ ├── EditTools-test.jsx │ │ ├── EmbeddedGraphAnnotation-test.jsx │ │ ├── EmbeddedGraphAnnotations-test.jsx │ │ ├── EmbeddedNavBar-test.jsx │ │ ├── EmbeddedNavButtons-test.jsx │ │ ├── Graph-test.jsx │ │ ├── GraphAnnotation-test.jsx │ │ ├── GraphAnnotationForm-test.jsx │ │ ├── GraphAnnotationList-test.jsx │ │ ├── GraphHeader-test.jsx │ │ ├── GraphNavButtons-test.jsx │ │ ├── GraphTitle-test.jsx │ │ ├── Node-test.jsx │ │ ├── NodeLabel-test.jsx │ │ ├── Root-test.jsx │ │ ├── UpdateCaptionForm-test.jsx │ │ ├── UpdateEdgeForm-test.jsx │ │ ├── UpdateNodeForm-test.jsx │ │ └── support.js ├── fonts │ └── glyphicons-halflings-regular.woff ├── helpers.js ├── main.jsx ├── models │ ├── Annotation.js │ ├── Caption.js │ ├── Edge.js │ ├── Graph.js │ ├── Helpers.js │ ├── Node.js │ └── __tests__ │ │ ├── Annotation-test.js │ │ ├── Caption-test.js │ │ ├── Edge-test.js │ │ ├── Graph-test.js │ │ ├── Helpers-test.js │ │ └── Node-test.js ├── reducers.js ├── reducers │ ├── __tests__ │ │ ├── annotations-test.js │ │ └── graph-test.js │ ├── annotations.js │ ├── editTools.js │ ├── graph.js │ ├── position.js │ ├── selection.js │ ├── settings.js │ ├── showHelpScreen.js │ ├── showSettings.js │ ├── title.js │ ├── undoable-graph.js │ └── zoom.js └── styles │ ├── bootstrap-3.3.6.css │ ├── oligrapher.annotations.css │ ├── oligrapher.css │ ├── oligrapher.editor.css │ ├── oligrapher.embedded.css │ └── test │ └── styleMock.js ├── build ├── GoogleSheetDataSource.js ├── LsDataConverter.js ├── LsDataSource.js ├── PopoloDataConverter.js ├── dev.html ├── embedded.html ├── fake_news.html ├── index.html ├── oligrapher-demo-data.js ├── oligrapher.js ├── oligrapher.min.js ├── puerto-rico-sample-data.js └── puerto_rico.html ├── package.json ├── webpack.dev.config.js └── webpack.prod.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Emacs-specific stuff 11 | .tern-port 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | 32 | build/oligrapher-dev.js 33 | 34 | # mac stuff 35 | .DS_Store 36 | 37 | flycheck 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6.10.3" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Oligrapher CHANGELOG 2 | 3 | ## [0.2.1] - 2017-05-25 4 | ### added 5 | - url option to configuration to enable linkable title 6 | 7 | ### removed 8 | - 'Click to view this...' text in embedded mode 9 | 10 | 11 | ## [0.2.0] - 2017-05-18 12 | ### added 13 | - this changelog :) 14 | 15 | ### changed 16 | - Updated React to 15.5 17 | - Updated other libraries 18 | - Use prop-types library as it is now depreciated from react's core 19 | -------------------------------------------------------------------------------- /app/CaptionDisplaySettings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lineHeight: 20, 3 | cornerRadius: 5, 4 | highlightFillColor: "#ff0", 5 | selectFillColor: "#0f0", 6 | textOpacity: { 7 | "normal": 1, 8 | "highlighted": 1, 9 | "faded": 0.2 10 | }, 11 | highlightOpacity: 0.5, 12 | selectOpacity: 0.5 13 | }; -------------------------------------------------------------------------------- /app/EdgeDisplaySettings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | curveStrength: 0.5, 3 | lineColor: { 4 | normal: "#999", 5 | highlighted: "#999", 6 | faded: "#ddd" 7 | }, 8 | textColor: { 9 | normal: "#999", 10 | highlighted: "#444", 11 | faded: "#ddd" 12 | }, 13 | bgColor: { 14 | normal: "#fff", 15 | highlighted: "#ff0", 16 | faded: "#fff" 17 | }, 18 | selectColor: "#0f0", 19 | bgOpacity: { 20 | normal: 0, 21 | highlighted: 0.5, 22 | faded: 0 23 | }, 24 | selectOpacity: 0.5, 25 | bgWidthDiff: 5, 26 | selectWidthDiff: 12 27 | }; -------------------------------------------------------------------------------- /app/NodeDisplaySettings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | circleRadius: 25, 3 | circleSpacing: 4, 4 | textMarginTop: 20, 5 | lineHeight: 20, 6 | imageScale: 3, 7 | cornerRadius: 5, 8 | circleColor: { 9 | normal: "#ccc", 10 | highlighted: "#ccc", 11 | faded: "#ccc" 12 | }, 13 | textColor: { 14 | normal: "#000", 15 | highlighted: "#000", 16 | faded: "#000" 17 | }, 18 | textOpacity: { 19 | normal: 1, 20 | highlighted: 1, 21 | faded: 0.2 22 | }, 23 | bgColor: { 24 | normal: "#fff", 25 | highlighted: "#ff0", 26 | faded: "#fff" 27 | }, 28 | selectColor: "#0f0", 29 | bgOpacity: { 30 | normal: 0, 31 | highlighted: 0.5, 32 | faded: 0 33 | }, 34 | imageOpacity: { 35 | normal: 1, 36 | highlighted: 1, 37 | faded: 0.2 38 | }, 39 | circleOpacity: { 40 | normal: 1, 41 | highlighted: 1, 42 | faded: 0.2 43 | }, 44 | bgRadiusDiff: 4, 45 | selectionRadiusDiff: 10 46 | }; -------------------------------------------------------------------------------- /app/components/AddCaptionForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import { HotKeys } from 'react-hotkeys'; 5 | 6 | export default class AddCaptionForm extends BaseComponent { 7 | constructor(props) { 8 | super(props); 9 | this.bindAll('_handleSubmit', '_handleScaleChange'); 10 | 11 | this.state = { 12 | scaleValue: 1 13 | }; 14 | } 15 | 16 | render() { 17 | 18 | const scales = [ 19 | [null, "Scale"], 20 | [1, "1x"], 21 | [1.25, "1.25x"], 22 | [1.5, "1.5x"], 23 | [2, "2x"], 24 | [2.5, "2.5x"], 25 | [3, "3x"], 26 | [4, "4x"], 27 | [5, "5x"] 28 | ]; 29 | 30 | return ( 31 |
32 |
33 | 34 |   44 |
45 |
46 | ); 47 | } 48 | 49 | _handleScaleChange() { 50 | this.setState({scaleValue: this.refs.scale.value}) 51 | } 52 | 53 | _handleSubmit(e) { 54 | let text = this.refs.text.value.trim(); 55 | let scale = this.state.scaleValue; 56 | 57 | this.props.addCaption({ display: { text, scale } }); 58 | this._clear(); 59 | e.preventDefault(); 60 | } 61 | 62 | _clear() { 63 | this.refs.text.value = ''; 64 | this.refs.scale.value = 1; 65 | } 66 | } -------------------------------------------------------------------------------- /app/components/AddConnectedNodesForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import { HotKeys } from 'react-hotkeys'; 5 | import mapKeys from 'lodash/mapKeys'; 6 | 7 | export default class AddConnectedNodesForm extends BaseComponent { 8 | constructor(props) { 9 | super(props); 10 | this.bindAll('_handleSubmit'); 11 | } 12 | 13 | render() { 14 | const keyMap = { 15 | 'esc': 'esc' 16 | }; 17 | 18 | const keyHandlers = { 19 | 'esc': () => this.props.closeAddForm() 20 | }; 21 | 22 | return ( 23 |
24 | 25 | 32 | { this._renderOptions() } 33 |   34 | 35 |
36 | ); 37 | } 38 | 39 | _renderOptions() { 40 | let options = this.props.source.getConnectedNodesOptions; 41 | return options ? Object.keys(options).map(key => { 42 | return (); 47 | }) : null; 48 | } 49 | 50 | _handleSubmit(e) { 51 | let num = parseInt(this.refs.num.value); 52 | let nodeId = this.props.data.id; 53 | let nodeIds = Object.keys(this.props.graph.nodes); 54 | let options = this._options(); 55 | 56 | this.props.source.getConnectedNodes(nodeId, nodeIds, options, (data) => { 57 | this.props.addSurroundingNodes(nodeId, data.nodes); 58 | 59 | data.edges.forEach(edge => { 60 | if (!this.props.graph.edges[edge.id]) { 61 | this.props.addEdge(edge); 62 | } 63 | }); 64 | }); 65 | 66 | e.preventDefault(); 67 | } 68 | 69 | _options() { 70 | return Object.keys(this.refs).reduce((result, ref) => { 71 | result[ref] = this.refs[ref].value; 72 | return result; 73 | }, {}); 74 | } 75 | 76 | _clear() { 77 | this.refs.num.value = "5"; 78 | Object.keys(this.props.source.getConnectedNodesOptions).forEach(key => { 79 | this.refs[key].value = null 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/components/AddEdgeForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import values from 'lodash/values'; 5 | import sortBy from 'lodash/sortBy'; 6 | import { HotKeys } from 'react-hotkeys'; 7 | 8 | export default class AddEdgeForm extends BaseComponent { 9 | constructor(props) { 10 | super(props); 11 | this.bindAll('_handleSubmit'); 12 | } 13 | 14 | render() { 15 | let node1Id, node2Id; 16 | 17 | if (Array.isArray(this.props.data) && this.props.data.length == 2) { 18 | node1Id = this.props.data[0].id; 19 | node2Id = this.props.data[1].id; 20 | } else { 21 | node1Id = this.props.data ? this.props.data.id : null; 22 | node2Id = null; 23 | } 24 | 25 | const keyMap = { 26 | 'altN': ['alt+n', 'ctrl+n'], 27 | 'esc': 'esc' 28 | }; 29 | 30 | const keyHandlers = { 31 | 'altN': () => this.props.closeAddForm(), 32 | 'esc': () => this._clear() 33 | }; 34 | 35 | let nodes = sortBy(values(this.props.nodes), (node) => node.display.name); 36 | 37 | return ( 38 |
39 | 40 |
41 | 47 | 53 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | _handleSubmit(e) { 61 | let node1Id = this.refs.node1Id.value; 62 | let node2Id = this.refs.node2Id.value; 63 | let label = this.refs.label.value.trim(); 64 | 65 | if (node1Id && node2Id && label) { 66 | this.props.addEdge({ node1_id: node1Id, node2_id: node2Id, display: { label } }); 67 | this._clear(); 68 | this.props.closeAddForm(); 69 | } 70 | 71 | e.preventDefault(); 72 | } 73 | 74 | _clear() { 75 | this.refs.node1Id.value = ''; 76 | this.refs.node2Id.value = ''; 77 | this.refs.label.value = ''; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/components/AddNodeForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import AddNodeResult from './AddNodeResult'; 5 | import { HotKeys } from 'react-hotkeys'; 6 | 7 | export default class AddNodeForm extends BaseComponent { 8 | constructor(props) { 9 | super(props); 10 | this.bindAll('_handleSubmit', '_handleSearch'); 11 | } 12 | 13 | render() { 14 | const keyMap = { 15 | 'altN': ['alt+n', 'ctrl+n'], 16 | 'esc': 'esc' 17 | }; 18 | 19 | const keyHandlers = { 20 | 'altN': () => this.props.closeAddForm(), 21 | 'esc': () => this.props.closeAddForm() 22 | }; 23 | 24 | // filter existing nodes out of results 25 | const results = this.props.results.filter(node => !this.props.nodes[node.id]); 26 | 27 | return ( 28 |
29 | 30 |

Add Node

31 |
32 |
33 | 34 | { this.props.source && results.length > 0 ? 35 |
36 | or import from {this.props.source.name}:   37 | { results.map((node, i) => 38 | 45 | ) } 46 |
: null } 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | componentWillUnmount() { 54 | window.clearTimeout(this.timeout); 55 | } 56 | 57 | _handleSubmit(e) { 58 | let name = this.refs.name.value.trim(); 59 | this.props.addNode({ display: { name } }); 60 | this._clear(); 61 | this.props.closeAddForm(); 62 | e.preventDefault(); 63 | } 64 | 65 | _handleSearch() { 66 | // text and source required for search 67 | if (this.props.source) { 68 | let that = this; 69 | 70 | // cancel previously queued search 71 | window.clearTimeout(this.timeout); 72 | 73 | // queue new search 74 | this.timeout = setTimeout(() => { 75 | let query = that.refs.name.value.trim(); 76 | 77 | if (query) { 78 | that.props.source.findNodes(query, nodes => that._addResults(nodes)); 79 | } 80 | }, 200); 81 | } 82 | } 83 | 84 | _addResults(nodes) { 85 | this.props.setNodeResults(nodes); 86 | } 87 | 88 | _clear() { 89 | this.refs.name.value = ''; 90 | } 91 | } -------------------------------------------------------------------------------- /app/components/AddNodeInput.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import BaseComponent from './BaseComponent'; 5 | import AddNodeResult from './AddNodeResult'; 6 | import { HotKeys } from 'react-hotkeys'; 7 | 8 | export default class AddNodeInput extends BaseComponent { 9 | constructor(props) { 10 | super(props); 11 | this.bindAll('_handleSubmit', '_handleSearch'); 12 | } 13 | 14 | render() { 15 | // filter existing nodes out of results 16 | const results = this.props.results.filter(node => !this.props.nodes[node.id]); 17 | 18 | const keyMap = { 19 | 'esc': 'esc' 20 | }; 21 | 22 | const keyHandlers = { 23 | 'esc': () => this.clear() 24 | }; 25 | 26 | return ( 27 |
28 | 29 |
30 |
31 | { this.props.source ? 32 |
    0 ? "block" : "none" }} ref="results"> 33 | { results.map((node, i) => 34 | this.clear()} /> 42 | ) } 43 |
: null } 44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | componentWillUnmount() { 51 | window.clearTimeout(this.timeout); 52 | } 53 | 54 | clear() { 55 | this.refs.name.value = ''; 56 | this.refs.name.blur(); 57 | this.props.setNodeResults([]); 58 | } 59 | 60 | _handleSubmit(e) { 61 | let name = this.refs.name.value.trim(); 62 | this.props.addNode({ display: { name } }); 63 | this.clear(); 64 | this.props.closeAddForm(); 65 | e.preventDefault(); 66 | } 67 | 68 | _handleSearch() { 69 | // text and source required for search 70 | if (this.props.source) { 71 | let that = this; 72 | 73 | // cancel previously queued search 74 | window.clearTimeout(this.timeout); 75 | 76 | // queue new search 77 | this.timeout = setTimeout(() => { 78 | let query = that.refs.name.value.trim(); 79 | 80 | if (query) { 81 | that.props.source.findNodes(query, nodes => that._addResults(nodes)); 82 | } else { 83 | this.setState({ results: [] }) 84 | } 85 | }, 200); 86 | } 87 | } 88 | 89 | _addResults(nodes) { 90 | this.props.setNodeResults(nodes); 91 | } 92 | } -------------------------------------------------------------------------------- /app/components/AddNodeResult.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | 5 | export default class AddNodeResult extends BaseComponent { 6 | constructor(props) { 7 | super(props); 8 | this.bindAll('_handleClick'); 9 | } 10 | 11 | render() { 12 | return ( 13 |
  • {this.props.node.display.name}
  • 14 | ); 15 | } 16 | 17 | _handleClick(e) { 18 | let { source, node, nodes } = this.props; 19 | 20 | if (source) { 21 | let nodeIds = Object.keys(nodes); 22 | 23 | let callback = (data) => { 24 | this.props.addNode(data.node); 25 | data.edges.forEach(edge => this.props.addEdge(edge)); 26 | this.props.clearResults(); 27 | }; 28 | 29 | source.getNodeWithEdges(node.id, nodeIds, callback); 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/components/AnnotationsTracker.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import times from 'lodash/times'; 5 | 6 | export default class AnnotationsTracker extends BaseComponent { 7 | constructor(props) { 8 | super(props); 9 | this.bindAll('_circle'); 10 | } 11 | 12 | render () { 13 | return ( 14 |
    15 | { times(this.props.annotationCount, this._circle) } 16 |
    17 | ) 18 | } 19 | 20 | _circle (i) { 21 | if (i === this.props.currentIndex) { 22 | return (
    ); 23 | } else { 24 | return (
    ); 25 | } 26 | } 27 | 28 | } 29 | 30 | AnnotationsTracker.propTypes = { 31 | annotationCount: PropTypes.number.isRequired, 32 | currentIndex: PropTypes.number.isRequired 33 | } 34 | -------------------------------------------------------------------------------- /app/components/BaseComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class BaseComponent extends Component { 4 | bindAll(...methods) { 5 | methods.forEach(method => { this[method] = this[method].bind(this); }); 6 | } 7 | } 8 | 9 | module.exports = BaseComponent; 10 | -------------------------------------------------------------------------------- /app/components/Caption.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import BaseComponent from './BaseComponent'; 5 | import { DraggableCore } from 'react-draggable'; 6 | import ds from '../CaptionDisplaySettings'; 7 | import merge from 'lodash/merge'; 8 | import { calculateDeltas } from '../helpers'; 9 | 10 | export default class Caption extends BaseComponent { 11 | constructor(props) { 12 | super(props); 13 | this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick'); 14 | this.state = props.caption.display; 15 | } 16 | 17 | render() { 18 | let { x, y, text, scale, status } = this.state; 19 | let transform = `translate(${x}, ${y})`; 20 | let highlighted = status == "highlighted"; 21 | 22 | return ( 23 | 28 | 29 | { highlighted ? this._highlightRect() : null } 30 | { this.props.selected ? this._selectionRect() : null } 31 | {text} 32 | 33 | 34 | ); 35 | } 36 | 37 | componentDidMount() { 38 | this._setRectWidths(); 39 | } 40 | 41 | componentDidUpdate(prevProps) { 42 | let prevDisplay = prevProps.caption.display; 43 | let display = this.props.caption.display; 44 | this._setRectWidths(); 45 | } 46 | 47 | componentWillReceiveProps(props) { 48 | let newState = merge({ text: null }, props.caption.display); 49 | this.setState(newState); 50 | } 51 | 52 | shouldComponentUpdate(nextProps, nextState) { 53 | return nextProps.selected !== this.props.selected || 54 | JSON.stringify(nextState) !== JSON.stringify(this.state); 55 | } 56 | 57 | _setRectWidths() { 58 | let element = ReactDOM.findDOMNode(this); 59 | let text = element.querySelector(".handle"); 60 | let highlightRect = this.refs.highlightRect; 61 | let selectRect = this.refs.selectRect; 62 | let heightAdj = -6 + this.props.caption.display.scale * 3; 63 | let textWidth = text.getComputedTextLength(); 64 | let textRect = text.getBoundingClientRect() 65 | let textHeight = textRect.bottom - textRect.top; 66 | 67 | if (highlightRect) { 68 | highlightRect.setAttribute("width", textWidth + 10); 69 | highlightRect.setAttribute("x", -5); 70 | highlightRect.setAttribute("height", textHeight + 10); 71 | highlightRect.setAttribute("y", -textHeight + heightAdj); 72 | } 73 | 74 | if (selectRect) { 75 | selectRect.setAttribute("width", textWidth + 10); 76 | selectRect.setAttribute("x", -5); 77 | selectRect.setAttribute("height", textHeight + 10); 78 | selectRect.setAttribute("y", - textHeight + heightAdj); 79 | } 80 | } 81 | 82 | _handleDragStart(e, data) { 83 | e.preventDefault(); 84 | this._startDrag = data; 85 | this._startPosition = { 86 | x: this.state.x, 87 | y: this.state.y 88 | }; 89 | } 90 | 91 | _handleDrag(e, data) { 92 | if (this.props.isLocked) return; 93 | this._dragging = true; 94 | let { x, y } = calculateDeltas(data, this._startPosition, this._startDrag, this.graph.state.actualZoom); 95 | this.setState({ x, y }); 96 | } 97 | 98 | _handleDragStop(e, data) { 99 | // event fires every mouseup so we check for actual drag before updating store 100 | if (this._dragging) { 101 | this.props.moveCaption(this.props.caption.id, this.state.x, this.state.y); 102 | } 103 | } 104 | 105 | _handleClick() { 106 | if (this._dragging) { 107 | this._dragging = false; 108 | } else if (this.props.clickCaption) { 109 | this.props.clickCaption(this.props.caption.id); 110 | } 111 | } 112 | 113 | _selectionRect() { 114 | let width = this.state.text.length * 8; 115 | let height = ds.lineHeight; 116 | 117 | return ( 118 | 127 | ); 128 | } 129 | 130 | _highlightRect() { 131 | let width = this.state.text.length * 8; 132 | let height = ds.lineHeight; 133 | 134 | return ( 135 | 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/components/ChangeColorInput.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import { CompactPicker } from 'react-color'; 5 | import ds from '../NodeDisplaySettings'; 6 | 7 | export default class ChangeColorInput extends BaseComponent { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.bindAll("handleClick", "handleClose", "handleValueChange", "handleClearClick", "onChange"); 12 | this.state = { 13 | displayColorPicker: false, 14 | color: props.value 15 | }; 16 | } 17 | 18 | handleClick() { 19 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 20 | } 21 | 22 | handleClose() { 23 | this.setState({ displayColorPicker: false }); 24 | } 25 | 26 | handleClearClick() { 27 | this.handleClose(); 28 | this.onChange(ds.circleColor[this.props.status]); 29 | } 30 | 31 | handleValueChange(color) { 32 | this.onChange(color.hex); 33 | } 34 | 35 | onChange(newColor) { 36 | this.setState({ color: newColor }); 37 | this.props.onChange(newColor); 38 | } 39 | 40 | render() { 41 | return ( 42 |
    43 |
    44 |
    45 | 48 |
    49 |
    50 | { this.state.displayColorPicker && 51 |
    52 |
    53 | 54 |
    55 | } 56 |
    57 |
    58 | ); 59 | } 60 | 61 | componentWillReceiveProps(props) { 62 | this.setState({ color: props.value || ds.circleColor[props.status] }); 63 | } 64 | } -------------------------------------------------------------------------------- /app/components/DeleteSelectedButton.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import { HotKeys } from 'react-hotkeys'; 5 | import mapKeys from 'lodash/mapKeys'; 6 | 7 | export default class DeleteSelectedButton extends BaseComponent { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | const whichClass = this.props.currentForm === "UpdateNodeForm" ? "nodeDelete" : "edgeDelete"; 14 | 15 | return ( 16 |
    19 | 25 |
    26 | ); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/components/EdgeArrowSelector.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import { newArrowState } from '../helpers'; 5 | import capitalize from 'lodash/capitalize'; 6 | 7 | export default class EdgeArrowSelector extends BaseComponent { 8 | 9 | constructor() { 10 | super(); 11 | this.state = { isOpen: false }; 12 | this.bindAll('_updateArrow', '_arrowClass'); 13 | } 14 | 15 | render() { 16 | let { arrowSide } = this.props; 17 | return( 18 |
    19 |
    this.setState({isOpen: true})} > 21 |
    22 | 23 |
    24 |
    25 | { this.state.isOpen && 26 |
      27 |
    • this._updateArrow(false) }> 29 | 30 |
    • 31 |
    • this._updateArrow(true) }> 33 | 34 |
    • 35 |
    36 | }
    ) 37 | } 38 | 39 | _arrowClass() { 40 | let { arrow, arrowSide } = this.props; 41 | if (this._displayArrow(arrowSide, arrow)) { 42 | return `svgDropdown${capitalize(arrowSide)}Arrow` 43 | } else { 44 | return ''; 45 | } 46 | } 47 | 48 | _updateArrow(showArrow) { 49 | const oldArrowState = this.props.arrow; 50 | const arrowSide = this.props.arrowSide; 51 | const _newArrowState = newArrowState(oldArrowState, arrowSide, showArrow) 52 | if (oldArrowState !== _newArrowState) { 53 | this.props.updateEdge(this.props.edgeId, {display: {arrow: _newArrowState }}); 54 | } 55 | this.setState({isOpen: false}); 56 | } 57 | 58 | _sideToNode(side) { 59 | if (side === 'left') { 60 | return 'node1' 61 | } else if (side === 'right') { 62 | return 'node2' 63 | } else { 64 | console.error('sideToNode only accepts left or right') 65 | return ''; 66 | } 67 | } 68 | 69 | _displayArrow(arrowSide, arrowState) { 70 | let node = this._sideToNode(arrowSide); 71 | let node1 = (node === 'node1'); 72 | let node2 = (node === 'node2'); 73 | if (arrowState === 'both') { 74 | return true; 75 | } 76 | if (node1 && arrowState === '2->1') { 77 | return true; 78 | } else if (node2 && arrowState === '1->2') { 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | } 84 | } 85 | 86 | /* 87 | arrowSide must be exactly 'left' or 'right' 88 | Possible arrow states: 'left', 'right', 'both', false/true 89 | */ 90 | EdgeArrowSelector.propTypes = { 91 | updateEdge: PropTypes.func.isRequired, 92 | edgeId: PropTypes.any.isRequired, 93 | arrowSide: PropTypes.string.isRequired, 94 | arrow: PropTypes.any 95 | }; 96 | -------------------------------------------------------------------------------- /app/components/EdgeDashSelector.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | 5 | export default class EdgeDashSelector extends BaseComponent { 6 | 7 | constructor(){ 8 | super(); 9 | this.state = { isOpen: false }; 10 | this.bindAll('_menuOptions', '_updateEdge'); 11 | } 12 | 13 | render(){ 14 | return( 15 |
    16 |
    17 |
    this.setState({isOpen: true})} > 20 | 21 |
    22 | {this.state.isOpen && this._menuOptions() } 23 |
    24 |
    ) 25 | } 26 | 27 | _menuOptions(){ 28 | return ( 29 |
      30 |
    • this._updateEdge(false) } > 32 | 33 |
    • 34 |
    • this._updateEdge(true) } > 36 | 37 |
    • 38 |
    39 | ) 40 | } 41 | 42 | _updateEdge(newDashState) { 43 | let { updateEdge, edgeId} = this.props; 44 | updateEdge(edgeId, {display: {dash: newDashState }}); 45 | this.setState({isOpen: false}); 46 | } 47 | 48 | } 49 | 50 | EdgeDashSelector.propTypes = { 51 | isDashed: PropTypes.bool.isRequired, 52 | updateEdge: PropTypes.func.isRequired, 53 | edgeId: PropTypes.any.isRequired 54 | }; 55 | -------------------------------------------------------------------------------- /app/components/EdgeDropdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import BaseComponent from './BaseComponent'; 3 | import { legacyArrowConverter } from '../helpers'; 4 | import EdgeArrowSelector from './EdgeArrowSelector'; 5 | import EdgeDashSelector from './EdgeDashSelector'; 6 | import NodeLabel from './NodeLabel'; 7 | 8 | export default class EdgeDropdown extends Component { 9 | 10 | render() { 11 | const graph = this.props.getGraph(); 12 | const edge = graph.edges[this.props.edgeId]; 13 | const node1_name = graph.nodes[edge.node1_id].display.name; 14 | const node2_name = graph.nodes[edge.node2_id].display.name; 15 | const arrow = this.props.arrow; 16 | 17 | return ( 18 |
    19 |
    20 | {this._nodeCircle(this._truncateName(node1_name))} 21 |
    22 |
    23 | 24 | 25 | 26 |
    27 |
    28 | {this._nodeCircle(this._truncateName(node2_name))} 29 |
    30 |
    31 | ); 32 | } 33 | 34 | _nodeCircle(nodeName) { 35 | return ( 36 | 37 | 38 | 39 | {nodeName} 40 | 41 | ); 42 | }; 43 | 44 | _truncateName(name) { 45 | if (name.length > 33) { 46 | return `${name.slice(0,30)}...`; 47 | } else { 48 | return name; 49 | } 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /app/components/EditButton.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class EditButton extends Component { 5 | 6 | render() { 7 | return ( 8 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/EditButtons.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import AddNodeInput from './AddNodeInput'; 4 | 5 | export default class EditButtons extends Component { 6 | 7 | render() { 8 | return ( 9 |
    10 | 19 | 20 | { this.props.showInterlocksButton && 21 | 22 | } 23 |
    24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/components/EditTools.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import UndoButtons from './UndoButtons'; 5 | import LayoutButtons from './LayoutButtons'; 6 | import EditButtons from './EditButtons'; 7 | import AddEdgeForm from './AddEdgeForm'; 8 | import AddCaptionForm from './AddCaptionForm'; 9 | import AddConnectedNodesForm from './AddConnectedNodesForm'; 10 | import DeleteSelectedButton from './DeleteSelectedButton'; 11 | import UpdateNodeForm from './UpdateNodeForm'; 12 | import UpdateEdgeForm from './UpdateEdgeForm'; 13 | import UpdateCaptionForm from './UpdateCaptionForm'; 14 | import HelpScreen from './HelpScreen'; 15 | 16 | export default class EditTools extends BaseComponent { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.bindAll('_handleDelete'); 21 | } 22 | 23 | render() { 24 | let { graphApi, source, data, graph, addForm, currentForm, helpScreen, 25 | clearGraph, closeAddForm, toggleHelpScreen, toggleAddEdgeForm } = this.props; 26 | 27 | let { zoomIn, zoomOut, resetZoom, prune, circleLayout, 28 | addNode, addEdge, addCaption, addSurroundingNodes, 29 | updateNode, updateEdge, updateCaption, deselectAll, 30 | deleteAll, getGraph } = graphApi; 31 | 32 | return ( 33 |
    34 |
    35 | 47 | { currentForm == 'UpdateCaptionForm' && 48 | } 52 | { currentForm != 'UpdateCaptionForm' && 53 | } 55 | 59 | 64 | 65 | { this.props.hideHelp ? null : } 66 | 67 |
    68 | 69 | { addForm == 'AddEdgeForm' && 70 | } 75 | { currentForm == 'UpdateNodeForm' && 76 | } 80 | { currentForm == 'UpdateEdgeForm' && 81 | } 86 | { currentForm == 'UpdateNodeForm' && source && source.getConnectedNodes && 87 | } 94 | { (currentForm == 'UpdateNodeForm' || currentForm == 'UpdateEdgeForm') && 95 | } 98 | { helpScreen && !this.props.hideHelp ? : null } 99 |
    100 | ); 101 | } 102 | 103 | _handleDelete() { 104 | this.props.delete(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/components/Editor.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { HotKeys } from 'react-hotkeys'; 5 | import BaseComponent from './BaseComponent'; 6 | import ZoomButtons from './ZoomButtons'; 7 | import EditTools from './EditTools'; 8 | import merge from 'lodash/merge'; 9 | import values from 'lodash/values'; 10 | import cloneDeep from 'lodash/cloneDeep'; 11 | import pick from 'lodash/pick'; 12 | require('../styles/bootstrap-3.3.6.css'); 13 | require('../styles/oligrapher.editor.css'); 14 | 15 | export default class Editor extends BaseComponent { 16 | constructor(props) { 17 | super(props); 18 | this.state = { helpScreen: false }; 19 | } 20 | 21 | render() { 22 | let zoomIn, zoomOut, resetZoom; 23 | 24 | if (this.props.graphApi) { 25 | zoomIn = () => this.props.graphApi.zoomIn(); 26 | zoomOut = () => this.props.graphApi.zoomOut(); 27 | resetZoom = () => this.props.graphApi.resetZoom(); 28 | } 29 | 30 | const keyMap = { 31 | 'altP': ['alt+p', 'ctrl+p'], 32 | 'altO': ['alt+o', 'ctrl+o'], 33 | 'altN': ['alt+n', 'ctrl+n'], 34 | 'altE': ['alt+e', 'ctrl+e'], 35 | 'altH': ['alt+h', 'ctrl+h'], 36 | 'altR': ['alt+r', 'ctrl+r'], 37 | 'esc': 'esc', 38 | 'enter': 'enter' 39 | }; 40 | 41 | const keyHandlers = { 42 | 'altP': () => this.props.graphApi.prune(), 43 | 'altO': () => this.props.graphApi.circleLayout(), 44 | 'altN': () => this._focusAddNodeInput(), 45 | 'altE': () => this._toggleAddEdgeForm(), 46 | 'altH': () => this._toggleHelpScreen(), 47 | 'altR': () => this._toggleAddConnectedNodesForm(), 48 | 'esc': () => this._clearForms() 49 | }; 50 | 51 | let _closeAddForm = () => this.props.toggleAddForm(null); 52 | 53 | let { currentForm, formData, addForm } = this._computeEditForms(this.props.selection); 54 | 55 | let fetchInterlocks = () => {}; 56 | let showInterlocksButton = ( 57 | this.props.isEditor && this.props.dataSource && this.props.dataSource.getInterlocks && formData && formData.length == 2 58 | ); 59 | 60 | if (showInterlocksButton) { 61 | let node1Id = formData[0].id; 62 | let node2Id = formData[1].id; 63 | let nodeIds = Object.keys(this.props.graph.nodes); 64 | fetchInterlocks = () => { 65 | this.props.fetchInterlocks(node1Id, node2Id, nodeIds, this.props.dataSource.getInterlocks); 66 | } 67 | } 68 | 69 | return ( 70 |
    71 | 72 | 73 | { this.props.showEditButton && this.props.isEditor && 74 | 80 | } 81 | { this.props.showEditTools && 82 | this._toggleAddEdgeForm()} 88 | toggleHelpScreen={() => this._toggleHelpScreen()} 89 | clearGraph={() => this._clearGraph()} 90 | data={formData} 91 | addForm={addForm} 92 | currentForm={currentForm} 93 | showInterlocksButton={showInterlocksButton} 94 | fetchInterlocks={fetchInterlocks} 95 | delete={this.props.delete} /> 96 | } 97 | 98 |
    99 | ); 100 | } 101 | 102 | _computeEditForms() { 103 | let currentForm = null; 104 | let formData = null; 105 | let addForm = this.props.addForm; 106 | 107 | let { nodeIds, edgeIds, captionIds } = this.props.selection; 108 | let graph = this.props.graph; 109 | let nodes = pick(graph.nodes, nodeIds); 110 | let edges = pick(graph.edges, edgeIds); 111 | let captions = pick(graph.captions, captionIds); 112 | let nodeCount = Object.keys(nodes).length; 113 | let edgeCount = Object.keys(edges).length; 114 | let captionCount = Object.keys(captions).length; 115 | 116 | if (nodeCount == 1 && edgeCount == 0 && captionCount == 0) { 117 | if (addForm != 'AddEdgeForm') { currentForm = 'UpdateNodeForm'; } 118 | formData = values(nodes)[0]; 119 | } else if (nodeCount == 0 && edgeCount == 1 && captionCount == 0) { 120 | currentForm = 'UpdateEdgeForm'; 121 | addForm = null; 122 | formData = values(edges)[0]; 123 | } else if (nodeCount == 0 && edgeCount == 0 && captionCount == 1) { 124 | currentForm = 'UpdateCaptionForm'; 125 | addForm = null; 126 | formData = values(captions)[0]; 127 | } else if (nodeCount == 2 && edgeCount == 0 && captionCount == 0) { 128 | currentForm = null; 129 | addForm = 'AddEdgeForm'; 130 | formData = values(nodes); 131 | } else { 132 | currentForm = null; 133 | formData = null; 134 | } 135 | 136 | return { currentForm, formData, addForm }; 137 | } 138 | 139 | _toggleAddEdgeForm() { 140 | this.props.toggleAddForm('AddEdgeForm'); 141 | } 142 | 143 | _toggleAddCaptionForm() { 144 | this.props.toggleAddForm('AddCaptionForm'); 145 | } 146 | 147 | _toggleAddConnectedNodesForm() { 148 | this.props.toggleAddForm('AddConnectedNodesForm'); 149 | } 150 | 151 | _toggleHelpScreen() { 152 | this.props.toggleHelpScreen(); 153 | } 154 | 155 | _clearGraph() { 156 | if (confirm("Are you sure you want to clear the graph? This can't be undone!")) { 157 | this.props.graphApi.deleteAll(); 158 | this.props.toggleAddForm(null); 159 | } 160 | } 161 | 162 | _clearForms() { 163 | this.props.toggleAddForm(null); 164 | this.props.graphApi.deselectAll(); 165 | this.refs.editTools.refs.editButtons.refs.addNodeInput.clear(); 166 | } 167 | 168 | _focusAddNodeInput() { 169 | this.refs.editTools.refs.editButtons.refs.addNodeInput.refs.name.focus(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/components/EmbeddedGraphAnnotation.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { Scrollbars } from 'react-custom-scrollbars'; 4 | 5 | // some magic numbers: 6 | // the hight of the embeddedNavBar (set via css) + container margin offset 7 | const TRACKER_OFFSET = 38; 8 | // bootstrap adds negative margins of -15 9 | const MARGIN_OFFSET = 15; 10 | 11 | export default class EmbeddedGraphAnnotation extends Component { 12 | 13 | render() { 14 | let { header, text } = this.props.annotation; 15 | let hasTracker = this.props.hasTracker; 16 | let { annotationHeight } = this.props.embedded; 17 | let height = annotationHeight - MARGIN_OFFSET - (hasTracker ? TRACKER_OFFSET : 0); 18 | 19 | let divStyle = { 20 | marginTop: '10px' 21 | } 22 | 23 | let scrollbarStyle = { 24 | height: height, 25 | width: '100%' 26 | } 27 | 28 | return ( 29 |
    30 | 33 |
    {header}
    34 |
    36 |
    37 |
    38 |
    39 | ); 40 | } 41 | 42 | } 43 | 44 | EmbeddedGraphAnnotation.propTypes = { 45 | annotation: PropTypes.object, 46 | embedded: PropTypes.object, 47 | hasTracker: PropTypes.bool 48 | } 49 | -------------------------------------------------------------------------------- /app/components/EmbeddedGraphAnnotations.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import EmbeddedNavBar from './EmbeddedNavBar'; 4 | import EmbeddedGraphAnnotation from './EmbeddedGraphAnnotation'; 5 | 6 | export default class EmbeddedGraphAnnotations extends Component { 7 | render () { 8 | let hasTracker = this.props.annotationCount > 1; 9 | let { logoUrl, annotationHeight, logoWidth } = this.props.embedded; 10 | 11 | return ( 12 |
    13 |
    14 | { hasTracker && 15 | } 21 |
    22 | 23 |
    24 | 25 |
    26 | 27 | { logoUrl && 28 |
    29 | Oligrapher Logo 30 |
    31 | } 32 |
    33 | ); 34 | } 35 | } 36 | 37 | EmbeddedGraphAnnotations.propTypes = { 38 | embedded: PropTypes.object, 39 | annotationCount: PropTypes.number.isRequired, 40 | currentIndex: PropTypes.number.isRequired, 41 | annotation: PropTypes.object, 42 | nextClick: PropTypes.func, 43 | prevClick: PropTypes.func 44 | } 45 | -------------------------------------------------------------------------------- /app/components/EmbeddedNavBar.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import EmbeddedNavButtons from './EmbeddedNavButtons'; 5 | import AnnotationsTracker from './AnnotationsTracker'; 6 | 7 | export default class EmbeddedNavBar extends BaseComponent { 8 | constructor(props) { 9 | super(props); 10 | } 11 | render () { 12 | return ( 13 |
    14 | 19 | 23 |
    24 | ) 25 | } 26 | 27 | } 28 | 29 | EmbeddedNavBar.propTypes = { 30 | currentIndex: PropTypes.number, 31 | annotationCount: PropTypes.number.isRequired, 32 | nextClick: PropTypes.func.isRequired, 33 | prevClick: PropTypes.func.isRequired 34 | } 35 | -------------------------------------------------------------------------------- /app/components/EmbeddedNavButtons.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from "./BaseComponent"; 4 | 5 | export default class EmbeddedNavButtons extends BaseComponent { 6 | constructor(props) { 7 | super(props); 8 | this.bindAll('_isBackButtonDisabled', '_isNextButtonDisabled'); 9 | } 10 | render () { 11 | return ( 12 |
    13 | 18 | 19 | 24 |
    25 | ) 26 | } 27 | 28 | _isBackButtonDisabled() { 29 | return this.props.currentIndex === 0; 30 | } 31 | 32 | _isNextButtonDisabled() { 33 | return (this.props.currentIndex + 1) === this.props.annotationCount; 34 | } 35 | 36 | } 37 | 38 | EmbeddedNavButtons.propTypes = { 39 | currentIndex: PropTypes.number.isRequired, 40 | annotationCount: PropTypes.number.isRequired, 41 | nextClick: PropTypes.func.isRequired, 42 | prevClick: PropTypes.func.isRequired 43 | } 44 | -------------------------------------------------------------------------------- /app/components/GraphAnnotation.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class GraphAnnotation extends Component { 5 | 6 | render() { 7 | return ( 8 |
    9 |

    {this.props.annotation.header}

    10 |
    13 | { this.props.isEditor ? : null } 17 |
    18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/components/GraphAnnotationForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import merge from 'lodash/merge'; 5 | import pick from 'lodash/pick'; 6 | import Editor from 'react-medium-editor'; 7 | 8 | export default class GraphAnnotationForm extends BaseComponent { 9 | constructor(props) { 10 | super(props); 11 | this.bindAll('_handleHeaderChange', '_handleTextChange', '_handleRemove'); 12 | } 13 | 14 | render() { 15 | let editorOptions = { 16 | toolbar: { buttons: [ 17 | 'bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote', 'unorderedlist', 'orderedlist' 18 | ] }, 19 | targetBlank: true, 20 | placeholder: { text: "annotation text" } 21 | } 22 | 23 | 24 | return ( 25 |
    26 | 35 | 43 | 47 |
    48 | ); 49 | } 50 | 51 | _handleRemove() { 52 | if (confirm("Are you sure you want to delete this annotation?")) { 53 | this.props.remove(); 54 | } 55 | } 56 | 57 | _handleHeaderChange() { 58 | this.props.update({ header: this.refs.header.value }); 59 | } 60 | 61 | _handleTextChange(text) { 62 | this.props.update({ text }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/components/GraphAnnotationList.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | 5 | export default class GraphAnnotationList extends BaseComponent { 6 | constructor(props) { 7 | super(props); 8 | this.bindAll('_handleClick', '_handleDragOver', '_handleDragStart', '_handleDragEnd'); 9 | this._placeholder = document.createElement("li"); 10 | this._placeholder.className = "placeholder"; 11 | } 12 | 13 | render() { 14 | return ( 15 |
    16 |
      17 | { this.props.annotations.map((annotation, index) => 18 |
    • 26 | {annotation.header.trim().length > 0 ? annotation.header : "Untitled Annotation"} 27 |
    • 28 | ) } 29 |
    30 | { this.props.isEditor ? 31 | : null } 38 |
    39 | ); 40 | } 41 | 42 | _handleClick(e) { 43 | this.props.show(parseInt(e.target.dataset.id)); 44 | 45 | if (this.props.isEditor) { 46 | this.props.hideEditTools(); 47 | }; 48 | } 49 | 50 | _handleDragStart(e) { 51 | this._startY = e.clientY; 52 | this._dragged = e.currentTarget; 53 | this._placeholder.innerHTML = e.currentTarget.innerHTML; 54 | 55 | e.dataTransfer.effectAllowed = "move"; 56 | e.dataTransfer.setData("text/html", e.currentTarget); 57 | } 58 | 59 | _handleDragEnd(e) { 60 | this._dragged.style.display = "block"; 61 | this._dragged.parentNode.removeChild(this._placeholder); 62 | 63 | // update store 64 | let from = Number(this._dragged.dataset.id); 65 | let to = Number(this._over.dataset.id); 66 | 67 | this.props.move(from, to); 68 | 69 | this._startY = undefined; 70 | } 71 | 72 | _handleDragOver(e) { 73 | e.preventDefault(); 74 | 75 | let thisHeight = this._dragged.offsetHeight; 76 | this._dragged.style.display = "none"; 77 | 78 | if (e.target.className == "placeholder") return; 79 | 80 | this._over = e.target; 81 | 82 | let relY = e.clientY - this._startY; 83 | let height = (this._over.offsetHeight || thisHeight) / 2; 84 | let parent = e.target.parentNode; 85 | 86 | if (relY > height) { 87 | parent.insertBefore(this._placeholder, e.target.nextElementSibling); 88 | } 89 | else if (relY < height) { 90 | parent.insertBefore(this._placeholder, e.target); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /app/components/GraphAnnotationListItem.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | 5 | export default class GraphAnnotationListItem extends BaseComponent { 6 | constructor(props) { 7 | super(props); 8 | this.bindAll('_handleDragStart', '_handleDragEnd'); 9 | } 10 | 11 | render() { 12 | let active = this.props.currentIndex == this.props.index; 13 | 14 | return ( 15 |
  • 21 | {this.props.annotation.header} 22 |
  • 23 | ); 24 | } 25 | 26 | _handleDragStart() { 27 | 28 | } 29 | 30 | _handleDragEnd() { 31 | 32 | } 33 | 34 | _handleClick() { 35 | this.props.show(this.props.index); 36 | } 37 | } -------------------------------------------------------------------------------- /app/components/GraphAnnotations.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from "./BaseComponent"; 4 | import GraphNavButtons from './GraphNavButtons'; 5 | import GraphAnnotationList from './GraphAnnotationList'; 6 | import GraphAnnotation from './GraphAnnotation'; 7 | import GraphAnnotationForm from './GraphAnnotationForm'; 8 | 9 | export default class GraphAnnotations extends BaseComponent { 10 | constructor(props) { 11 | super(props); 12 | this.bindAll('_remove', '_update'); 13 | } 14 | 15 | render() { 16 | let { prevClick, nextClick, isEditor, editForm, navList, 17 | swapAnnotations, annotation, currentIndex, 18 | update, remove, swapEditForm, annotations, show, 19 | create, move, canClickPrev, canClickNext } = this.props; 20 | 21 | let navComponent = ( 22 | 23 | ); 24 | 25 | let formComponent = ( 26 | 30 | ); 31 | 32 | let annotationComponent = ( 33 | 34 | ); 35 | 36 | let navListComponent = ( 37 | 38 | ); 39 | 40 | return ( 41 |
    42 | { (annotation || isEditor) && navComponent } 43 | { isEditor && navList && navListComponent } 44 | { annotation && (isEditor ? formComponent : annotationComponent) } 45 |
    46 | ); 47 | } 48 | 49 | _remove() { 50 | this.props.remove(this.props.currentIndex); 51 | } 52 | 53 | _update(data) { 54 | this.props.update(this.props.currentIndex, data); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/components/GraphByLine.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class GraphByLine extends Component { 5 | 6 | render() { 7 | let { user, date } = this.props; 8 | 9 | return ( 10 |
    11 | { user ? this._renderUser(user) : null } 12 | { date ? {date} : null } 13 |
    14 | ); 15 | } 16 | 17 | _renderUser(user) { 18 | return ( 19 | by { user.url ? {user.name} : {user.name} }   20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /app/components/GraphHeader.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import GraphTitle from './GraphTitle'; 4 | import GraphTitleForm from './GraphTitleForm'; 5 | import GraphByLine from './GraphByLine'; 6 | import GraphLinks from './GraphLinks'; 7 | 8 | 9 | export default class GraphHeader extends Component { 10 | 11 | render() { 12 | let { user, date, links, title, isEditor, updateTitle, url, isEmbedded, embedded } = this.props; 13 | 14 | return ( 15 |
    17 | { isEditor ? 18 | : 19 | } 20 | { (!isEmbedded && (user || date)) ? : null } 21 | { (links && !isEmbedded) ? : null } 22 | 23 |
    24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/components/GraphLinks.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class GraphLinks extends Component { 5 | 6 | render() { 7 | let { links } = this.props; 8 | 9 | return ( 10 | 15 | ); 16 | } 17 | 18 | _getLink(link, i) { 19 | return {link.text}; 20 | } 21 | 22 | _postLink(link, i) { 23 | return ( 24 |
    25 | 26 | e.target.parentElement.submit()}>{link.text} 27 |
    28 | ); 29 | } 30 | 31 | _renderUser(user) { 32 | return ( 33 | by { user.url ? {user.name} : {user.name} }   34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /app/components/GraphNavButtons.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class GraphNavButtons extends Component { 5 | 6 | render() { 7 | var shouldShowNav = this.props.isEditor || this.props.annotations.length > 1; 8 | 9 | return ( 10 |
    11 | { shouldShowNav && 12 |
    13 | 18 | 23 |
    24 | } 25 |
    26 | 33 |
    34 |
    35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /app/components/GraphSettingsForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import titleize from 'titleize'; 4 | 5 | export default class GraphSettingsForm extends Component { 6 | 7 | render() { 8 | return ( 9 |
    10 | { Object.keys(this.props.settings).map(key => 11 |
    12 | {titleize(key.replace(/[_-]+/, " "))}  13 | this.handleChange(event)} /> 19 |
    20 | ) } 21 |
    22 | ); 23 | } 24 | 25 | handleChange(event) { 26 | let key = event.target.name; 27 | let value = event.target.checked; 28 | this.props.updateSettings({ [key]: value }); 29 | } 30 | } -------------------------------------------------------------------------------- /app/components/GraphTitle.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class GraphTitle extends Component { 5 | 6 | render() { 7 | let h1Style = Boolean(this.props.isEmbedded) ? this.props.embedded.headerFontStyle : {}; 8 | return ( 9 |

    { this.props.url ? {this.props.title} : this.props.title }

    10 | ); 11 | } 12 | } 13 | 14 | GraphTitle.PropTypes = { 15 | url: PropTypes.string, 16 | title: PropTypes.string, 17 | isEmbedded: PropTypes.boolean, 18 | embedded: PropTypes.object 19 | } 20 | -------------------------------------------------------------------------------- /app/components/GraphTitleForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class GraphTitleForm extends Component { 5 | 6 | render() { 7 | return ( 8 |

    9 | this._handleChange(event)} /> 15 |

    16 | ); 17 | } 18 | 19 | _handleChange(event) { 20 | this.props.updateTitle(this.refs.title.value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/components/HelpButton.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class HelpButton extends Component { 5 | 6 | render() { 7 | return ( 8 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/HelpScreen.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class HelpScreen extends Component { 5 | 6 | render() { 7 | return ( 8 |
    9 |
    10 |

    User Guide

    11 | Use this editor to create a network graph along with an optional series of annotations overlaying the graph. Annotations consists of a title, a text body, and a highlighted section of the graph.
    12 |
    13 | The pencil button swaps between graph editing mode and annotation editing mode. It will appear green when editing the graph and yellow when editing annotations. You can edit the graph title at the top of the screen.
    14 |
    15 | When you are finished editing, click the SAVE button to save your changes.
    16 |
    17 | Graph Editing Mode
    18 |
    19 | Type a name in the "add node" box and press enter to add the node to the graph. 20 | { this.props.source ? ` Nodes from ${this.props.source.name} matching the name you type will appear below; click on them to add them to the graph.` : "" } 21 |
    22 | ALT+C opens a form for adding a new caption in the top right of the graph.
    23 |
    24 | CLICK a node, edge, or caption to select or deselect it.
    25 | SHIFT+CLICK to select mutiple nodes, edges, or captions.
    26 |
    27 | Select a single node, edge, or caption to view an editing form in the top-right corner of the map. Changes you make in the form will upate the item immediately. Selecting two nodes will display a form to add an edge between them. 28 | { this.props.source && this.props.source.getInterlocks && `Selecting two nodes will also display a button to add interlocks, meaning nodes that they are both connected to.` } 29 |
    30 |
    31 | The CIRCLE button arranges nodes in a circle.
    32 | The PRUNE button removes unconnected nodes.
    33 | The CLEAR button deletes all content from the graph.
    34 | The HELP button displays this user guide.
    35 |
    36 | Annotation Editing Mode
    37 |
    38 | Annotations are edited using the sidebar on the right. Click the big "A" button to hide or show the sidebar.
    39 |
    40 | Click the NEW ANNOTATION button create a new annotation and display a form for editing it. Click on any annotation title in the list to edit it. A REMOVE button at the bottom of the edit form will delete the annotation. When editing an annotation, click on nodes, edges, or captions from the graph to highlight them in that annotation. Drag annotaions up and down the list to reorder them.
    41 |
    42 | Shortcut Keys
    43 |
    44 | LEFT & RIGHT ARROWS navigate to the previous and next annotations.
    45 | ALT+H toggles this user guide.
    46 | ALT+H swaps the editing mode between graph editing and annotations.
    47 | ALT+D deletes selected nodes and edges.
    48 | ALT+E adds an edge. Selected nodes will be auto-populated in the form.
    49 | ALT+C adds a caption.
    50 |
    51 | If ALT keys interfere with your browser or operating system shortcuts, all of the above shortcuts will work with CTRL instead of ALT.
    52 |
    53 | CTRL+"=" zooms in.
    54 | CTRL+"-" zooms out.
    55 | CTRL+0 resets zoom.
    56 |
    57 | ESC closes all forms.
    58 |
    59 | ); 60 | } 61 | } -------------------------------------------------------------------------------- /app/components/LayoutButtons.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class LayoutButtons extends Component { 5 | 6 | render() { 7 | return ( 8 |
    9 | 10 | 11 | 12 |
    13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /app/components/Node.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import NodeLabel from './NodeLabel'; 5 | import NodeCircle from './NodeCircle'; 6 | import ds from '../NodeDisplaySettings'; 7 | import { DraggableCore } from 'react-draggable'; 8 | import Graph from '../models/Graph'; 9 | import merge from 'lodash/merge'; 10 | import classNames from 'classnames'; 11 | import Helpers from '../models/Helpers'; 12 | import { calculateDeltas } from '../helpers'; 13 | 14 | export default class Node extends BaseComponent { 15 | constructor(props) { 16 | super(props); 17 | this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick'); 18 | this.state = props.node.display; 19 | } 20 | 21 | render() { 22 | const n = this.props.node; 23 | const { x, y, name } = this.state; 24 | const groupId = `node-${n.id}`; 25 | const transform = `translate(${x}, ${y})`; 26 | 27 | return ( 28 | 33 | 38 | 39 | { this.state.name ? : null } 40 | 41 | 42 | ); 43 | } 44 | 45 | componentWillReceiveProps(props) { 46 | let newState = merge({ name: null, image: null, url: null }, props.node.display); 47 | this.setState(newState); 48 | } 49 | 50 | shouldComponentUpdate(nextProps, nextState) { 51 | return nextProps.selected !== this.props.selected || 52 | JSON.stringify(nextState) !== JSON.stringify(this.state); 53 | } 54 | 55 | // keep initial position for comparison with drag position 56 | _handleDragStart(e, data) { 57 | e.preventDefault(); 58 | this._startDrag = data; 59 | this._startPosition = { 60 | x: this.state.x, 61 | y: this.state.y 62 | } 63 | } 64 | 65 | // while dragging node and its edges are updated only in state, not store 66 | _handleDrag(e, data) { 67 | if (this.props.isLocked) return; 68 | 69 | this._dragging = true; // so that _handleClick knows it's not just a click 70 | 71 | let n = this.props.node; 72 | 73 | let { x, y } = calculateDeltas(data, this._startPosition, this._startDrag, this.graph.state.actualZoom); 74 | this.setState({ x, y }); 75 | 76 | // update state of connecting edges 77 | let edges = Graph.edgesConnectedToNode(this.props.graph, n.id); 78 | 79 | edges.forEach(edge => { 80 | let thisNodeNum = edge.node1_id == n.id ? 1 : 2; 81 | let newEdge = Graph.moveEdgeNode(edge, thisNodeNum, x, y); 82 | this.graph.edges[edge.id].setState(newEdge.display); 83 | }); 84 | } 85 | 86 | // store updated once dragging is done 87 | _handleDragStop(e, data) { 88 | // event fires every mouseup so we check for actual drag before updating store 89 | if (this._dragging) { 90 | this.props.moveNode(this.props.node.id, this.state.x, this.state.y); 91 | } 92 | } 93 | 94 | _handleClick() { 95 | if (this._dragging) { 96 | this._dragging = false; 97 | } else if (this.props.clickNode) { 98 | this.props.clickNode(this.props.node.id); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/components/NodeCircle.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import BaseComponent from './BaseComponent'; 5 | import ds from '../NodeDisplaySettings'; 6 | 7 | export default class NodeCircle extends BaseComponent { 8 | render() { 9 | return ( 10 | 11 | { this.props.selected ? this._selectionCirlce() : null } 12 | {this._bgCircle()} 13 | {this._circle()} 14 | 15 | ); 16 | } 17 | 18 | componentDidMount() { 19 | let element = ReactDOM.findDOMNode(this); 20 | let images = element.querySelectorAll("image"); 21 | 22 | for (var i = 0; i < images.length; ++i) { 23 | images[i].ondragstart = (e) => { e.preventDefault(); return false; }; 24 | } 25 | } 26 | 27 | _selectionCirlce() { 28 | const { scale } = this.props.node.display; 29 | const r = ds.circleRadius * scale; 30 | const bgColor = ds.selectColor; 31 | const bgOpacity = 0.5; 32 | const bgRadius = r + (ds.selectionRadiusDiff * scale); 33 | return ; 34 | } 35 | 36 | _bgCircle() { 37 | const { scale, status } = this.props.node.display; 38 | const r = ds.circleRadius * scale; 39 | const bgColor = ds.bgColor[status]; 40 | const bgOpacity = ds.bgOpacity[status]; 41 | const bgRadius = r + (ds.bgRadiusDiff * scale); 42 | return ; 43 | } 44 | 45 | _circle() { 46 | const n = this.props.node; 47 | const { scale, status, color, image } = n.display; 48 | const r = ds.circleRadius * scale; 49 | const clipId = `image-clip-${n.id}`; 50 | const clipPath = `url(#${clipId})`; 51 | const imageWidth = r * ds.imageScale; 52 | const imageOpacity = ds.imageOpacity[status]; 53 | const innerHTML = { __html: 54 | ` 55 | 56 | 57 | 67 | ` 68 | }; 69 | 70 | return image ? 71 | : 72 | 77 | ; 78 | } 79 | } -------------------------------------------------------------------------------- /app/components/NodeLabel.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import ds from '../NodeDisplaySettings'; 5 | 6 | export default class NodeLabel extends Component { 7 | render() { 8 | let n = this.props.node; 9 | let { name, url, scale, status } = n.display; 10 | let r = ds.circleRadius * scale; 11 | let textOffsetY = ds.textMarginTop + r; 12 | let textLines = this._textLines(name); 13 | 14 | let tspans = textLines.map((line, i) => 15 | 22 | {line} 23 | 24 | ); 25 | 26 | let rects = textLines.map((line, i) => { 27 | let width = line.length * 8; 28 | let height = ds.lineHeight; 29 | let y = r + 4 + (i * ds.lineHeight); 30 | return ; 41 | }); 42 | 43 | return ( 44 | 45 | {rects} 46 | 47 | 48 | { tspans } 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | componentDidMount() { 56 | this._setRectWidths(); 57 | } 58 | 59 | componentDidUpdate(prevProps) { 60 | if (prevProps.node.display.name !== this.props.node.display.name) { 61 | this._setRectWidths(); 62 | } 63 | } 64 | 65 | _setRectWidths() { 66 | let element = ReactDOM.findDOMNode(this); 67 | let texts = element.querySelectorAll(".nodeLabelText"); 68 | let rects = element.querySelectorAll(".nodeLabelRect"); 69 | 70 | for (var i = 0; i < rects.length; i++) { 71 | let textWidth = texts[i].getComputedTextLength(); 72 | let width = textWidth + 10; 73 | rects[i].setAttribute("width", width); 74 | rects[i].setAttribute("x", -width/2); 75 | } 76 | } 77 | 78 | _textLines(text){ 79 | const maxWidth = text.length > 40 ? 25 : 18; 80 | let words = text.trim().split(/\s+/g), 81 | word, 82 | lines = [], 83 | lineNumber = 1, 84 | line = "", 85 | lineWords = []; 86 | 87 | while (word = words.shift()) { 88 | lineWords.push(word); 89 | line = lineWords.join(" "); 90 | 91 | if (line.length > maxWidth) { 92 | lineWords.pop(); 93 | line = lineWords.join(" "); 94 | lines.push(line); 95 | lineNumber += 1; 96 | lineWords = [word]; 97 | } 98 | } 99 | 100 | if (line = lineWords.join(" ")) { 101 | if (line.length < 4 && lines.length > 0) { 102 | lines.push(lines.pop() + " " + line); 103 | } else { 104 | lines.push(line); 105 | } 106 | } 107 | 108 | return lines; 109 | } 110 | } -------------------------------------------------------------------------------- /app/components/SaveButton.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class SaveButton extends Component { 5 | 6 | render() { 7 | return ( 8 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/SettingsButton.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class SettingsButton extends Component { 5 | 6 | render() { 7 | return ( 8 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/UndoButtons.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class UndoButtons extends Component { 5 | 6 | render() { 7 | return ( 8 |
    9 | 10 | 11 |
    12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /app/components/UpdateCaptionForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import { HotKeys } from 'react-hotkeys'; 5 | import merge from 'lodash/merge'; 6 | 7 | export default class UpdateCaptionForm extends BaseComponent { 8 | 9 | render() { 10 | let { display } = this.props.data; 11 | 12 | const scales = [ 13 | [null, "Scale"], 14 | [1, "1x"], 15 | [1.25, "1.25x"], 16 | [1.5, "1.5x"], 17 | [2, "2x"], 18 | [2.5, "2.5x"], 19 | [3, "3x"], 20 | [4, "4x"], 21 | [5, "5x"] 22 | ]; 23 | 24 | const keyMap = { 25 | 'esc': 'esc', 26 | 'enter': 'enter' 27 | }; 28 | 29 | const keyHandlers = { 30 | 'esc': () => this.props.deselect(), 31 | 'enter': () => this.props.deselect() 32 | }; 33 | 34 | return ( 35 | 36 |
    37 | 38 | this.apply()} /> 45 |   54 | 55 |
    56 | ); 57 | } 58 | 59 | 60 | apply() { 61 | if (this.props.data) { 62 | let text = this.refs.text.value.trim(); 63 | let scale = parseFloat(this.refs.scale.value); 64 | this.props.updateCaption(this.props.data.id, { display: { text, scale } }); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/components/UpdateEdgeForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import EdgeDropdown from './EdgeDropdown'; 5 | import { HotKeys } from 'react-hotkeys'; 6 | import values from 'lodash/values'; 7 | import sortBy from 'lodash/sortBy'; 8 | import merge from 'lodash/merge'; 9 | 10 | export default class UpdateEdgeForm extends BaseComponent { 11 | 12 | render() { 13 | let { display } = this.props.data; 14 | const keyMap = { 15 | 'esc': 'esc' 16 | }; 17 | 18 | const keyHandlers = { 19 | 'esc': () => this.props.deselect() 20 | }; 21 | 22 | 23 | const scales = [ 24 | [null, "Scale"], 25 | [1, "1x"], 26 | [1.5, "1.5x"], 27 | [2, "2x"], 28 | [3, "3x"] 29 | ]; 30 | 31 | return ( 32 |
    33 | 34 |
    35 | this.apply()} /> 42 |   51 |
    52 |
    53 | this.apply()} /> 61 |
    62 | this.apply(arrow, dash)}/> 70 |
    71 |
    72 | ); 73 | } 74 | 75 | apply(whichArrow, isDashed) { 76 | let label = this.refs.label.value; 77 | let arrow = whichArrow || this.refs.edgeDropdown.props.whichArrow; 78 | let dash = isDashed; 79 | let scale = parseFloat(this.refs.scale.value); 80 | let url = this.refs.url.value.trim(); 81 | this.props.updateEdge(this.props.data.id, { display: { label, arrow, dash, scale, url } }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/components/UpdateNodeForm.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import BaseComponent from './BaseComponent'; 4 | import ChangeColorInput from './ChangeColorInput'; 5 | import { HotKeys } from 'react-hotkeys'; 6 | import merge from 'lodash/merge'; 7 | import ds from '../NodeDisplaySettings'; 8 | 9 | export default class UpdateNodeForm extends BaseComponent { 10 | 11 | render() { 12 | let { display } = this.props.data; 13 | 14 | if (!display.color) { 15 | display.color = ds.circleColor[display.status]; 16 | } 17 | 18 | const keyMap = { 19 | 'esc': 'esc' 20 | }; 21 | 22 | const keyHandlers = { 23 | 'esc': () => this.props.deselect() 24 | }; 25 | 26 | const scales = [ 27 | [1, "1x"], 28 | [1.5, "1.5x"], 29 | [2, "2x"], 30 | [3, "3x"] 31 | ]; 32 | 33 | return ( 34 |
    35 | 36 |
    37 | this.apply()} /> 45 |   46 | this.apply()} /> 54 |   55 | this.apply(color)} /> 60 |   61 | 71 |
    72 |
    73 | this.apply()} /> 82 |
    83 |
    84 |
    85 | ); 86 | } 87 | 88 | apply(newColor) { 89 | if (this.props.data) { 90 | let name = this.refs.name.value; 91 | let image = this.refs.image.value.trim(); 92 | let color = newColor || this.refs.color.state.color; 93 | let scale = parseFloat(this.refs.scale.value); 94 | let url = this.refs.url.value.trim(); 95 | this.props.updateNode(this.props.data.id, { display: { name, image, color, scale, url } }); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/components/ZoomButtons.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | export default class ZoomButtons extends Component { 5 | 6 | render() { 7 | return ( 8 |
    9 | 10 | 11 |
    12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /app/components/__tests__/AnnotationsTracker-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import AnnotationsTracker from '../AnnotationsTracker'; 4 | 5 | describe('AnnotationsTracker', () => { 6 | 7 | it('has correct number of circles', () => { 8 | expect(shallow().find('.tracker-circle').length).toEqual(10); 9 | }); 10 | 11 | it('has only one selected circle', () => { 12 | expect(shallow().find('.tracker-circle-selected').length).toEqual(1); 13 | }); 14 | 15 | it('highlights correct circle', () => { 16 | let annotationsTracker = shallow() 17 | expect(annotationsTracker.find('#annotationsTracker').childAt(2).hasClass('tracker-circle-selected')).toEqual(true); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/__tests__/Caption-test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from "enzyme"; 3 | import Caption from '../Caption'; 4 | 5 | describe("Caption Component", () => { 6 | 7 | const data = { id: 1, display: { text: "Here's an interesting fact!" } }; 8 | 9 | it("should have an svg transform", () => { 10 | let wrapper = shallow( 11 | 12 | ); 13 | let element = wrapper.find("g.caption"); 14 | let { x, y } = data.display; 15 | 16 | expect(element.props().transform).toBe(`translate(${x}, ${y})`); 17 | }); 18 | 19 | it("should display text", () => { 20 | let wrapper = shallow( 21 | 22 | ); 23 | let text = wrapper.find("text"); 24 | 25 | expect(text.text()).toBe(data.display.text); 26 | }); 27 | 28 | it("should call click callback if clicked", () => { 29 | let clickCaption = jest.genMockFunction(); 30 | let wrapper = shallow( 31 | 32 | ); 33 | let element = wrapper.find("g.caption"); 34 | element.simulate("click"); 35 | 36 | expect(clickCaption.mock.calls[0][0]).toBe(data.id); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /app/components/__tests__/ChangeColorInput-test.jsx: -------------------------------------------------------------------------------- 1 | jest.disableAutomock(); 2 | 3 | import React from "react"; 4 | import { shallow } from "enzyme"; 5 | 6 | import ChangeColorInput from "../ChangeColorInput"; 7 | import ds from "../../NodeDisplaySettings"; 8 | import { CompactPicker } from "react-color"; 9 | 10 | describe("ChangeColorInput", () => { 11 | let wrapper; 12 | let onChange; 13 | 14 | beforeEach(() => { 15 | onChange = jest.genMockFunction(); 16 | wrapper = shallow( 17 | 21 | ); 22 | }); 23 | 24 | it("shows a swatch with the currently selected color", () => { 25 | let swatch = wrapper.find(".nodeColorInputSwatch"); 26 | expect(swatch.props().style).toEqual({ background: "#abc" }); 27 | }); 28 | 29 | it("shows a clear button", () => { 30 | let clearer = wrapper.find(".nodeColorInputClearer"); 31 | let glyph = wrapper.find(".glyphicon-remove-sign"); 32 | expect(clearer.length).toBe(1); 33 | expect(glyph.length).toBe(1); 34 | }); 35 | 36 | it("clears color and hides picker when clear button is clicked", () => { 37 | wrapper.setState({ displayColorPicker: true }); 38 | let clearer = wrapper.find(".nodeColorInputClearer"); 39 | clearer.simulate("click"); 40 | expect(wrapper.state().color).toBe(ds.circleColor["highlighted"]); 41 | }); 42 | 43 | it ("shows and closes color picker when swatch is clicked", () => { 44 | let swatch = wrapper.find(".nodeColorInputSwatch"); 45 | swatch.simulate("click"); 46 | 47 | let picker = wrapper.find(CompactPicker); 48 | expect(picker.length).toBe(1); 49 | expect(picker.props().color).toBe("#abc"); 50 | 51 | swatch.simulate("click"); 52 | picker = wrapper.find(CompactPicker); 53 | expect(picker.length).toBe(0); 54 | }); 55 | 56 | it("updates color when receiving new props", () => { 57 | wrapper.setProps({ value: "#def" }); 58 | expect(wrapper.state().color).toBe("#def"); 59 | }); 60 | 61 | it("calls onChange when CompactPicker changes color", () => { 62 | wrapper.setState({ displayColorPicker: true }); 63 | let picker = wrapper.find(CompactPicker); 64 | let pickerOnChange = picker.props().onChange; 65 | pickerOnChange({ hex: "#def" }); 66 | 67 | expect(onChange.mock.calls.length).toBe(1); 68 | expect(onChange.mock.calls[0][0]).toBe("#def"); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /app/components/__tests__/DeleteSelectedButton-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import DeleteSelectedButton from '../DeleteSelectedButton'; 4 | 5 | describe('DeleteSelectedButton', function(){ 6 | let doDeleteMock = jest.fn(); 7 | let wrapper = shallow( 8 | ); 11 | 12 | it('sets correct class names on div', () => { 13 | expect(wrapper.hasClass('editForm')).toEqual(true); 14 | expect(wrapper.hasClass('form-inline')).toEqual(true); 15 | expect(wrapper.hasClass('nodeDelete')).toEqual(true); 16 | expect(wrapper.hasClass('edgeDelete')).toEqual(false); 17 | }); 18 | 19 | it('clicking on button triggers doDelete', () => { 20 | wrapper.find('button').simulate('click'); 21 | expect(doDeleteMock.mock.calls.length).toEqual(1); 22 | }); 23 | 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /app/components/__tests__/Edge-test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import Edge from '../Edge'; 5 | import merge from 'lodash/merge'; 6 | 7 | describe("Edge Component", () => { 8 | 9 | const data = { 10 | id: 93362, 11 | node1_id: 34963, 12 | node2_id: 15957, 13 | display: { 14 | label: "former chair", 15 | cx: null, 16 | cy: null, 17 | scale: 1, 18 | arrow: true, 19 | dash: null, 20 | status: "normal", 21 | url: "//littlesis.org/relationship/view/id/93362", 22 | x1: 569.3762350992062, 23 | y1: -255.7819366545154, 24 | x2: -56.66444213745007, 25 | y2: -402.53869654584355, 26 | s1: 1, 27 | s2: 1.5 28 | } 29 | }; 30 | 31 | it("should render a curve with a control point", () => { 32 | let getArrow = jest.genMockFunction(); 33 | let edge = TestUtils.renderIntoDocument( 34 | 35 | ); 36 | let element = ReactDOM.findDOMNode(edge); 37 | let curve = element.querySelector(".edge-line"); 38 | 39 | expect(curve.getAttribute("d")).toMatch(/M [-\d.]+, [-\d.]+ Q [-\d.]+, [-\d.]+, [-\d.]+, [-\d.]+/); 40 | }); 41 | 42 | it("should render a label", () => { 43 | let getArrow = jest.genMockFunction(); 44 | let edge = TestUtils.renderIntoDocument( 45 | 46 | ); 47 | let element = ReactDOM.findDOMNode(edge); 48 | let label = element.querySelector("text"); 49 | 50 | expect(label.textContent).toBe(data.display.label); 51 | }); 52 | 53 | /*not sure of most effective way of testing arrow rendering*/ 54 | it("should render an arrow", () => { 55 | let getArrow = jest.genMockFunction().mockImplementation(function () { 56 | data["display"]["arrow"] = "1->2"; 57 | return data; 58 | }); 59 | let updateArrow = jest.genMockFunction(); 60 | 61 | let edge = TestUtils.renderIntoDocument( 62 | 63 | ); 64 | let element = ReactDOM.findDOMNode(edge); 65 | let curve = element.querySelector(".edge-line"); 66 | let hasMarker = curve.getAttribute("marker-start") || curve.getAttribute("marker-end"); 67 | 68 | expect(hasMarker).toBeTruthy(); 69 | }); 70 | 71 | it("should call click callback if clicked", () => { 72 | let getArrow = jest.genMockFunction(); 73 | let updateArrow = jest.genMockFunction(); 74 | let clickEdge = jest.genMockFunction(); 75 | let edge = TestUtils.renderIntoDocument( 76 | 77 | ); 78 | let element = ReactDOM.findDOMNode(edge); 79 | let select = element.querySelector(".edgeSelect"); 80 | 81 | TestUtils.Simulate.click(select); 82 | expect(clickEdge.mock.calls[0][0]).toBe(data.id); 83 | }); 84 | 85 | it('should calculate correct arrow attribute for markerStart', () => { 86 | let edge = TestUtils.renderIntoDocument( ); 87 | expect(edge._markerStartArrow('1->2', false)).toEqual(''); 88 | expect(edge._markerStartArrow('1->2', true)).toEqual("url(#marker2)"); 89 | expect(edge._markerStartArrow('2->1', false)).toEqual("url(#marker2)"); 90 | expect(edge._markerStartArrow('2->1', true)).toEqual(""); 91 | expect(edge._markerStartArrow('both', true)).toEqual("url(#marker2)"); 92 | expect(edge._markerStartArrow('both', false)).toEqual("url(#marker2)"); 93 | }); 94 | 95 | it('should calculate correct arrow attribute for markerEnd', () => { 96 | let edge = TestUtils.renderIntoDocument( ); 97 | expect(edge._markerEndArrow('1->2', false)).toEqual('url(#marker1)'); 98 | expect(edge._markerEndArrow('1->2', true)).toEqual(''); 99 | expect(edge._markerEndArrow('2->1', false)).toEqual(''); 100 | expect(edge._markerEndArrow('2->1', true)).toEqual('url(#marker1)'); 101 | expect(edge._markerEndArrow('both', true)).toEqual("url(#marker1)"); 102 | expect(edge._markerEndArrow('both', false)).toEqual("url(#marker1)"); 103 | }); 104 | 105 | }); 106 | -------------------------------------------------------------------------------- /app/components/__tests__/EdgeArrowSelector-test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from "enzyme"; 3 | import EdgeArrowSelector from '../EdgeArrowSelector'; 4 | import { newArrowState } from '../../helpers'; 5 | 6 | 7 | const element = () => shallow(); 8 | 9 | describe('EdgeArrowSelector', () => { 10 | 11 | it('has dropDownHolder', () => expect(element().find('.dropdownHolder').length).toEqual(1)); 12 | it('has selectedEdgeDisplay', () => expect(element().find('.selectedEdgeDisplay').length).toEqual(1)); 13 | it('sets correct class name for left-left', () =>{ 14 | let e = shallow(); 15 | expect(e.find('.svgDropdownLeftArrow').length).toEqual(1); 16 | }); 17 | it('sets correct class name for left-right', () =>{ 18 | let e = shallow(); 19 | expect(e.find('.svgDropdownLeftArrow').length).toEqual(0); 20 | }); 21 | it('sets correct class name for right-right', () =>{ 22 | let e = shallow(); 23 | expect(e.find('.svgDropdownRightArrow').length).toEqual(1); 24 | }); 25 | it('sets correct class name for right-left', () =>{ 26 | let e = shallow(); 27 | expect(e.find('.svgDropdownRightArrow').length).toEqual(0); 28 | }); 29 | it('sets correct class name for right-both', () =>{ 30 | let e = shallow(); 31 | expect(e.find('.svgDropdownRightArrow').length).toEqual(1); 32 | }); 33 | it('sets correct class name for left-both', () =>{ 34 | let e = shallow(); 35 | expect(e.find('.svgDropdownLeftArrow').length).toEqual(1); 36 | }); 37 | 38 | it('updates state and re-renders when clicked on', () => { 39 | let e = element(); 40 | expect(e.state().isOpen).toBe(false); 41 | expect(e.find('.edgeDropdownOptions').length).toEqual(0); 42 | e.find('.selectedEdgeDisplay').simulate('click'); 43 | expect(e.state().isOpen).toBe(true); 44 | expect(e.find('.edgeDropdownOptions').length).toEqual(1); 45 | }); 46 | 47 | it('updates Edge with new arrow setting', () => { 48 | let updateEdgeMock = jest.fn(); 49 | let e = shallow(); 50 | e.find('.selectedEdgeDisplay').simulate('click'); 51 | e.find('.svgDropdownLeftNoArrow').simulate('click'); 52 | expect(e.state().isOpen).toBe(false); 53 | expect(updateEdgeMock.mock.calls.length).toEqual(1); 54 | expect(updateEdgeMock.mock.calls[0]).toEqual(['x', {display: {arrow: false} } ]); 55 | }); 56 | 57 | }); 58 | 59 | 60 | describe('newArrowState()', ()=>{ 61 | 62 | it('when current state is "1->2"', () =>{ 63 | expect(newArrowState('1->2', 'left', false)).toEqual('1->2'); 64 | expect(newArrowState('1->2', 'left', true)).toEqual('both'); 65 | expect(newArrowState('1->2', 'right', false)).toEqual(false); 66 | expect(newArrowState('1->2', 'right', true)).toEqual('1->2'); 67 | }); 68 | 69 | it('when current state is "2->1"', ()=> { 70 | expect(newArrowState('2->1', 'left', false)).toEqual(false); 71 | expect(newArrowState('2->1', 'left', true)).toEqual('2->1'); 72 | expect(newArrowState('2->1', 'right', false)).toEqual('2->1'); 73 | expect(newArrowState('2->1', 'right', true)).toEqual('both'); 74 | }); 75 | 76 | it('when current state is both', ()=> { 77 | expect(newArrowState('both', 'left', false)).toEqual('1->2'); 78 | expect(newArrowState('both', 'left', true)).toEqual('both'); 79 | expect(newArrowState('both', 'right', false)).toEqual('2->1'); 80 | expect(newArrowState('both', 'right', true)).toEqual('both'); 81 | }); 82 | 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /app/components/__tests__/EdgeDashSelector-test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from "enzyme"; 3 | import EdgeDashSelector from '../EdgeDashSelector'; 4 | 5 | describe('EdgeDashSelector', () => { 6 | 7 | it('sets svgDropdownDashed if dashed', () => { 8 | let wrapper = shallow(); 9 | expect(wrapper.find('.svgDropdownDashed').length).toEqual(1); 10 | }); 11 | 12 | it('does not set svgDropdownDashed if not dashed', () => { 13 | let wrapper = shallow(); 14 | expect(wrapper.find('.svgDropdownDashed').length).toEqual(0); 15 | }); 16 | 17 | describe('click on menu', () => { 18 | let wrapper = shallow(); 19 | it('updates state and re-renders', ()=>{ 20 | expect(wrapper.state().isOpen).toBe(false); 21 | expect(wrapper.find('.edgeDropdownOptions').length).toEqual(0); 22 | wrapper.find('.selectedEdgeDisplay').childAt(0).simulate('click'); 23 | expect(wrapper.state().isOpen).toBe(true); 24 | expect(wrapper.find('.edgeDropdownOptions').length).toEqual(1); 25 | }); 26 | }); 27 | 28 | describe('select an option', ()=>{ 29 | it('calls updateEdge with correct arg - undashed', () =>{ 30 | let updateEdgeMock = jest.fn(); 31 | let wrapper = shallow(); 32 | wrapper.find('.selectedEdgeDisplay').childAt(0).simulate('click'); 33 | wrapper.find('.svgDropdownUndashed').simulate('click'); 34 | expect(updateEdgeMock.mock.calls.length).toEqual(1); 35 | expect(updateEdgeMock.mock.calls[0]).toEqual(['x', {display: {dash: false} } ]); 36 | expect(wrapper.state().isOpen).toBe(false); 37 | 38 | }); 39 | it('calls updateEdge with correct arg - dashed', () =>{ 40 | let updateEdgeMock = jest.fn(); 41 | let wrapper = shallow(); 42 | wrapper.find('.selectedEdgeDisplay').childAt(0).simulate('click'); 43 | wrapper.find('.svgDropdownDashed').simulate('click'); 44 | expect(updateEdgeMock.mock.calls.length).toEqual(1); 45 | expect(updateEdgeMock.mock.calls[0]).toEqual(['x', {display: {dash: true}} ]); 46 | expect(wrapper.state().isOpen).toBe(false); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /app/components/__tests__/EdgeDropdown-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from 'react-dom'; 3 | import { shallow } from "enzyme"; 4 | import EdgeDropdown from "../EdgeDropdown"; 5 | import EdgeArrowSelector from '../EdgeArrowSelector'; 6 | import EdgeDashSelector from '../EdgeDashSelector'; 7 | 8 | describe("EdgeDropdown", () => { 9 | 10 | const graph = { 11 | "nodes": { 12 | "1": { 13 | "id": "1", 14 | "display": { 15 | "x": 0, 16 | "y": 0, 17 | "scale": 1, 18 | "name": "n1" 19 | } 20 | }, 21 | "2": { 22 | "id": "2", 23 | "display": { 24 | "x": 0, 25 | "y": 0, 26 | "scale": 1, 27 | "name": "n2" 28 | } 29 | } 30 | }, 31 | "edges": { 32 | "123": { 33 | "id": "123", 34 | "display": { 35 | "scale": 1, 36 | "arrow": true, 37 | "label": "edge!", 38 | "x1": 0, 39 | "y1": 0, 40 | "x2": 122.87581699346406, 41 | "y2": -5.228758169934641, 42 | "s1": 1, 43 | "s2": 1 44 | }, 45 | "node1_id": "1", 46 | "node2_id": "2" 47 | } 48 | } 49 | }; 50 | 51 | const getGraph = () => graph; 52 | 53 | describe('layout', () => { 54 | let wrapper = shallow(); 60 | 61 | it('has EdgeDashSelector', () => expect(wrapper.find(EdgeDashSelector).length).toEqual(1)); 62 | it('has two EdgeArrowSelector', () => expect(wrapper.find(EdgeArrowSelector).length).toEqual(2)); 63 | it('has 1 strokeDropdown div', () => expect(wrapper.find('.strokeDropdowns').length).toEqual(1)); 64 | it('has 2 arrow-node-name divs', () => expect(wrapper.find('.arrow-node-name').length).toEqual(2)); 65 | it('contains two svgs', ()=> expect(wrapper.find('svg').length).toEqual(2)); 66 | it('contains with node names', ()=>{ 67 | expect(wrapper.find('text').map( node => node.text())).toEqual(['n1', 'n2']); 68 | }); 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /app/components/__tests__/EmbeddedGraphAnnotation-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | 4 | import EmbeddedGraphAnnotation from "../EmbeddedGraphAnnotation"; 5 | import { Scrollbars } from 'react-custom-scrollbars'; 6 | 7 | 8 | describe('EmbeddedGraphAnnotation', () => { 9 | let wrapper; 10 | let annotation = { 11 | header: "Header", 12 | text: "

    A modest amount of annotation text." 13 | }; 14 | let embedded = {annotationHeight: 100, linkUrl: null, linkText: null}; 15 | 16 | describe('layout', () => { 17 | beforeEach( () => wrapper = shallow() ) 18 | it('shows title', () => expect(wrapper.find('strong').text()).toEqual('Header') ); 19 | it('shows text', () => { 20 | expect(wrapper.find('#oligrapherEmbeddedGraphAnnotationText').render().text()).toEqual("A modest amount of annotation text."); 21 | }) 22 | it('has Scrollbars', () => expect(wrapper.find(Scrollbars).length).toEqual(1)); 23 | }) 24 | 25 | describe('tracker option', () => { 26 | it('sets height correctly if there is a tracker', () => { 27 | let element = shallow(); 28 | expect(element.find(Scrollbars).prop('style').height).toEqual(47); 29 | }); 30 | it('sets height correctly if there is not a tracker', () => { 31 | let element = shallow(); 32 | expect(element.find(Scrollbars).prop('style').height).toEqual(85); 33 | }); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /app/components/__tests__/EmbeddedGraphAnnotations-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import noop from 'lodash/noop'; 4 | import merge from 'lodash/merge'; 5 | import EmbeddedGraphAnnotations from '../EmbeddedGraphAnnotations'; 6 | import EmbeddedNavBar from '../EmbeddedNavBar'; 7 | import EmbeddedGraphAnnotation from '../EmbeddedGraphAnnotation'; 8 | 9 | 10 | describe('EmbeddedGraphAnnotations', () => { 11 | const embedded = {annotationSize: '100px'}; 12 | 13 | it('should have EmbeddedNavbar', () => { 14 | let wrapper = shallow(); 15 | expect(wrapper.find(EmbeddedNavBar).length).toEqual(1); 16 | }); 17 | 18 | it('should have two columns if there is a tracker', () => { 19 | let wrapper = shallow(); 20 | expect(wrapper.find('div.col-sm-12').length).toEqual(2); 21 | }); 22 | 23 | it('should not have EmbeddedNavbar if annotation count is 1', () => { 24 | let wrapper = shallow(); 25 | expect(wrapper.find(EmbeddedNavBar).length).toEqual(0); 26 | }); 27 | 28 | it('should have EmbeddedGraphAnnotation', () => { 29 | let wrapper = shallow(); 30 | expect(wrapper.find(EmbeddedGraphAnnotation).length).toEqual(1); 31 | }); 32 | 33 | it('should have img if provided logoUrl', () => { 34 | let e = merge(embedded, {logoUrl: '/logo.png'}) 35 | let wrapper = shallow(); 36 | expect(wrapper.find('img').length).toEqual(1); 37 | }); 38 | 39 | it('should not have img if there is no provided logoUrl', () => { 40 | let wrapper = shallow(); 41 | expect(wrapper.find('img').length).toEqual(1); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /app/components/__tests__/EmbeddedNavBar-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import noop from 'lodash/noop'; 4 | import EmbeddedNavBar from '../EmbeddedNavBar'; 5 | import EmbeddedNavButtons from '../EmbeddedNavButtons'; 6 | import AnnotationsTracker from '../AnnotationsTracker'; 7 | 8 | 9 | describe('EmbeddedNavBar', () => { 10 | it('should have EmbeddedNavButtons', () => { 11 | let wrapper = shallow() 12 | expect(wrapper.find(EmbeddedNavButtons).length).toEqual(1); 13 | }); 14 | 15 | it('should have AnnotationsTracker', () => { 16 | let wrapper = shallow() 17 | expect(wrapper.find(EmbeddedNavButtons).length).toEqual(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/components/__tests__/EmbeddedNavButtons-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import sinon from 'sinon'; 4 | import noop from 'lodash/noop'; 5 | import EmbeddedNavButtons from '../EmbeddedNavButtons'; 6 | 7 | 8 | describe('EmbeddedNavButtons', () => { 9 | it('should have 2 buttons', () => { 10 | let wrapper = shallow(); 11 | expect(wrapper.find('button').length).toEqual(2); 12 | expect(wrapper.find('button.btn-annotation-next').prop('disabled')).toEqual(false); 13 | expect(wrapper.find('button.btn-annotation-back').prop('disabled')).toEqual(false); 14 | }); 15 | 16 | it('has next button and back button is disabled when current index is 0', () => { 17 | let wrapper = shallow(); 18 | expect(wrapper.find('button.btn-annotation-back').prop('disabled')).toEqual(true); 19 | expect(wrapper.find('button.btn-annotation-next').prop('disabled')).toEqual(false); 20 | expect(wrapper.find('button.btn-annotation-next').length).toEqual(1); 21 | }); 22 | 23 | it('has back button and next button is disabled when current index is the last', () => { 24 | let wrapper = shallow(); 25 | expect(wrapper.find('button').length).toEqual(2); 26 | expect(wrapper.find('button.btn-annotation-back').prop('disabled')).toEqual(false); 27 | expect(wrapper.find('button.btn-annotation-next').prop('disabled')).toEqual(true); 28 | }); 29 | 30 | describe('clicking', () => { 31 | it('triggers prevClick', () => { 32 | let spy = sinon.spy(); 33 | let wrapper = shallow(); 34 | wrapper.find('button.btn-annotation-back').simulate('click'); 35 | expect(spy.calledOnce).toEqual(true); 36 | }); 37 | 38 | it('triggers nextClick', () => { 39 | let spy = sinon.spy(); 40 | let wrapper = shallow(); 41 | wrapper.find('button.btn-annotation-next').simulate('click'); 42 | expect(spy.calledOnce).toEqual(true); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /app/components/__tests__/Graph-test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from "enzyme"; 3 | import Graph from '../Graph'; 4 | import Node from "../Node"; 5 | import Edge from "../Edge"; 6 | import Caption from "../Caption"; 7 | 8 | describe("Graph Component", () => { 9 | 10 | const data = { id: "someid", nodes: { 1: { id: 1, display: { name: "Node 1", scale: 1, status: "normal", x: 260.81190983749696, y: 78.09522392060452 } }, 2: { id: 2, display: { name: "Node 2", scale: 1, status: "normal", x: 1.3981085859366804, y: -5.974907363126558 } }, 3: { id: 3, display: { name: "Node 3", scale: 1, status: "normal", x: -258.01571204120563, y: -90.04497885992393 } } }, edges: { 1: { id: 1, node1_id: 1, node2_id: 2, display: { label: "Edge 1", cx: null, cy: null, scale: 1, arrow: false, status: "normal", x1: 260.81190983749696, y1: 78.09522392060452, x2: 1.3981085859366804, y2: -5.974907363126558, s1: 1, s2: 1 } }, 2: { id: 2, node1_id: 2, node2_id: 3, display: { label: "Edge 1", cx: null, cy: null, scale: 1, arrow: false, status: "normal", x1: 1.3981085859366804, y1: -5.974907363126558, x2: -258.01571204120563, y2: -90.04497885992393, s1: 1, s2: 1 } } }, captions: { 1: { id: 1, display: { text: "Caption 1", x: -458.01571204120563, y: -90.04497885992393 } } } }; 11 | 12 | it("sould render nodes", () => { 13 | let wrapper = shallow( 14 | 15 | ); 16 | let nodes = wrapper.find(Node); 17 | 18 | expect(nodes.length).toBe(Object.keys(data.nodes).length); 19 | }); 20 | 21 | it("sould render edges", () => { 22 | let wrapper = shallow( 23 | 24 | ); 25 | let edges = wrapper.find(Edge); 26 | 27 | expect(edges.length).toBe(Object.keys(data.edges).length); 28 | }); 29 | 30 | it("sould render captions", () => { 31 | let wrapper = shallow( 32 | 33 | ); 34 | let captions = wrapper.find(Caption); 35 | 36 | expect(captions.length).toBe(Object.keys(data.captions).length); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /app/components/__tests__/GraphAnnotation-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import GraphAnnotation from "../GraphAnnotation"; 4 | 5 | describe("GraphAnnotation", () => { 6 | let wrapper; 7 | let annotation = { 8 | header: "Header", 9 | text: "

    A modest amount of annotation text." 10 | }; 11 | 12 | beforeEach(() => { 13 | wrapper = shallow( 14 | 15 | ); 16 | }); 17 | 18 | it("shows header", () => { 19 | let header = wrapper.find("h2"); 20 | expect(header.text()).toBe(annotation.header); 21 | }); 22 | 23 | it("shows text", () => { 24 | let text = wrapper.find("#oligrapherGraphAnnotationText"); 25 | expect(text.props().dangerouslySetInnerHTML.__html).toBe(annotation.text); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/components/__tests__/GraphAnnotationForm-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow, mount } from "enzyme"; 3 | import GraphAnnotationForm from "../GraphAnnotationForm"; 4 | import Editor from "react-medium-editor"; 5 | 6 | describe("GraphAnnotationForm", () => { 7 | let wrapper; 8 | let annotation = { 9 | header: "Header", 10 | text: "This is some text to test." 11 | }; 12 | let remove; 13 | let update; 14 | 15 | beforeEach(() => { 16 | remove = jest.genMockFunction(); 17 | update = jest.genMockFunction(); 18 | wrapper = mount( 19 | 23 | ); 24 | }); 25 | 26 | describe("rendering", () => { 27 | it("shows header input", () => { 28 | let header = wrapper.ref("header"); 29 | expect(header.props().value).toBe(annotation.header); 30 | }); 31 | 32 | it("shows text editor", () => { 33 | let text = wrapper.find(Editor); 34 | expect(text.props().text).toBe(annotation.text); 35 | }); 36 | 37 | it("shows delete button", () => { 38 | let button = wrapper.find("button"); 39 | expect(button.text()).toBe("Remove"); 40 | }); 41 | }); 42 | 43 | describe("behavior", () => { 44 | it("updates annotation with new header", () => { 45 | let header = wrapper.ref("header"); 46 | header.get(0).value = "New Header"; 47 | header.simulate("change"); 48 | 49 | expect(update.mock.calls.length).toBe(1); 50 | expect(update.mock.calls[0][0].header).toBe("New Header"); 51 | }); 52 | 53 | it("updates annotation with new text", () => { 54 | let editor = wrapper.ref("text"); 55 | let newText = "This is some EDITED text to test."; 56 | editor.get(0).props.onChange(newText); 57 | 58 | expect(update.mock.calls.length).toBe(1); 59 | expect(update.mock.calls[0][0].text).toBe(newText); 60 | }); 61 | 62 | it("removes annotation when delete button is clicked", () => { 63 | let button = wrapper.find("button"); 64 | spyOn(window, 'confirm').and.returnValue(true); 65 | button.simulate("click"); 66 | 67 | expect(remove.mock.calls.length).toBe(1); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /app/components/__tests__/GraphAnnotationList-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow, mount } from "enzyme"; 3 | 4 | import GraphAnnotationList from "../GraphAnnotationList"; 5 | 6 | describe("GraphAnnotationList", () => { 7 | let wrapper; 8 | let annotations = [ 9 | { id: 1, header: "Annotation 1", text: "

    annotation text 1

    " }, 10 | { id: 2, header: "Annotation 2", text: "

    annotation text 2

    " }, 11 | { id: 3, header: "", text: "

    annotation text 3

    " }, 12 | { id: 4, header: "Annotation 4", text: "

    annotation text 4

    " } 13 | ]; 14 | 15 | describe("rendering", () => { 16 | let currentIndex; 17 | 18 | beforeEach(() => { 19 | currentIndex = 3; 20 | wrapper = shallow( 21 | 29 | ); 30 | }); 31 | 32 | it("shows annotations", () => { 33 | let items = wrapper.find("li"); 34 | items.forEach((item, i) => { 35 | expect(item.props()["data-id"]).toBe(i); 36 | expect(item.props().draggable).toBe(true); 37 | expect(item.text()).toBe(annotations[i].header || "Untitled Annotation"); 38 | 39 | if (i == currentIndex) { 40 | expect(item.props().className).toBe("active"); 41 | } 42 | }); 43 | }); 44 | 45 | it("shows add button", () => { 46 | let button = wrapper.find("button"); 47 | expect(button.text()).toBe("New Annotation"); 48 | }); 49 | }); 50 | 51 | describe("behavior", () => { 52 | let currentIndex = 3; 53 | let create, show, move, hideEditTools; 54 | 55 | beforeEach(() => { 56 | create = jest.genMockFunction(); 57 | show = jest.genMockFunction(); 58 | move = jest.genMockFunction(); 59 | hideEditTools = jest.genMockFunction(); 60 | wrapper = mount( 61 | 69 | ); 70 | }); 71 | 72 | it("shows annotation when clicked", () => { 73 | let items = wrapper.find("li"); 74 | items.forEach(item => { 75 | item.simulate("click", { target: { dataset: { id: item.props()["data-id"] } } }); 76 | }); 77 | 78 | expect(show.mock.calls.length).toBe(items.length); 79 | expect(show.mock.calls.map(call => call[0])).toEqual(annotations.map((a, i) => i)); 80 | expect(hideEditTools.mock.calls.length).toBe(items.length); 81 | }); 82 | 83 | it("creates annotation", () => { 84 | let button = wrapper.find("button"); 85 | button.simulate("click"); 86 | expect(create.mock.calls.length).toBe(1); 87 | }); 88 | 89 | // TODO: test dragging 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /app/components/__tests__/GraphHeader-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow, mount } from "enzyme"; 3 | import merge from 'lodash/merge'; 4 | 5 | import GraphHeader from '../GraphHeader'; 6 | import GraphTitle from '../GraphTitle'; 7 | import GraphTitleForm from '../GraphTitleForm'; 8 | import GraphByLine from '../GraphByLine'; 9 | import GraphLinks from '../GraphLinks'; 10 | 11 | describe('GraphHeader', () => { 12 | 13 | const defaultProps = { 14 | user: { name: 'name', url: 'http://url.com' }, 15 | date: 'right now', 16 | title: 'eyes on the ties', 17 | updateTitle: function(){}, 18 | url: 'oligrapher.info', 19 | isEmbedded: false 20 | } 21 | 22 | let embedded = {headerFontSize: '20px'}; 23 | 24 | const graphHeader = props => shallow(); 25 | const hasComponent = (root, c) => expect(root.find(c).length).toEqual(1); 26 | const hasNoComponent = (root, c) => expect(root.find(c).length).toEqual(0); 27 | 28 | describe('When editor', () => { 29 | it('has GraphTitleForm', () => hasComponent(graphHeader({isEditor: true}), GraphTitleForm)); 30 | it('does not have GraphTitle', () => hasNoComponent(graphHeader({isEditor: true}), GraphTitle)); 31 | }); 32 | 33 | describe('When not editor', () => { 34 | it('does not GraphTitleForm', () => hasNoComponent(graphHeader({isEditor: false}), GraphTitleForm)); 35 | it('has GraphTitle', () => hasComponent(graphHeader({isEditor: false}), GraphTitle)); 36 | }); 37 | 38 | describe('when user and date is provided', () => { 39 | it('has GraphByLine', () => hasComponent(graphHeader({}), GraphByLine)); 40 | }); 41 | 42 | describe('when user and date is provided and isEmbedded is true', () => { 43 | it('does not have GraphByLine', () => { 44 | hasNoComponent(graphHeader({isEmbedded: true, embedded: embedded}), GraphByLine); 45 | }); 46 | }); 47 | 48 | describe('when user or date is not provided', () => { 49 | it('does not have graphByLine', () => { 50 | hasNoComponent(graphHeader({user: null, date: null}), GraphByLine) 51 | }) 52 | }); 53 | 54 | describe('graph links', () => { 55 | it('has Graphlinks', () => { 56 | let gh = shallow(); 57 | hasComponent(gh, GraphLinks); 58 | }); 59 | 60 | it('has no Graph links', () => hasNoComponent(graphHeader({links: null}), GraphLinks)); 61 | 62 | it('does not have graphLinks when embedded is true', () => { 63 | hasNoComponent(graphHeader({links: [], isEmbedded: true}), GraphLinks); 64 | }); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /app/components/__tests__/GraphNavButtons-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow, mount } from "enzyme"; 3 | 4 | import GraphNavButtons from "../GraphNavButtons"; 5 | 6 | describe("GraphNavButtons", () => { 7 | 8 | describe("rendering", () => { 9 | let wrapper; 10 | 11 | beforeEach(() => { 12 | wrapper = mount( 13 | 20 | ); 21 | }); 22 | 23 | it("shows prev button", () => { 24 | let prev = wrapper.find("button").filterWhere(button => button.text() == "Prev"); 25 | expect(prev.length).toBe(1); 26 | expect(prev.get(0).disabled).toBe(false); 27 | 28 | wrapper.setProps({ canClickPrev: false }); 29 | expect(prev.get(0).disabled).toBe(true); 30 | }); 31 | 32 | it("shows next button", () => { 33 | let next = wrapper.find("button").filterWhere(button => button.text() == "Next"); 34 | expect(next.length).toBe(1); 35 | expect(next.get(0).disabled).toBe(false); 36 | 37 | wrapper.setProps({ canClickNext: false }); 38 | expect(next.get(0).disabled).toBe(true); 39 | }); 40 | 41 | it("shows hide button", () => { 42 | let hide = wrapper.find("#oligrapherHideAnnotationsButton"); 43 | expect(hide.length).toBe(1); 44 | }); 45 | 46 | 47 | }); 48 | 49 | describe("alternateRendering", () => { 50 | let wrapper; 51 | let annotations = [ 52 | { id: 1, header: "Annotation 1", text: "

    annotation text 1

    " } 53 | ]; 54 | 55 | beforeEach(() => { 56 | wrapper = mount( 57 | 65 | ); 66 | }); 67 | 68 | it("doesn't show prev button", () => { 69 | let prev = wrapper.find("button").filterWhere(button => button.text() == "Prev"); 70 | expect(prev.length).toBe(0); 71 | }); 72 | 73 | it("shows next button", () => { 74 | let next = wrapper.find("button").filterWhere(button => button.text() == "Next"); 75 | expect(next.length).toBe(0); 76 | }); 77 | 78 | it("shows hide button", () => { 79 | let hide = wrapper.find("#oligrapherHideAnnotationsButton"); 80 | expect(hide.length).toBe(1); 81 | }); 82 | 83 | 84 | }); 85 | 86 | describe("behavior", () => { 87 | let prevClick = jest.genMockFunction(); 88 | let nextClick = jest.genMockFunction(); 89 | let swapAnnotations = jest.genMockFunction(); 90 | let wrapper; 91 | 92 | beforeEach(() => { 93 | wrapper = shallow( 94 | 101 | ); 102 | }); 103 | 104 | it("uses prevClick", () => { 105 | let prev = wrapper.find("button").filterWhere(button => button.text() == "Prev"); 106 | prev.simulate("click"); 107 | expect(prevClick.mock.calls.length).toBe(1); 108 | }); 109 | 110 | it("uses nextClick", () => { 111 | let next = wrapper.find("button").filterWhere(button => button.text() == "Next"); 112 | next.simulate("click"); 113 | expect(nextClick.mock.calls.length).toBe(1); 114 | }); 115 | 116 | it("uses swapAnnotations", () => { 117 | let hide = wrapper.find("#oligrapherHideAnnotationsButton"); 118 | hide.simulate("click"); 119 | expect(swapAnnotations.mock.calls.length).toBe(1); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /app/components/__tests__/GraphTitle-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import GraphTitle from '../GraphTitle'; 4 | 5 | describe('GraphTitle', () => { 6 | 7 | it('has h1', () => { 8 | expect(shallow().find('h1').length).toEqual(1); 9 | }); 10 | 11 | it('does not have fontSize set when isEmedded is false', () => { 12 | let graphTitle = shallow() 13 | expect(graphTitle.find('h1').prop('style').fontSize).toBeUndefined(); 14 | }); 15 | 16 | it('does not have fontSize set when isEmedded is undefined', () => { 17 | let graphTitle = shallow() 18 | expect(graphTitle.find('h1').prop('style').fontSize).toBeUndefined(); 19 | }); 20 | 21 | it('has fontSize set when isEmedded', () => { 22 | let graphTitle = shallow() 23 | expect(graphTitle.find('h1').prop('style').fontSize).toEqual('20px'); 24 | }) 25 | 26 | it('does not have a link by default', () => { 27 | let graphTitle = shallow() 28 | expect(graphTitle.find('a').exists()).toEqual(false); 29 | }) 30 | 31 | it('create link when provided url as prop', () => { 32 | let graphTitle = shallow() 33 | expect(graphTitle.find('a').exists()).toEqual(true); 34 | }) 35 | 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /app/components/__tests__/Node-test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from "enzyme"; 3 | import Node from '../Node'; 4 | import NodeCircle from "../NodeCircle"; 5 | import NodeLabel from "../NodeLabel"; 6 | import TestUtils from 'react-addons-test-utils'; 7 | 8 | 9 | describe("Node Component", () => { 10 | 11 | const data = { 12 | id: 34627, 13 | display: { 14 | name: "Ben Bernanke", 15 | x: 332.1385584381518, 16 | y: -305.26797977275623, 17 | scale: 1, 18 | status: "normal", 19 | image: "//s3.amazonaws.com/pai-littlesis/images/profile/6a7809b213a39c95b7d15334a33fe1f41e417a7b_1232040065.png", 20 | url: "//littlesis.org/person/34627/Ben_Bernanke" 21 | } 22 | }; 23 | 24 | it("should have an svg transform", () => { 25 | let wrapper = shallow( 26 | 27 | ); 28 | let element = wrapper.find("g.node"); 29 | let { x, y } = data.display; 30 | 31 | expect(element.props().transform).toBe(`translate(${x}, ${y})`); 32 | }); 33 | 34 | it("should display a NodeCircle", () => { 35 | let wrapper = shallow( 36 | 37 | ); 38 | let circle = wrapper.find(NodeCircle); 39 | 40 | expect(circle.props().node).toBe(data); 41 | expect(circle.props().selected).toBe(true); 42 | }); 43 | 44 | it("should display a NodeLabel", () => { 45 | let wrapper = shallow( 46 | 47 | ); 48 | let label = wrapper.find(NodeLabel); 49 | 50 | expect(label.props().node).toBe(data); 51 | }); 52 | 53 | it("should call click callback if clicked", () => { 54 | let clickNode = jest.genMockFunction(); 55 | let wrapper = shallow( 56 | 57 | ); 58 | let element = wrapper.find("g.node"); 59 | element.simulate("click"); 60 | 61 | expect(clickNode.mock.calls[0][0]).toBe(data.id); 62 | }); 63 | 64 | // NOT WORKING: TO FIND .handle WE NEED FULL RENDER, WHICH ISN'T WORKING FOR SVG 65 | xit("can be dragged to a new position", () => { 66 | let moveNode = jest.genMockFunction(); 67 | let wrapper = shallow( 68 | 69 | ); 70 | let handle = wrapper.find(".handle"); 71 | 72 | handle.simulate("dragStart"); 73 | handle.simulate("drag"); 74 | handle.simulate("dragEnd"); 75 | 76 | expect(moveNode.mock.calls.length).toBe(1); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /app/components/__tests__/NodeLabel-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import NodeLabel from "../NodeLabel"; 4 | import ds from "../../NodeDisplaySettings"; 5 | import merge from "lodash/merge"; 6 | 7 | describe("NodeLabel", () => { 8 | let wrapper; 9 | const data = { 10 | id: 34627, 11 | display: { 12 | name: "Benjamin Shalom Bernanke", 13 | x: 332.1385584381518, 14 | y: -305.26797977275623, 15 | scale: 1, 16 | status: "highlighted", 17 | image: "//s3.amazonaws.com/pai-littlesis/images/profile/6a7809b213a39c95b7d15334a33fe1f41e417a7b_1232040065.png", 18 | url: "//littlesis.org/person/34627/Ben_Bernanke" 19 | } 20 | }; 21 | 22 | beforeEach(() => { 23 | wrapper = shallow( 24 | 25 | ); 26 | }); 27 | 28 | it("should show text with tspans", () => { 29 | let text = wrapper.find("text"); 30 | expect(text.length).toBe(1); 31 | expect(text.props().textAnchor).toBe("middle"); 32 | 33 | let tspans = wrapper.find("tspan"); 34 | expect(tspans.length).toBe(2); 35 | expect(tspans.map(tspan => tspan.text()).join(" ")).toBe(data.display.name.replace(/\s+/g, " ")); 36 | 37 | tspans.forEach(tspan => { 38 | expect(tspan.props().fill).toBe(ds.textColor[data.display.status]); 39 | expect(tspan.props().opacity).toBe(ds.textOpacity[data.display.status]); 40 | }); 41 | }); 42 | 43 | it("should show link", () => { 44 | let link = wrapper.find("a"); 45 | expect(link.length).toBe(1); 46 | expect(link.props().xlinkHref).toBe(data.display.url); 47 | expect(link.props().target).toBe("_blank"); 48 | }); 49 | 50 | it("should show rects", () => { 51 | let rects = wrapper.find("rect"); 52 | expect(rects.length).toBe(2); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /app/components/__tests__/UpdateCaptionForm-test.jsx: -------------------------------------------------------------------------------- 1 | jest.disableAutomock(); 2 | 3 | import React from "react"; 4 | import { mount } from "enzyme"; 5 | 6 | import UpdateCaptionForm from "../UpdateCaptionForm"; 7 | 8 | describe("UpdateCaptionForm", () => { 9 | let wrapper; 10 | let updateCaption; 11 | let data = { 12 | id: 1, 13 | node1_id: 1, 14 | node2_id: 2, 15 | display: { 16 | text: "Caption", 17 | scale: 3, 18 | status: "highlighted" 19 | } 20 | }; 21 | 22 | beforeEach(() => { 23 | updateCaption = jest.genMockFunction(); 24 | wrapper = mount( 25 | 29 | ); 30 | }); 31 | 32 | describe("rendering", () => { 33 | it("shows text input", () => { 34 | let input = wrapper.ref("text"); 35 | expect(input.length).toBe(1); 36 | expect(input.props().value).toBe(data.display.text); 37 | }); 38 | 39 | it("shows scale dropdown with selected option", () => { 40 | let select = wrapper.ref("scale"); 41 | expect(select.length).toBe(1); 42 | expect(select.props().value).toBe(data.display.scale); 43 | 44 | let option = select.children().findWhere(child => child.props().value === data.display.scale); 45 | expect(option.length).toBe(1); 46 | expect(option.text()).toBe(data.display.scale + "x"); 47 | }); 48 | }); 49 | 50 | describe("behavior", () => { 51 | it("passes updated text to updateCaption", () => { 52 | let input = wrapper.ref("text"); 53 | input.get(0).value = "Caption 2 "; 54 | input.props().onChange(); 55 | 56 | expect(updateCaption.mock.calls.length).toBe(1); 57 | expect(updateCaption.mock.calls[0][1].display.text).toBe("Caption 2"); 58 | }); 59 | 60 | it("passes updated scale to updateCaption", () => { 61 | let select = wrapper.ref("scale"); 62 | select.get(0).value = 1.5; 63 | select.props().onChange(); 64 | 65 | expect(updateCaption.mock.calls.length).toBe(1); 66 | expect(updateCaption.mock.calls[0][1].display.scale).toBe(1.5); 67 | }); 68 | }); 69 | }); -------------------------------------------------------------------------------- /app/components/__tests__/UpdateEdgeForm-test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import UpdateEdgeForm from "../UpdateEdgeForm"; 4 | import EdgeDropdown from "../EdgeDropdown"; 5 | 6 | describe("UpdateEdgeForm", () => { 7 | let wrapper; 8 | let updateEdge; 9 | let data = { 10 | id: 1, 11 | node1_id: 1, 12 | node2_id: 2, 13 | display: { 14 | label: "Edge", 15 | url: "url", 16 | scale: 3, 17 | arrow: "right", 18 | dash: false, 19 | status: "highlighted" 20 | } 21 | }; 22 | 23 | beforeEach(() => { 24 | updateEdge = jest.genMockFunction(); 25 | wrapper = shallow( 26 | 31 | ); 32 | }); 33 | 34 | describe("rendering", () => { 35 | 36 | it("shows 2 inputs", () => { 37 | expect(wrapper.find('input').length).toBe(2); 38 | }); 39 | 40 | it('shows input with value label', () => { 41 | expect(wrapper.find('input[value="Edge"]').length).toBe(1); 42 | 43 | }); 44 | 45 | it('shows input with value url', () => { 46 | expect(wrapper.find('input[value="url"]').length).toBe(1); 47 | }); 48 | 49 | it('renders EdgeDropDown', () => { 50 | expect(wrapper.find(EdgeDropdown).length).toBe(1); 51 | }); 52 | 53 | it('passes correct props to EdgeDrop down', () => { 54 | let edgeProps = wrapper.find(EdgeDropdown).props(); 55 | expect(edgeProps.arrow).toBe(data.display.arrow); 56 | expect(edgeProps.dash).toBe(data.display.dash); 57 | }); 58 | 59 | it('shows