From e3cadcf90704f22f34e7172b499d2e22be7bc5ab Mon Sep 17 00:00:00 2001 From: David Ballester Mena Date: Fri, 15 Nov 2019 12:21:00 +0100 Subject: [PATCH 1/6] chore: add react-helmet dependency --- package-lock.json | 26 +++++++++++++++++++++++++- package.json | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index fca2f0b..cc0abc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "grapher", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -20912,6 +20912,17 @@ "react-kapsule": "^1.4.2" } }, + "react-helmet": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz", + "integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.5.4", + "react-fast-compare": "^2.0.2", + "react-side-effect": "^1.1.0" + } + }, "react-input-autosize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", @@ -21105,6 +21116,14 @@ "react-transition-group": "^2.2.1" } }, + "react-side-effect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz", + "integrity": "sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==", + "requires": { + "shallowequal": "^1.0.1" + } + }, "react-test-renderer": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", @@ -22383,6 +22402,11 @@ } } }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/package.json b/package.json index 9e4ac73..e6c1034 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-color": "2.17.3", "react-dom": "16.8.3", "react-force-graph-2d": "1.9.1", + "react-helmet": "^5.2.1", "react-markdown": "4.1.0", "react-redux": "6.0.1", "react-router-dom": "5.0.0", From 0e6c6de3a44a2ba6140cd22b26d3169a53a3a95c Mon Sep 17 00:00:00 2001 From: David Ballester Mena Date: Fri, 15 Nov 2019 12:28:09 +0100 Subject: [PATCH 2/6] feat: add meta tags to main page --- src/app.component.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app.component.js b/src/app.component.js index 14d84a4..cf572b6 100755 --- a/src/app.component.js +++ b/src/app.component.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { Route } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; import Canvas from './components/canvas'; import Graph from './scenes/graph'; @@ -11,6 +12,12 @@ export class App extends Component { render() { return ( + + + Grapher + + + } /> [, ]} /> } /> From c76c4b42709dd92f8e57a264c78c4b333e353c59 Mon Sep 17 00:00:00 2001 From: David Ballester Mena Date: Fri, 15 Nov 2019 12:42:18 +0100 Subject: [PATCH 3/6] chore: add basic robots.txt --- public/robots.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 public/robots.txt diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..2a23c4c --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: \ No newline at end of file From d1727db9ff8c8d32cd8fc35bca1a8363fefb1ccc Mon Sep 17 00:00:00 2001 From: David Ballester Mena Date: Fri, 15 Nov 2019 12:42:40 +0100 Subject: [PATCH 4/6] fix(graph): return empty links and nodes if empty state --- src/ducks/graph/graph.selectors.js | 6 +++--- src/ducks/graph/graph.selectors.spec.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ducks/graph/graph.selectors.js b/src/ducks/graph/graph.selectors.js index e9d1508..0d76ee3 100644 --- a/src/ducks/graph/graph.selectors.js +++ b/src/ducks/graph/graph.selectors.js @@ -15,11 +15,11 @@ export function getName(state) { } function getNodes(state) { - return graphSelector(state).nodes; + return graphSelector(state).nodes || {}; } export function getLinks(state) { - return graphSelector(state).links; + return graphSelector(state).links || {}; } export function getLinkById(state, linkId) { @@ -64,7 +64,7 @@ export const getSerializedGraph = createSelector( const getGroups = createSelector( graphSelector, - (graph) => graph.groups + (graph) => graph.groups || [] ); export const getGroupsAsArray = createSelector( diff --git a/src/ducks/graph/graph.selectors.spec.js b/src/ducks/graph/graph.selectors.spec.js index 272ee7f..915da68 100644 --- a/src/ducks/graph/graph.selectors.spec.js +++ b/src/ducks/graph/graph.selectors.spec.js @@ -76,6 +76,11 @@ describe('selectors', () => { const nodes = getNodesAsArray(appState); expect(nodes).toEqual([node1, node2, node3]); }); + + it('returns an empty array if the state contains no nodes', () => { + const nodes = getNodesAsArray({ graph: {} }); + expect(nodes).toEqual([]); + }); }); describe('getLinksAsArray', () => { @@ -105,6 +110,11 @@ describe('selectors', () => { const links = getLinksAsArray(appState); expect(links).toEqual([link1, link2, link3]); }); + + it('returns an empty array if the state contains no links', () => { + const links = getLinksAsArray({ graph: {} }); + expect(links).toEqual([]); + }); }); describe(getNodesIds.name, () => { From 5c831424e7077e58954b4c6938cd3a954af218f6 Mon Sep 17 00:00:00 2001 From: David Ballester Mena Date: Fri, 15 Nov 2019 13:13:06 +0100 Subject: [PATCH 5/6] feat(graph): include in the state a graph not found error --- src/ducks/graph/graph.actions.js | 7 ++++++ src/ducks/graph/graph.reducer.js | 19 ++++++++++++++- src/ducks/graph/graph.reducer.spec.js | 34 +++++++++++++++++++++++---- src/ducks/graph/graph.sagas.js | 7 +++++- src/ducks/graph/graph.sagas.spec.js | 9 ++++++- src/ducks/graph/graph.selectors.js | 4 ++++ 6 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/ducks/graph/graph.actions.js b/src/ducks/graph/graph.actions.js index 9c78489..8c1df45 100644 --- a/src/ducks/graph/graph.actions.js +++ b/src/ducks/graph/graph.actions.js @@ -4,6 +4,7 @@ export const GRAPH_SET_NAME = 'grapher/Graph/SET_NAME'; export const GRAPH_CREATE = 'grapher/Graph/CREATE'; export const GRAPH_LOAD = 'grapher/Graph/LOAD'; export const GRAPH_LOAD_SUCCESS = 'grapher/Graph/LOAD_SUCCESS'; +export const GRAPH_LOAD_ERROR = 'grapher/Graph/LOAD_ERROR'; export const GRAPH_DELETE = 'grapher/Graph/DELETE'; export const GRAPH_SET_CONTENTS = 'grapher/Graph/SET_CONTENTS'; export const GRAPH_SET_TEXT = 'grapher/Graph/SET_TEXT'; @@ -44,6 +45,12 @@ export function loadGraphSuccess(graph) { }; } +export function loadGraphError() { + return { + type: GRAPH_LOAD_ERROR, + }; +} + export function deleteGraph(id) { return { type: GRAPH_DELETE, diff --git a/src/ducks/graph/graph.reducer.js b/src/ducks/graph/graph.reducer.js index 011cfbc..a55c65b 100644 --- a/src/ducks/graph/graph.reducer.js +++ b/src/ducks/graph/graph.reducer.js @@ -1,4 +1,12 @@ -import { GRAPH_SET_NAME, GRAPH_CREATE, GRAPH_LOAD_SUCCESS, GRAPH_SET_CONTENTS, GRAPH_SET_TEXT, GRAPH_SET_TEXT_ERROR } from './graph.actions'; +import { + GRAPH_SET_NAME, + GRAPH_CREATE, + GRAPH_LOAD_SUCCESS, + GRAPH_LOAD_ERROR, + GRAPH_SET_CONTENTS, + GRAPH_SET_TEXT, + GRAPH_SET_TEXT_ERROR, +} from './graph.actions'; const initialState = { id: '', @@ -66,6 +74,7 @@ const initialState = { groups: {}, text: '', textError: undefined, + loadError: false, }; export default function reducer(state = initialState, action) { @@ -87,11 +96,19 @@ export default function reducer(state = initialState, action) { groups: {}, text: '', ...graph, + loadError: false, }; } case GRAPH_LOAD_SUCCESS: { return { ...action.payload, + loadError: false, + }; + } + case GRAPH_LOAD_ERROR: { + return { + ...state, + loadError: true, }; } case GRAPH_SET_CONTENTS: { diff --git a/src/ducks/graph/graph.reducer.spec.js b/src/ducks/graph/graph.reducer.spec.js index ac5aed5..fcfc099 100644 --- a/src/ducks/graph/graph.reducer.spec.js +++ b/src/ducks/graph/graph.reducer.spec.js @@ -4,7 +4,7 @@ jest.mock('uuid/v4', () => ({ default: () => 'uuid', })); -import { setGraphName, createGraph, loadGraphSuccess, setContents, setText, setTextError } from './graph.actions'; +import { setGraphName, createGraph, loadGraphSuccess, loadGraphError, setContents, setText, setTextError } from './graph.actions'; import reducer from './graph.reducer'; describe('reducer', () => { @@ -67,7 +67,14 @@ describe('reducer', () => { }; const action = createGraph({}); const state = reducer(initialState, action); - expect(state).toEqual(expectedState); + expect(state).toEqual(expect.objectContaining(expectedState)); + }); + + it('sets the loadError flag to false', () => { + const initialState = { loadError: true }; + const action = createGraph({}); + const state = reducer(initialState, action); + expect(state.loadError).toBeFalsy(); }); }); @@ -83,9 +90,26 @@ describe('reducer', () => { }; const action = loadGraphSuccess(graph); const state = reducer(initialState, action); - expect(state).toEqual({ - ...graph, - }); + expect(state).toEqual( + expect.objectContaining({ + ...graph, + }) + ); + }); + + it('sets the loadError flag to false', () => { + const initialState = { loadError: true }; + const action = loadGraphSuccess({}); + const state = reducer(initialState, action); + expect(state.loadError).toBeFalsy(); + }); + }); + + describe('GRAPH_LOAD_ERROR', () => { + it('sets the loadError flag to true', () => { + const action = loadGraphError(); + const state = reducer({}, action); + expect(state.loadError).toBeTruthy(); }); }); diff --git a/src/ducks/graph/graph.sagas.js b/src/ducks/graph/graph.sagas.js index a6edf33..7c5fc92 100644 --- a/src/ducks/graph/graph.sagas.js +++ b/src/ducks/graph/graph.sagas.js @@ -9,6 +9,7 @@ import { GRAPH_LOAD, GRAPH_DELETE, loadGraphSuccess, + loadGraphError, GRAPH_SET_CONTENTS, GRAPH_SET_TEXT, setTextError, @@ -30,7 +31,11 @@ export function* saveGraphSaga() { export function* doLoadGraph(action) { const graphId = action.payload; const graph = yield call([graphService, 'readGraph'], graphId); - yield put(loadGraphSuccess(graph)); + if (graph) { + yield put(loadGraphSuccess(graph)); + } else { + yield put(loadGraphError()); + } } export function* loadGraphSaga() { diff --git a/src/ducks/graph/graph.sagas.spec.js b/src/ducks/graph/graph.sagas.spec.js index db7034a..412912f 100644 --- a/src/ducks/graph/graph.sagas.spec.js +++ b/src/ducks/graph/graph.sagas.spec.js @@ -9,6 +9,7 @@ import { GRAPH_LOAD, loadGraph, loadGraphSuccess, + loadGraphError, GRAPH_DELETE, deleteGraph, GRAPH_SET_CONTENTS, @@ -96,12 +97,18 @@ describe('graph', () => { expect(gen.next().value).toEqual(call([graphService, 'readGraph'], action.payload)); }); - it('puts a `loadGraphSuccess` action with the graph returned by `readGraph`', () => { + it('puts a `loadGraphSuccess` action with the graph returned by `readGraph` if there is such a graph', () => { const graph = { foo: 'bar' }; const gen = cloneableGenerator(doLoadGraph)(action); gen.next(); expect(gen.next(graph).value).toEqual(put(loadGraphSuccess(graph))); }); + + it('puts a loadGraphError action if there is no graph read', () => { + const gen = cloneableGenerator(doLoadGraph)(action); + gen.next(); + expect(gen.next(undefined).value).toEqual(put(loadGraphError())); + }); }); describe(deleteGraphSaga.name, () => { diff --git a/src/ducks/graph/graph.selectors.js b/src/ducks/graph/graph.selectors.js index 0d76ee3..7bd9a7e 100644 --- a/src/ducks/graph/graph.selectors.js +++ b/src/ducks/graph/graph.selectors.js @@ -83,3 +83,7 @@ export function getText(state) { export function getTextError(state) { return graphSelector(state).textError; } + +export function getLoadError(state) { + return graphSelector(state).loadError; +} From 01eff77d254e500030c4193d322c6af577faff68 Mon Sep 17 00:00:00 2001 From: David Ballester Mena Date: Fri, 15 Nov 2019 13:13:22 +0100 Subject: [PATCH 6/6] feat(graph): display a not found component for graphs not found --- .../graph/components/not-found.component.js | 37 +++++++++++++++++++ src/scenes/graph/graph.component.js | 13 +++++-- src/scenes/graph/graph.component.spec.js | 6 +++ src/scenes/graph/graph.container.js | 3 +- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/scenes/graph/components/not-found.component.js diff --git a/src/scenes/graph/components/not-found.component.js b/src/scenes/graph/components/not-found.component.js new file mode 100644 index 0000000..7e8e001 --- /dev/null +++ b/src/scenes/graph/components/not-found.component.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import Container from '@material-ui/core/Container'; +import Typography from '@material-ui/core/Typography'; +import Link from '@material-ui/core/Link'; +import { withStyles } from '@material-ui/core/styles'; + +const styles = (theme) => ({ + title: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(4), + }, + text: { + marginBottom: theme.spacing(2), + }, +}); + +function NotFound({ classes }) { + return ( + + + Graph not found! + + + That's unfortunate. If you are looking for a graph that was there before, remember that they are stored in your browser. Are you using a + different browser? + + + To avoid accidental losses, remember that you can export your graphs from the top right corner and save them as JSON files into your computer + that you can then import into Grapher whenever you want. + + Go to your graphs + + ); +} + +export default withStyles(styles, { withTheme: true })(NotFound); diff --git a/src/scenes/graph/graph.component.js b/src/scenes/graph/graph.component.js index 2927192..75cc417 100644 --- a/src/scenes/graph/graph.component.js +++ b/src/scenes/graph/graph.component.js @@ -11,13 +11,20 @@ import Export from './components/export'; import Onboarding from './components/onboarding'; import GraphLarge from './components/graph-large.component'; import GraphSmall from './components/graph-small.component'; +import NotFound from './components/not-found.component'; + +export default function Graph({ graphId, graphName, loadedGraphId, loadGraph, openGraphList, loadError }) { + const theme = useTheme(); + const bigScreen = useMediaQuery(theme.breakpoints.up('md')); + + if (loadError) { + return ; + } -export default function Graph({ graphId, graphName, loadedGraphId, loadGraph, openGraphList, classes }) { if (!!graphId && graphId !== loadedGraphId) { loadGraph(graphId); } - const theme = useTheme(); - const bigScreen = useMediaQuery(theme.breakpoints.up('md')); + return ( <> diff --git a/src/scenes/graph/graph.component.spec.js b/src/scenes/graph/graph.component.spec.js index 742ead8..aeef55a 100644 --- a/src/scenes/graph/graph.component.spec.js +++ b/src/scenes/graph/graph.component.spec.js @@ -2,6 +2,7 @@ import React from 'react'; import { createShallow } from '@material-ui/core/test-utils'; import Graph from './graph.component'; +import NotFound from './components/not-found.component'; describe(Graph.name, () => { let shallow; @@ -35,4 +36,9 @@ describe(Graph.name, () => { shallow(); expect(loadGraph).toHaveBeenCalledWith('foo'); }); + + it('returns the NotFound component if the loadError prop is true', () => { + const component = shallow(); + expect(component.find(NotFound).getElement()).toBeDefined(); + }); }); diff --git a/src/scenes/graph/graph.container.js b/src/scenes/graph/graph.container.js index 29a9d5e..1b84f09 100644 --- a/src/scenes/graph/graph.container.js +++ b/src/scenes/graph/graph.container.js @@ -3,7 +3,7 @@ import { withRouter } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import Graph from './graph.component'; -import { loadGraph, getId, getName } from '../../ducks/graph'; +import { loadGraph, getId, getName, getLoadError } from '../../ducks/graph'; import { openGraphList } from '../../ducks/navigation.duck'; function mapStateToProps(state, ownProps) { @@ -11,6 +11,7 @@ function mapStateToProps(state, ownProps) { graphId: ownProps.match.params.graphId, loadedGraphId: getId(state), graphName: getName(state), + loadError: getLoadError(state), }; }