diff --git a/.babelrc b/.babelrc index 38733e673..3b129c7f0 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,10 @@ { - "presets": ["react", "es2015", "stage-0"] + "presets": ["react", "es2015", "stage-0"], + "env": { + "development": { + "plugins": [ + "transform-react-display-name", + ] + } + } } diff --git a/app/scripts/app.js b/app/scripts/app.js index 12b11cd9f..d091f86ee 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -1,6 +1,7 @@ import React from 'react' -import ReactDOM from 'react-dom' -import Router from 'react-router' +import {render} from 'react-dom' +import Router, {hashHistory} from 'react-router' + import routes from './routes' require('../styles/app.less') @@ -9,8 +10,8 @@ if (process.env.NODE_ENV !== 'production') { window.uiDebug = require('debug') } -document.addEventListener('DOMContentLoaded', function () { - Router.run(routes, function (Handler) { - ReactDOM.render(, document.getElementById('root')) - }) +document.addEventListener('DOMContentLoaded', () => { + const root = document.getElementById('root') + + render(, root) }) diff --git a/app/scripts/pages/files.js b/app/scripts/pages/files.js index e7a47e79c..c4313b135 100644 --- a/app/scripts/pages/files.js +++ b/app/scripts/pages/files.js @@ -1,16 +1,19 @@ import React from 'react' import ReactDOM from 'react-dom' +import $ from 'jquery' +import {Row, Col, Nav, NavItem, Panel} from 'react-bootstrap' +import {LinkContainer} from 'react-router-bootstrap' + import FileList from '../views/filelist' import LocalStorage from '../utils/localStorage' -import $ from 'jquery' import i18n from '../utils/i18n.js' -import {Row, Col, Nav, Panel} from 'react-bootstrap' export default React.createClass({ displayName: 'Files', propTypes: { ipfs: React.PropTypes.object, - gateway: React.PropTypes.string + gateway: React.PropTypes.string, + location: React.PropTypes.object }, getInitialState: function () { var t = this @@ -125,62 +128,130 @@ export default React.createClass({ // TODO }, - render: function () { - var tab = window.location.hash.split('/') - tab = tab.length >= 3 ? tab[2] : tab[1] + _renderTitle () { + switch (this.props.location.pathname) { + case '/files': + return this._renderAdder() + case '/files/pinned': + return

{i18n.t('Pinned Files')}

+ case '/files/all': + return

{i18n.t('All Local Files')}

+ default: + return '' + } + }, + _renderAdder () { return ( - - - - -
-
-
-
-
-

{i18n.t('Drag-and-drop your files here')}

-

{i18n.t('or')}

-

- -

-
-
-

{i18n.t('Drop your file here to add it to IPFS')}

-
-
-

{i18n.t('Added')} {this.state.confirm}

-
- +
+
-
- - - - +
+
+

+ {i18n.t('Drag-and-drop your files here')} +

+

+ {i18n.t('or')} +

+

+ +

+
+
+

+ {i18n.t('Drop your file here to add it to IPFS')} +

+
+
+

+ {i18n.t('Added')} {this.state.confirm} +

+
+
+ ) + }, -
-

{i18n.t('Pinned Files')}

- - - -
+ _renderPanel () { + switch (this.props.location.pathname) { + case '/files': + return ( + + + + ) + case '/files/pinned': + return ( + + + + ) + case '/files/all': + return ( + + + + ) + default: + return '' + } + }, -
-

{i18n.t('All Local Files')}

- - - -
- - + render: function () { + return ( + + + + +
+ {this._renderTitle()} + {this._renderPanel()} +
+ +
) } }) diff --git a/app/scripts/pages/notfound.js b/app/scripts/pages/notfound.js index 366664161..c884db749 100644 --- a/app/scripts/pages/notfound.js +++ b/app/scripts/pages/notfound.js @@ -1,7 +1,8 @@ -var React = require('react') -var Row = require('react-bootstrap').Row -var Col = require('react-bootstrap').Col -var i18n = require('../utils/i18n.js') +import React from 'react' +import {Row, Col} from 'react-bootstrap' +import i18n from '../utils/i18n.js' + +import {Link} from 'react-router' export default React.createClass({ displayName: 'NotFound', @@ -9,11 +10,12 @@ export default React.createClass({ return ( -

{i18n.t('404 - Not Found')}

- -

{i18n.t('Go to console home')}

- +

+ + {i18n.t('Go to console home')} + +

) diff --git a/app/scripts/pages/objects.js b/app/scripts/pages/objects.js index 77616d2e1..af911c28b 100644 --- a/app/scripts/pages/objects.js +++ b/app/scripts/pages/objects.js @@ -1,5 +1,4 @@ import React from 'react' -import Router from 'react-router' import $ from 'jquery' import ObjectView from '../views/object' import {parse} from '../utils/path.js' @@ -8,10 +7,15 @@ import {Row, Col, Button} from 'react-bootstrap' export default React.createClass({ displayName: 'Objects', + + contextTypes: { + router: React.PropTypes.object.isRequired + }, + propTypes: { - gateway: React.PropTypes.string + gateway: React.PropTypes.string, + params: React.PropTypes.object }, - mixins: [ Router.State ], componentDidMount: function () { window.addEventListener('hashchange', this.updateState) @@ -22,7 +26,7 @@ export default React.createClass({ }, getStateFromRoute: function () { - var params = this.context.router.getCurrentParams() + const params = this.props.params var state = {} if (params.path) { var path = parse(params.path) @@ -66,9 +70,13 @@ export default React.createClass({ update: function (e) { if (e.which && e.which !== 13) return - var params = this.context.router.getCurrentParams() + const params = this.props.params params.path = parse(this.state.pathInput).urlify() - this.context.router.transitionTo('objects', params) + + const route = ['/objects'] + if (params.path) route.push(params.path) + + this.context.router.push(route.join('/')) }, render: function () { @@ -82,8 +90,7 @@ export default React.createClass({ : null // TODO add provider-view here - var views = { - object: (!error && this.state.object + var views = (!error && this.state.object ?
: null) - } - - var params = this.context.router.getCurrentParams() - var tab = params.tab return ( @@ -117,7 +120,7 @@ export default React.createClass({ {error} - {views[tab]} + {views} ) diff --git a/app/scripts/routes.js b/app/scripts/routes.js index 987ef9aab..bb33e65ac 100644 --- a/app/scripts/routes.js +++ b/app/scripts/routes.js @@ -1,5 +1,5 @@ import React from 'react' -import {Route, DefaultRoute, NotFoundRoute, Redirect} from 'react-router' +import {Route, IndexRoute, Redirect} from 'react-router' import Page from './views/page' import HomePage from './pages/home' @@ -13,18 +13,21 @@ import LogPage from './pages/logs' import NotFoundPage from './pages/notfound' export default ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ) diff --git a/app/scripts/views/config.js b/app/scripts/views/config.js index 181c0add2..b67dc15bd 100644 --- a/app/scripts/views/config.js +++ b/app/scripts/views/config.js @@ -11,7 +11,7 @@ export default React.createClass({ getInitialState: function () { return { - body: JSON.stringify(this.props.config, null, ' '), + body: JSON.stringify(this.props.config, null, 2), error: null, saving: false, saved: false @@ -20,7 +20,7 @@ export default React.createClass({ reset: function () { this.setState({ - body: JSON.stringify(this.props.config, null, ' '), + body: JSON.stringify(this.props.config, null, 2), error: null }) }, @@ -60,8 +60,6 @@ export default React.createClass({ saving: true }) - console.log(t.state.body) - t.props.ipfs.config.replace(new Buffer(t.state.body), function (err) { var newState = { saving: false } if (err) { diff --git a/app/scripts/views/filelist.js b/app/scripts/views/filelist.js index 4570e8a56..288d3afee 100644 --- a/app/scripts/views/filelist.js +++ b/app/scripts/views/filelist.js @@ -51,7 +51,7 @@ export default React.createClass({ } var gatewayPath = t.props.gateway + '/ipfs/' + file.id - var dagPath = '#/objects/object/' + file.id + var dagPath = '#/objects/' + file.id var tooltip = ( {i18n.t('Remove')} diff --git a/app/scripts/views/icon.js b/app/scripts/views/icon.js new file mode 100644 index 000000000..0a3da8443 --- /dev/null +++ b/app/scripts/views/icon.js @@ -0,0 +1,17 @@ +import React, {PropTypes} from 'react' + +function Icon ({glyph}) { + return ( + + ) +} + +Icon.propTypes = { + glyph: PropTypes.string.isRequired +} + +export default Icon diff --git a/app/scripts/views/nav-item.js b/app/scripts/views/nav-item.js new file mode 100644 index 000000000..17a9a8b67 --- /dev/null +++ b/app/scripts/views/nav-item.js @@ -0,0 +1,21 @@ +import React, {PropTypes} from 'react' +import {Link} from 'react-router' + +import i18n from '../utils/i18n' +import Icon from './icon' + +function NavItem ({title, url, icon}) { + return ( + + {i18n.t(title)} + + ) +} + +NavItem.propTypes = { + title: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired +} + +export default NavItem diff --git a/app/scripts/views/nav.js b/app/scripts/views/nav.js index ebbf015dc..99d0b349b 100644 --- a/app/scripts/views/nav.js +++ b/app/scripts/views/nav.js @@ -1,42 +1,35 @@ import React from 'react' -import {Link} from 'react-router' -import i18n from '../utils/i18n.js' + +import NavItem from './nav-item' export default React.createClass({ displayName: 'Nav', + + contextTypes: { + router: React.PropTypes.object.isRequired + }, + render: function () { return (
    -
  • - - {i18n.t('Home')} - +
  • +
  • - - {i18n.t('Connections')} - +
  • - - {i18n.t('Files')} - +
  • - - {i18n.t('DAG')} - +
  • - - {i18n.t('Config')} - +
  • - - {i18n.t('Logs')} - +
diff --git a/app/scripts/views/object.js b/app/scripts/views/object.js index c1a373f42..1ef71741e 100644 --- a/app/scripts/views/object.js +++ b/app/scripts/views/object.js @@ -1,6 +1,6 @@ import React from 'react' import {Link} from 'react-router' -import upath from '../utils/path.js' +import {parse} from '../utils/path.js' import i18n from '../utils/i18n.js' export default React.createClass({ @@ -19,7 +19,7 @@ export default React.createClass({ var t = this var parent = this.props.path.parent() var parentlink = parent - ? + ? {i18n.t('Parent object')} : null @@ -45,18 +45,18 @@ export default React.createClass({ if (link.Name) { path = t.props.path.append(link.Name).urlify() } else { // support un-named links - path = upath.parse(link.Hash).urlify() + path = parse(link.Hash).urlify() } return ( - + {link.Name} - + {link.Hash} @@ -73,7 +73,7 @@ export default React.createClass({ var resolved = this.props.permalink ?
  • {i18n.t('permalink:')} - + {this.props.permalink.toString()}
  • diff --git a/app/scripts/views/page.js b/app/scripts/views/page.js index 33b0d534f..9d1dad1df 100644 --- a/app/scripts/views/page.js +++ b/app/scripts/views/page.js @@ -1,9 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom' import Nav from './nav' -import {RouteHandler, Link} from 'react-router' +import {Link} from 'react-router' import $ from 'jquery' + import i18n from '../utils/i18n.js' +import {parse} from '../utils/path' var host = window.location.hostname var port = window.location.port || 80 @@ -18,6 +20,14 @@ var ipfsHost = window.location.host export default React.createClass({ displayName: 'Page', + + contextTypes: { + router: React.PropTypes.object.isRequired + }, + propTypes: { + children: React.PropTypes.object + }, + getInitialState: function () { var t = this @@ -50,7 +60,7 @@ export default React.createClass({ showDAG: function () { var path = $(ReactDOM.findDOMNode(this)).find('.dag-path').val() - window.location = '#/objects/object/' + path.replace(/\//g, '\\') + this.context.router.push(`/objects/${parse(path).urlify()}`) }, update: function () { @@ -86,7 +96,7 @@ export default React.createClass({
    - + IPFS{i18n.t('IPFS')}
    @@ -134,7 +144,9 @@ export default React.createClass({
    {update} - + {this.props.children && React.cloneElement( + this.props.children, {ipfs: ipfs, host: ipfsHost, gateway: this.state.gateway} + )}
    diff --git a/karma.conf.js b/karma.conf.js index 5f6a7e7c3..290e66237 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,4 +1,4 @@ -var webpackConfig = require('./webpack.config') +var webpackConfig = require('./make-config')(true) module.exports = function (config) { config.set({ @@ -10,11 +10,11 @@ module.exports = function (config) { frameworks: [ 'mocha' ], files: [ - 'tests.webpack.js' + 'test/setup.js' ], preprocessors: { - 'tests.webpack.js': [ 'webpack', 'sourcemap' ] + 'test/setup.js': ['webpack', 'sourcemap'] }, reporters: [ 'dots' ], diff --git a/make-config.js b/make-config.js new file mode 100644 index 000000000..317de8be0 --- /dev/null +++ b/make-config.js @@ -0,0 +1,42 @@ +var createConfig = require('hjs-webpack') + +module.exports = function makeConfig (isDev) { + var config = createConfig({ + isDev: isDev, + in: './app/scripts/app.js', + out: 'dist', + html: function (ctx) { + return ctx.defaultTemplate({ + publicPath: '' + }) + }, + clearBeforeBuild: '!(locale|img|favicon.ico)' + }) + + // Handle js-ipfs-api + config.module.loaders.push({ + test: /\.js$/, + include: /node_modules\/(hoek|qs|wreck|boom|ipfs-api)/, + loader: 'babel-loader' + }) + + config.externals = { + net: '{}', + fs: '{}', + tls: '{}', + console: '{}', + 'require-dir': '{}' + } + + config.resolve = { + modulesDirectories: [ + 'node_modules' + ], + alias: { + http: 'stream-http', + https: 'https-browserify' + } + } + + return config +} diff --git a/package.json b/package.json index dea5f1639..a04652ac9 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "react-bootstrap": "^0.28.1", "react-dom": "^0.14.2", "react-localstorage": "^0.2.8", - "react-router": "^0.13.5", + "react-router": "^2.0.0-rc4", "three": "^0.73.0" }, "devDependencies": { @@ -28,6 +28,7 @@ "babel-core": "^6.3.21", "babel-eslint": "^5.0.0-beta6", "babel-loader": "^6.2.0", + "babel-plugin-transform-react-display-name": "^6.3.13", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babel-preset-stage-0": "^6.3.13", @@ -43,6 +44,7 @@ "https-browserify": "0.0.1", "ipfs-api": "^2.10.1", "json-loader": "^0.5.4", + "jsx-chai": "^2.0.0", "karma": "^0.13.15", "karma-chrome-launcher": "^0.2.2", "karma-firefox-launcher": "^0.1.7", @@ -55,6 +57,7 @@ "postcss-loader": "^0.8.0", "pre-commit": "^1.1.2", "react-addons-test-utils": "^0.14.3", + "react-router-bootstrap": "^0.20.0", "stream-http": "^2.0.2", "style-loader": "^0.13.0", "url-loader": "^0.5.7", diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 000000000..7eeefc33b --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 000000000..9d3400782 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,19 @@ +// This gets replaced by karma webpack with the updated files on rebuild +var __karmaWebpackManifest__ = [] + +// require all modules ending in '_test' from the +// current directory and all subdirectories +var testsContext = require.context('.', true, /\.spec\.js$/) + +function inManifest (path) { + return __karmaWebpackManifest__.indexOf(path) >= 0 +} + +var runnable = testsContext.keys().filter(inManifest) + +// Run all tests if we didn't find any changes +if (!runnable.length) { + runnable = testsContext.keys() +} + +runnable.forEach(testsContext) diff --git a/test/test-helpers.js b/test/test-helpers.js new file mode 100644 index 000000000..871a408b3 --- /dev/null +++ b/test/test-helpers.js @@ -0,0 +1,13 @@ +import chai, {expect} from 'chai' +import jsxChai from 'jsx-chai' +import {createRenderer} from 'react-addons-test-utils' + +chai.use(jsxChai) + +export function shallowRender (comp) { + const renderer = createRenderer() + renderer.render(comp) + return renderer.getRenderOutput() +} + +export {expect} diff --git a/test/utils/path.spec.js b/test/utils/path.spec.js index 9201d17dd..a48bc6f95 100644 --- a/test/utils/path.spec.js +++ b/test/utils/path.spec.js @@ -1,5 +1,3 @@ -/* eslint-env mocha */ - import {expect} from 'chai' import {parse} from '../../app/scripts/utils/path' diff --git a/test/views/config.spec.js b/test/views/config.spec.js new file mode 100644 index 000000000..ed373a634 --- /dev/null +++ b/test/views/config.spec.js @@ -0,0 +1,20 @@ +import {expect, shallowRender} from '../test-helpers' +import React from 'react' + +import ConfigView from '../../app/scripts/views/config' + +describe('ConfigView', () => { + it('renders the given config', () => { + const config = {a: true, b: {c: 'hello'}} + const el = shallowRender() + + expect(el).to.contain( +