diff --git a/README.md b/README.md index 6e40c8708..48c6321d4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,18 @@ Install dependencies by running the following in the root of the project: To contribute to the repository, please create a feature branch off of the dev branch. Once you're finished working on the feature, make a pull request to merge it into dev. Please make sure that every pull request has passed the build checks, which appear just before the "Merge pull request" button in github. +### Updating npm-shrinkwrap.json + +Use **npm v5+** for this. +General workflow to update `npm-shrinkwrap.json` would be: + +- `npm install --no-optional` - with old npm-shrinkwrap (--no-optional to skip fsevents) +- update `package.json` if you need to remove/update/add any packages +- remove `npm-shrinkwrap.json` +- `npm install --no-optional` with new `package.json` +- `npm shrinkwrap` - to convert `package-lock.json` to `npm-shrinkwrap.json` +- the new `npm-shrinkwrap.json` will have just the minimal diff + ### Code Style ***Checkout the code and comments in `src/components/ExampleComponent` for an example React component, `.scss` file, and tests.*** diff --git a/config/webpack/default.js b/config/webpack/default.js index 8f05ed557..68fe34300 100644 --- a/config/webpack/default.js +++ b/config/webpack/default.js @@ -22,7 +22,8 @@ module.exports = { output: { path : path.join(dirname, '/dist'), filename : '[name].[hash].js', - chunkFilename : '[name].[hash].js' + chunkFilename : '[name].[hash].js', + publicPath : '/' }, module: { diff --git a/config/webpack/development.js b/config/webpack/development.js index cbedc683f..54ab79896 100644 --- a/config/webpack/development.js +++ b/config/webpack/development.js @@ -6,7 +6,9 @@ const defaultConfig = require('./default') const dirname = path.resolve(__dirname, '../..') -module.exports = webpackMerge(defaultConfig, { +module.exports = webpackMerge.strategy({ + entry: 'prepend' // to put 'react-hot-loader/patch' first +})(defaultConfig, { entry: [ 'react-hot-loader/patch' ], diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 000c3a8d2..56d5f344f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -122,7 +122,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, "requires": { "kind-of": "3.2.2", "longest": "1.0.1", @@ -189,6 +188,7 @@ "requires": { "axios": "0.8.1", "history": "1.17.0", + "html-webpack-plugin": "1.7.0", "humps": "0.6.0", "isomorphic-fetch": "2.2.1", "jwt-decode": "1.5.1", @@ -206,6 +206,11 @@ "redux-thunk": "0.1.0" }, "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, "axios": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.8.1.tgz", @@ -214,6 +219,25 @@ "follow-redirects": "0.0.7" } }, + "clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "requires": { + "commander": "2.8.1", + "source-map": "0.4.4" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "requires": { + "graceful-readlink": "1.0.1" + } + } + } + }, "fbjs": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.6.1.tgz", @@ -226,6 +250,44 @@ "whatwg-fetch": "0.9.0" } }, + "he": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.0.0.tgz", + "integrity": "sha1-baWyZdfyw7XkgHSRaODhWdBXKNo=" + }, + "html-minifier": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-1.5.0.tgz", + "integrity": "sha1-vrBf2cw0CUWGXBD0Cu30aa9LFTQ=", + "requires": { + "change-case": "2.3.1", + "clean-css": "3.4.28", + "commander": "2.9.0", + "concat-stream": "1.5.2", + "he": "1.0.0", + "ncname": "1.0.0", + "relateurl": "0.2.7", + "uglify-js": "2.6.4" + } + }, + "html-webpack-plugin": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-1.7.0.tgz", + "integrity": "sha1-zQxzx5G9DIxFsk4wAb4zSmt0KXs=", + "requires": { + "bluebird": "3.5.0", + "blueimp-tmpl": "2.5.7", + "html-minifier": "1.5.0", + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, "react": { "version": "0.14.9", "resolved": "https://registry.npmjs.org/react/-/react-0.14.9.tgz", @@ -254,6 +316,24 @@ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-0.1.0.tgz", "integrity": "sha1-jjR2BoCLNb+Kkn30YW9v7RAYJuU=" }, + "uglify-js": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.6.4.tgz", + "integrity": "sha1-ZeovswWck5RpLxX+2HwrNsFrmt8=", + "requires": { + "async": "0.2.10", + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, "whatwg-fetch": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz", @@ -1574,8 +1654,12 @@ "bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", - "dev": true + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "blueimp-tmpl": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/blueimp-tmpl/-/blueimp-tmpl-2.5.7.tgz", + "integrity": "sha1-M/sSwTnWVRKuQK+9ji3vjZ25ZJA=" }, "bmp-js": { "version": "0.0.3", @@ -1861,8 +1945,7 @@ "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" }, "camelcase-keys": { "version": "2.1.0", @@ -1928,7 +2011,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, "requires": { "align-text": "0.1.4", "lazy-cache": "1.0.4" @@ -1963,6 +2045,48 @@ "supports-color": "2.0.0" } }, + "change-case": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-2.3.1.tgz", + "integrity": "sha1-LE/ePwY7tB0AzWjg1aCdthy+iU8=", + "requires": { + "camel-case": "1.2.2", + "constant-case": "1.1.2", + "dot-case": "1.1.2", + "is-lower-case": "1.1.3", + "is-upper-case": "1.1.2", + "lower-case": "1.1.4", + "lower-case-first": "1.0.2", + "param-case": "1.1.2", + "pascal-case": "1.1.2", + "path-case": "1.1.2", + "sentence-case": "1.1.3", + "snake-case": "1.1.2", + "swap-case": "1.1.2", + "title-case": "1.1.2", + "upper-case": "1.1.3", + "upper-case-first": "1.1.2" + }, + "dependencies": { + "camel-case": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-1.2.2.tgz", + "integrity": "sha1-Gsp8TRlTWaLOmVV5NDPG5VQlEfI=", + "requires": { + "sentence-case": "1.1.3", + "upper-case": "1.1.3" + } + }, + "param-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-1.1.2.tgz", + "integrity": "sha1-3LCRpDwlm5Io8cNB57akTqC/l0M=", + "requires": { + "sentence-case": "1.1.3" + } + } + } + }, "change-emitter": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", @@ -2128,7 +2252,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, "requires": { "center-align": "0.1.3", "right-align": "0.1.3", @@ -2138,8 +2261,7 @@ "wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" } } }, @@ -2377,7 +2499,6 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "dev": true, "requires": { "inherits": "2.0.1", "readable-stream": "2.0.6", @@ -2405,6 +2526,15 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, + "constant-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-1.1.2.tgz", + "integrity": "sha1-jsLKW6ND4Aqjjb9OIA/VrJB+/WM=", + "requires": { + "snake-case": "1.1.2", + "upper-case": "1.1.3" + } + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -2449,8 +2579,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { "version": "4.0.0", @@ -2821,8 +2950,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decorate-component-with-props": { "version": "1.0.2", @@ -3093,6 +3221,14 @@ "domelementtype": "1.3.0" } }, + "dot-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-1.1.2.tgz", + "integrity": "sha1-HnOCaQDeKNbeVIC8HeMdCEKwa+w=", + "requires": { + "sentence-case": "1.1.3" + } + }, "draft-js": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.10.1.tgz", @@ -5447,6 +5583,14 @@ "is-extglob": "1.0.0" } }, + "is-lower-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz", + "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", + "requires": { + "lower-case": "1.1.4" + } + }, "is-my-json-valid": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", @@ -5595,6 +5739,14 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-upper-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz", + "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", + "requires": { + "upper-case": "1.1.3" + } + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -5610,8 +5762,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -5838,7 +5989,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "1.1.5" } @@ -5855,8 +6005,7 @@ "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" }, "lcid": { "version": "1.0.0", @@ -6102,8 +6251,7 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, "loose-envify": { "version": "1.3.1", @@ -6126,8 +6274,15 @@ "lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + }, + "lower-case-first": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-1.0.2.tgz", + "integrity": "sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=", + "requires": { + "lower-case": "1.1.4" + } }, "lru-cache": { "version": "4.1.1", @@ -6558,7 +6713,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/ncname/-/ncname-1.0.0.tgz", "integrity": "sha1-W1etGLHKCShk72Kwse2BlPODtxw=", - "dev": true, "requires": { "xml-char-classes": "1.0.0" } @@ -7186,12 +7340,40 @@ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", "dev": true }, + "pascal-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-1.1.2.tgz", + "integrity": "sha1-Pl1kogBDgwp8STRMLXS0G+DJyZs=", + "requires": { + "camel-case": "1.2.2", + "upper-case-first": "1.1.2" + }, + "dependencies": { + "camel-case": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-1.2.2.tgz", + "integrity": "sha1-Gsp8TRlTWaLOmVV5NDPG5VQlEfI=", + "requires": { + "sentence-case": "1.1.3", + "upper-case": "1.1.3" + } + } + } + }, "path-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", "dev": true }, + "path-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-1.1.2.tgz", + "integrity": "sha1-UM5roNO+090LXCqcRVNpdDRAlRQ=", + "requires": { + "sentence-case": "1.1.3" + } + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -8024,8 +8206,7 @@ "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "progress": { "version": "1.1.8", @@ -8517,28 +8698,60 @@ "prop-types": "15.5.10" } }, - "react-router": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.0.tgz", - "integrity": "sha512-sXlLOg0TRCqnjCVskqBHGjzNjcJKUqXEKnDSuxMYJSPJNq9hROE9VsiIW2kfIq7Ev+20Iz0nxayekXyv0XNmsg==", + "react-router-dom": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.2.2.tgz", + "integrity": "sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==", "requires": { - "create-react-class": "15.6.0", - "history": "3.3.0", - "hoist-non-react-statics": "1.2.0", + "history": "4.7.2", "invariant": "2.2.2", "loose-envify": "1.3.1", "prop-types": "15.5.10", + "react-router": "4.2.0", "warning": "3.0.0" }, "dependencies": { "history": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-3.3.0.tgz", - "integrity": "sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw=", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", + "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", "requires": { "invariant": "2.2.2", "loose-envify": "1.3.1", - "query-string": "4.3.4", + "resolve-pathname": "2.2.0", + "value-equal": "0.4.0", + "warning": "3.0.0" + } + }, + "hoist-non-react-statics": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", + "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + }, + "react-router": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.2.0.tgz", + "integrity": "sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==", + "requires": { + "history": "4.7.2", + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "loose-envify": "1.3.1", + "path-to-regexp": "1.7.0", + "prop-types": "15.5.10", "warning": "3.0.0" } }, @@ -8701,7 +8914,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true, "requires": { "core-util-is": "1.0.2", "inherits": "2.0.1", @@ -8974,8 +9186,7 @@ "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, "remarkable": { "version": "1.7.1", @@ -9022,8 +9233,7 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "repeating": { "version": "2.0.1", @@ -9149,6 +9359,11 @@ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", "dev": true }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -9255,7 +9470,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, "requires": { "align-text": "0.1.4" } @@ -9517,6 +9731,14 @@ } } }, + "sentence-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-1.1.3.tgz", + "integrity": "sha1-gDSq/CFFdy06vhUJqkLJ4QQtwTk=", + "requires": { + "lower-case": "1.1.4" + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -9670,6 +9892,14 @@ "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", "dev": true }, + "snake-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-1.1.2.tgz", + "integrity": "sha1-DC8l4wUVjZoY09l3BmGH/vilpmo=", + "requires": { + "sentence-case": "1.1.3" + } + }, "sntp": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", @@ -10015,8 +10245,7 @@ "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "string-width": { "version": "1.0.2", @@ -10247,6 +10476,15 @@ "whet.extend": "0.9.9" } }, + "swap-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz", + "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", + "requires": { + "lower-case": "1.1.4", + "upper-case": "1.1.3" + } + }, "symbol-observable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", @@ -17976,6 +18214,15 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" }, + "title-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-1.1.2.tgz", + "integrity": "sha1-+uSmrlRr+iLQg6DuqRCkDRLtT1o=", + "requires": { + "sentence-case": "1.1.3", + "upper-case": "1.1.3" + } + }, "tlds": { "version": "1.196.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.196.0.tgz", @@ -18106,8 +18353,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "ua-parser-js": { "version": "0.7.14", @@ -18148,8 +18394,7 @@ "uglify-to-browserify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" }, "uglifyjs-webpack-plugin": { "version": "0.4.6", @@ -18234,8 +18479,15 @@ "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "upper-case-first": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", + "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", + "requires": { + "upper-case": "1.1.3" + } }, "urix": { "version": "0.1.0", @@ -18307,8 +18559,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utila": { "version": "0.4.0", @@ -18338,6 +18589,11 @@ "spdx-expression-parse": "1.0.4" } }, + "value-equal": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -19063,8 +19319,7 @@ "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" }, "wordwrap": { "version": "0.0.3", @@ -19119,8 +19374,7 @@ "xml-char-classes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", - "integrity": "sha1-ZGV4SKIP/F31g6Qq2KJ3tFErvE0=", - "dev": true + "integrity": "sha1-ZGV4SKIP/F31g6Qq2KJ3tFErvE0=" }, "xml-parse-from-string": { "version": "1.0.1", @@ -19160,7 +19414,6 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, "requires": { "camelcase": "1.2.1", "cliui": "2.1.0", diff --git a/package.json b/package.json index a7e675b58..84bd35526 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "start": "webpack-dev-server --history-api-fallback --env=development --hot --inline --progress --port 3000 --host 0.0.0.0", "build": "npm rebuild node-sass; ./node_modules/.bin/webpack --bail --progress --colors --env=production", - "lint": "eslint --fix --format table --ext .js,.jsx .", + "lint": "eslint --format table --ext .js,.jsx .", + "lint:fix": "eslint --fix --format table --ext .js,.jsx .", "test": "cross-env NODE_ENV=test mocha --colors --reporter nyan --compilers js:babel-core/register --require ignore-styles --recursive \"./src/**/*.spec.js\"", "test:watch": "watch-run -i -p 'src/**/*' npm run test" }, @@ -85,7 +86,7 @@ "react-layout-pane": "^0.1.16", "react-modal": "^1.9.7", "react-redux": "^4.4.5", - "react-router": "^3.2.0", + "react-router-dom": "^4.2.2", "react-s-alert": "^1.1.4", "react-scroll": "^1.5.4", "react-stickynode": "^1.2.1", diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 81e97556c..000000000 --- a/src/App.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' - -import browserHistory from 'react-router/lib/browserHistory' -import Router from 'react-router/lib/Router' - -import routes from './routes' -import { TCEmitter } from './helpers' -import { EVENT_ROUTE_CHANGE } from './config/constants' - -const onRouteChange = () => { - TCEmitter.emit(EVENT_ROUTE_CHANGE, window.location.pathname) -} - -export default () => ( - -) diff --git a/src/actions/loadUser.js b/src/actions/loadUser.js index 0df628cb1..23f3e1991 100644 --- a/src/actions/loadUser.js +++ b/src/actions/loadUser.js @@ -71,7 +71,7 @@ export function loadUserSuccess(dispatch, token) { } } }] - if (window.analytics && window.analytics.user()) { + if (window.analytics && window.analytics.user && window.analytics.user()) { const anonymousId = window.analytics.user().anonymousId() if (anonymousId) { analyticsEvents.push({ diff --git a/src/components/AuthenticatedComponent.jsx b/src/components/AuthenticatedComponent.jsx index 4083d4dc6..9dbbbbeb5 100644 --- a/src/components/AuthenticatedComponent.jsx +++ b/src/components/AuthenticatedComponent.jsx @@ -1,7 +1,7 @@ import React from 'react' - -import {getFreshToken} from 'tc-accounts' -import {ACCOUNTS_APP_LOGIN_URL} from '../config/constants' +import { withRouter } from 'react-router-dom' +import { getFreshToken } from 'tc-accounts' +import { ACCOUNTS_APP_LOGIN_URL } from '../config/constants' export function requiresAuthentication(Component) { @@ -23,7 +23,7 @@ export function requiresAuthentication(Component) { }).catch((error) => { console.log(error) // FIXME should we include hash, search etc - const redirectBackToUrl = window.location.origin + '/' + this.props.location.pathname + const redirectBackToUrl = window.location.origin + this.props.location.pathname const newLocation = ACCOUNTS_APP_LOGIN_URL + '?retUrl=' + redirectBackToUrl console.log('redirecting... ', newLocation) window.location = newLocation @@ -43,5 +43,5 @@ export function requiresAuthentication(Component) { } } - return AuthenticatedComponent + return withRouter(AuthenticatedComponent) } diff --git a/src/components/Feed/Feed.jsx b/src/components/Feed/Feed.jsx index b51cc2e51..846de997e 100644 --- a/src/components/Feed/Feed.jsx +++ b/src/components/Feed/Feed.jsx @@ -5,7 +5,7 @@ import ActionCard from '../ActionCard/ActionCard' import Panel from '../Panel/Panel' import FeedComments from './FeedComments' import { Avatar } from 'appirio-tech-react-components' -import {Link} from 'react-router' +import {Link} from 'react-router-dom' import CommentEditToggle from '../ActionCard/CommentEditToggle' import RichTextArea from '../RichTextArea/RichTextArea' diff --git a/src/components/Home/Home.jsx b/src/components/Home/Home.jsx index 00f07d43c..7017c3724 100644 --- a/src/components/Home/Home.jsx +++ b/src/components/Home/Home.jsx @@ -1,6 +1,6 @@ import React from 'react' import { connect } from 'react-redux' -import { withRouter, Link } from 'react-router' +import { withRouter, Link } from 'react-router-dom' import './Home.scss' import homeImgSrc from '../../assets/images/hero-tc-landing.png' import { DOMAIN } from '../../config/constants' @@ -14,7 +14,7 @@ class Home extends React.Component { // redirect to project list if user is logged in. document.title = 'Connect - Topcoder' if (this.props.isLoggedIn) - this.props.router.push('/projects') + this.props.history.push('/projects') } render() { @@ -30,7 +30,7 @@ class Home extends React.Component {

Companies and agencies, from the world’s largest to the Valley’s newest, use crowdsourcing to deliver high-quality assets, faster.

Connect guides you through the entire crowdsourcing process, from entering requirements to receiving final deliverables, and it facilitates collaboration between your project team and Topcoder community members working on your project.

- Start a project + Start a project Learn more about Connect
diff --git a/src/components/ProjectType/ProjectType.jsx b/src/components/ProjectType/ProjectType.jsx index fbcc7a218..f7d87db23 100644 --- a/src/components/ProjectType/ProjectType.jsx +++ b/src/components/ProjectType/ProjectType.jsx @@ -4,7 +4,7 @@ import './ProjectType.scss' import PanelProject from '../PanelProject/PanelProject' import TextTruncate from 'react-text-truncate' import { findCategory } from '../../config/projectWizard' -import {Link} from 'react-router' +import {Link} from 'react-router-dom' const deviceMap = { phone:
Phone
, diff --git a/src/components/TopBar/ProjectToolBar.js b/src/components/TopBar/ProjectToolBar.js index a0f2cd0f4..04d3e2afb 100644 --- a/src/components/TopBar/ProjectToolBar.js +++ b/src/components/TopBar/ProjectToolBar.js @@ -2,7 +2,7 @@ require('./ProjectToolBar.scss') import _ from 'lodash' import React, {PropTypes} from 'react' -import {Link} from 'react-router' +import { NavLink } from 'react-router-dom' import { connect } from 'react-redux' import ReactDOM from 'react-dom' import { @@ -22,16 +22,10 @@ class ProjectToolBar extends React.Component { this.state = { isTooltipVisible: false } - this.onTransition = this.onTransition.bind(this) this.onNameEnter = this.onNameEnter.bind(this) this.onNameLeave = this.onNameLeave.bind(this) } - onTransition() { - // active links in menu are not automatically updated when navigating between project pages - this.forceUpdate() - } - onNameEnter() { const el = ReactDOM.findDOMNode(this.refs.name) if (isEllipsisActive(el)) { @@ -43,35 +37,18 @@ class ProjectToolBar extends React.Component { this.setState({isTooltipVisible: false}) } - componentDidMount() { - const {router} = this.context - router.registerTransitionHook(this.onTransition) - } - - componentWillUnmount() { - const {router} = this.context - router.unregisterTransitionHook(this.onTransition) - - } - render() { // TODO: removing isPowerUser until link challenges is needed once again. const {logo, userMenu, project } = this.props - const {router} = this.context const {isTooltipVisible} = this.state - const getLinkProps = (to) => ({ - to, - className: router.isActive(to, true) ? 'active': '' - }) - return (
{logo} {project &&
- Projects /  + Projects /  {project.name}
} {isTooltipVisible &&
{project.name}
} @@ -79,11 +56,11 @@ class ProjectToolBar extends React.Component {
{project && } @@ -108,10 +85,6 @@ ProjectToolBar.propTypes = { isPowerUser: PropTypes.bool } -ProjectToolBar.contextTypes = { - router: PropTypes.object.isRequired -} - const mapStateToProps = ({ projectState, loadUser }) => { let isPowerUser = false const roles = [ROLE_CONNECT_COPILOT, ROLE_CONNECT_MANAGER, ROLE_ADMINISTRATOR] diff --git a/src/components/TopBar/ProjectsToolBar.js b/src/components/TopBar/ProjectsToolBar.js index c556746ff..e4238d5db 100644 --- a/src/components/TopBar/ProjectsToolBar.js +++ b/src/components/TopBar/ProjectsToolBar.js @@ -2,6 +2,7 @@ require('./ProjectsToolBar.scss') import React, {PropTypes, Component} from 'react' import { connect } from 'react-redux' +import { Prompt, withRouter } from 'react-router-dom' import cn from 'classnames' import _ from 'lodash' import Modal from 'react-modal' @@ -52,7 +53,7 @@ class ProjectsToolBar extends Component { isProjectDirty : false }, () => { this.hideCreateProjectDialog() - this.props.router.push('/projects/' + nextProps.project.id) + this.props.history.push('/projects/' + nextProps.project.id) }) } else { this.setState({ @@ -63,24 +64,17 @@ class ProjectsToolBar extends Component { } componentDidMount() { - const { router, route } = this.props - // sets route leave hook to show unsaved changes alert and persist incomplete project - this.routeLeaveHook = router.setRouteLeaveHook(route, this.onLeave) - // sets window unload hook to show unsaved changes alert and persist incomplete project window.addEventListener('beforeunload', this.onLeave) } componentWillUnmount() { window.removeEventListener('beforeunload', this.onLeave) - if (this.routeLeaveHook) { - this.routeLeaveHook() - } const contentDiv = document.getElementById('wrapper-main') contentDiv.classList.remove('with-filters') } - onLeave(e) { + onLeave(e = {}) { const { isProjectDirty } = this.state const { creatingProject } = this.props if (isProjectDirty && !creatingProject) { @@ -148,7 +142,7 @@ class ProjectsToolBar extends Component { routeWithParams(criteria, page) { // remove any null values criteria = _.pickBy(criteria, _.identity) - this.props.router.push({ + this.props.history.push({ pathname: '/projects/', query: _.assign({}, criteria, { page }) }) @@ -178,9 +172,14 @@ class ProjectsToolBar extends Component { const isLoggedIn = userRoles && userRoles.length const noOfFilters = _.keys(criteria).length - 1 // -1 for default sort criteria + const onLeaveMessage = this.onLeave() || '' return (
+ { - render(( - - - - ), mountNode) +const renderApp = (Component) => { + render( + + + + + + + , + mountNode + ) } -renderApp(App) +renderApp(Routes) -/** - * Warning from React Router, caused by react-hot-loader. - * The warning can be safely ignored, so filter it from the console. - * Otherwise you'll see it every time something changes. - * See https://github.com/gaearon/react-hot-loader/issues/298 - * - * I think if update to react-router it has to disappear - */ if (module.hot) { - const isString = (str) => typeof str === 'string' - const orgError = console.error // eslint-disable-line no-console - console.error = (...args) => { // eslint-disable-line no-console - if (args && args.length === 1 && isString(args[0]) && args[0].indexOf('You cannot change ;') > -1) { - // React route changed - } else { - // Log the error as normally - orgError.apply(console, args) - } - } - - module.hot.accept('./App', () => { - const AppComponent = require('./App').default + module.hot.accept('./routes', () => { + // TODO + // it has to work without explicit require component + // but it doesn't update components even though hot reloading is triggered, why? + const RoutesNew = require('./routes').default - renderApp(AppComponent) + renderApp(RoutesNew) }) } diff --git a/src/indexTest.jsx b/src/indexTest.jsx index cc085ea72..d6602fa87 100644 --- a/src/indexTest.jsx +++ b/src/indexTest.jsx @@ -1,14 +1,15 @@ import 'babel-polyfill' -import React from 'react' -import { Provider } from 'react-redux' -import browserHistory from 'react-router/lib/browserHistory' -import Router from 'react-router/lib/Router' +import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router/lib/Router' import store from './config/store' -import routes from './routes' +import Routes from './routes' export const MemberSearchApp = () => ( - + + + ) diff --git a/src/projects/create/components/FillProjectDetails.js b/src/projects/create/components/FillProjectDetails.js index 50c8812fb..cf401023d 100644 --- a/src/projects/create/components/FillProjectDetails.js +++ b/src/projects/create/components/FillProjectDetails.js @@ -1,6 +1,6 @@ import _ from 'lodash' import React, { PropTypes as PT, Component } from 'react' -import { Link } from 'react-router' +import { Link } from 'react-router-dom' import Sticky from 'react-stickynode' import config from '../../../config/projectWizard' diff --git a/src/projects/create/components/IncompleteProjectConfirmation.js b/src/projects/create/components/IncompleteProjectConfirmation.js index def957993..dfc29c904 100644 --- a/src/projects/create/components/IncompleteProjectConfirmation.js +++ b/src/projects/create/components/IncompleteProjectConfirmation.js @@ -1,5 +1,5 @@ import React, { PropTypes as PT } from 'react' -import { Link } from 'react-router' +import { Link } from 'react-router-dom' import SVGIconImage from '../../../components/SVGIconImage' import './IncompleteProjectConfirmation.scss' diff --git a/src/projects/create/components/SelectProduct.js b/src/projects/create/components/SelectProduct.js index 03965f20d..f269e6b96 100644 --- a/src/projects/create/components/SelectProduct.js +++ b/src/projects/create/components/SelectProduct.js @@ -1,5 +1,5 @@ import React, { PropTypes as PT } from 'react' -import { Link } from 'react-router' +import { Link } from 'react-router-dom' import config from '../../../config/projectWizard' import ProductCard from './ProductCard' import SVGIconImage from '../../../components/SVGIconImage' diff --git a/src/projects/create/components/SelectProjectType.jsx b/src/projects/create/components/SelectProjectType.jsx index fcd756b87..f6c88371a 100644 --- a/src/projects/create/components/SelectProjectType.jsx +++ b/src/projects/create/components/SelectProjectType.jsx @@ -1,5 +1,5 @@ import React, { PropTypes as PT } from 'react' -import { Link } from 'react-router' +import { Link } from 'react-router-dom' import config from '../../../config/projectWizard' import ProjectTypeCard from './ProjectTypeCard' import SVGIconImage from '../../../components/SVGIconImage' diff --git a/src/projects/create/containers/CreateContainer.jsx b/src/projects/create/containers/CreateContainer.jsx index a14443d1e..d3d6ae324 100644 --- a/src/projects/create/containers/CreateContainer.jsx +++ b/src/projects/create/containers/CreateContainer.jsx @@ -1,12 +1,13 @@ import _ from 'lodash' import React, { PropTypes } from 'react' -import { withRouter, browserHistory } from 'react-router' +import { withRouter } from 'react-router-dom' import { connect } from 'react-redux' import { renderComponent, branch, compose, withProps } from 'recompose' import { createProjectWithStatus as createProjectAction, fireProjectDirty, fireProjectDirtyUndo } from '../../actions/project' import CoderBot from '../../../components/CoderBot/CoderBot' import spinnerWhileLoading from '../../../components/LoadingSpinner' import ProjectWizard from '../components/ProjectWizard' +import { findCategory, findProductCategory } from '../../../config/projectWizard' import { CREATE_PROJECT_FAILURE, LS_INCOMPLETE_PROJECT, @@ -33,8 +34,8 @@ const spinner = spinnerWhileLoading(props => !props.processing) const enhance = compose(errorHandler, spinner) const CreateView = (props) => { - const { route } = props - if (route.path === '/new-project-callback') { + const { match } = props + if (match.path === '/new-project-callback') { // can do some fancy loading (e.g. coderbot animation) here return
} @@ -70,21 +71,42 @@ class CreateConainer extends React.Component { // remove incomplete project, and navigate to project dashboard console.log('removing incomplete project') window.localStorage.removeItem(LS_INCOMPLETE_PROJECT) - this.props.router.push('/projects/' + projectId) + this.props.history.push('/projects/' + projectId) }) } else if (this.state.creatingProject !== nextProps.processing) { this.setState({ creatingProject : nextProps.processing }) } + + // when route is changed, save incomplete project + if (this.props.location.pathname !== nextProps.location.pathname) { + this.onLeave() + } } componentWillMount() { - const { processing, userRoles, route } = this.props + const { processing, userRoles, match, history } = this.props + // if we are on the project page validate product param + if (match.path === '/new-project/:product?') { + const product = match.params.product + // first try the path param to be a project category + let productCategory = findCategory(product) + // if it is not a category, it should be a product and we should be able to find a category for it + productCategory = !productCategory ? findProductCategory(product) : productCategory + if (product && product.trim().length > 0 && !productCategory) { + // workaround to add URL for incomplete project confirmation step + // ideally we should have better URL naming which resolves each route with distinct patterns + if (product !== 'incomplete') { + history.replace('/404') + } + } + } + // load incomplete project from local storage const incompleteProjectStr = window.localStorage.getItem(LS_INCOMPLETE_PROJECT) if(incompleteProjectStr) { const incompleteProject = JSON.parse(incompleteProjectStr) - if (route.path === '/new-project-callback' && !processing && userRoles && userRoles.length > 0) { + if (match.path === '/new-project-callback' && !processing && userRoles && userRoles.length > 0) { // if project wizard is loaded after redirection from register page // TODO should we validate the project again? console.log('calling createProjectAction...') @@ -94,15 +116,14 @@ class CreateConainer extends React.Component { } componentDidMount() { - // sets route leave hook to show unsaved changes alert and persist incomplete project - this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) - - // sets window unload hook to show unsaved changes alert and persist incomplete project + // sets window unload hook save incomplete project window.addEventListener('beforeunload', this.onLeave) } componentWillUnmount() { window.removeEventListener('beforeunload', this.onLeave) + // when we leave component, save incomplete project + this.onLeave() } // stores the incomplete project in local storage @@ -148,16 +169,16 @@ class CreateConainer extends React.Component { const projectType = _.get(updatedProject, 'type', null) const product = _.get(updatedProject, 'details.products[0]', null) if (wizardStep === ProjectWizard.Steps.WZ_STEP_INCOMP_PROJ_CONF) { - browserHistory.push(NEW_PROJECT_PATH + '/incomplete') + this.props.history.push(NEW_PROJECT_PATH + '/incomplete') } if (wizardStep === ProjectWizard.Steps.WZ_STEP_SELECT_PROJ_TYPE) { - browserHistory.push(NEW_PROJECT_PATH + '/' + window.location.search) + this.props.history.push(NEW_PROJECT_PATH + '/' + window.location.search) } if (projectType && wizardStep === ProjectWizard.Steps.WZ_STEP_SELECT_PROD_TYPE) { - browserHistory.push(NEW_PROJECT_PATH + '/' + projectType + window.location.search) + this.props.history.push(NEW_PROJECT_PATH + '/' + projectType + window.location.search) } if (projectType && product && wizardStep === ProjectWizard.Steps.WZ_STEP_FILL_PROJ_DETAILS) { - browserHistory.push(NEW_PROJECT_PATH + '/' + product + window.location.search) + this.props.history.push(NEW_PROJECT_PATH + '/' + product + window.location.search) } this.setState({ wizardStep @@ -171,7 +192,7 @@ class CreateConainer extends React.Component { // compares updated product with previous product to know if user has updated the product if (prevProduct !== product) { if (product) { - browserHistory.push(NEW_PROJECT_PATH + '/' + product + window.location.search) + this.props.history.push(NEW_PROJECT_PATH + '/' + product + window.location.search) } } this.setState({ diff --git a/src/projects/detail/ProjectDetail.jsx b/src/projects/detail/ProjectDetail.jsx index 9abd86a33..902410038 100644 --- a/src/projects/detail/ProjectDetail.jsx +++ b/src/projects/detail/ProjectDetail.jsx @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from 'react' -import { withRouter } from 'react-router' +import { withRouter } from 'react-router-dom' import { connect } from 'react-redux' import _ from 'lodash' import { renderComponent, branch, compose, withProps } from 'recompose' @@ -39,14 +39,14 @@ class ProjectDetail extends Component { } componentWillMount() { - const projectId = this.props.params.projectId + const projectId = this.props.match.params.projectId this.props.loadProjectDashboard(projectId) } componentWillReceiveProps({isProcessing, isLoading, error, project}) { // handle just deleted projects if (! (error || isLoading || isProcessing) && _.isEmpty(project)) - this.props.router.push('/projects/') + this.props.history.push('/projects/') if (project && project.name) { document.title = `${project.name} - Topcoder` } diff --git a/src/projects/detail/components/EditProjectForm.jsx b/src/projects/detail/components/EditProjectForm.jsx index 9a60abb3b..f6cccbd02 100644 --- a/src/projects/detail/components/EditProjectForm.jsx +++ b/src/projects/detail/components/EditProjectForm.jsx @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react' -import { withRouter } from 'react-router' +import { Prompt } from 'react-router-dom' import Modal from 'react-modal' import _ from 'lodash' import { unflatten } from 'flat' @@ -96,7 +96,6 @@ class EditProjectForm extends Component { } componentDidMount() { - this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) window.addEventListener('beforeunload', this.onLeave) } @@ -106,7 +105,7 @@ class EditProjectForm extends Component { } // Notify user if they navigate away while the form is modified. - onLeave(e) { + onLeave(e = {}) { if (this.isChanged()) { // TODO: remove this block - it disables unsaved changes popup // for app screens changes @@ -188,6 +187,7 @@ class EditProjectForm extends Component { render() { const { isEdittable, sections } = this.props const { project, dirtyProject } = this.state + const onLeaveMessage = this.onLeave() || '' const renderSection = (section, idx) => { const anySectionInvalid = _.some(this.props.sections, (s) => s.isInvalid) return ( @@ -213,6 +213,10 @@ class EditProjectForm extends Component { return (
+ { - browserHistory.push(`/projects/${project.id}`) + this.props.history.push(`/projects/${project.id}`) }) } @@ -166,4 +166,4 @@ ProjectSpecSidebar.PropTypes = { } const mapDispatchToProps = { updateProject } -export default connect(null, mapDispatchToProps)(ProjectSpecSidebar) +export default withRouter(connect(null, mapDispatchToProps)(ProjectSpecSidebar)) diff --git a/src/projects/detail/containers/FeedContainer.js b/src/projects/detail/containers/FeedContainer.js index 353adbd22..19daa8ac7 100644 --- a/src/projects/detail/containers/FeedContainer.js +++ b/src/projects/detail/containers/FeedContainer.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react' -import { withRouter } from 'react-router' +import { Prompt } from 'react-router-dom' import _ from 'lodash' import { THREAD_MESSAGES_PAGE_SIZE, @@ -51,9 +51,7 @@ class FeedView extends React.Component { } componentDidMount() { - const routeLeaveHook = this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) window.addEventListener('beforeunload', this.onLeave) - this.setState({ routeLeaveHook }) } componentWillMount() { @@ -65,14 +63,11 @@ class FeedView extends React.Component { } componentWillUnmount() { - if (this.state.routeLeaveHook) { - this.state.routeLeaveHook() - } window.removeEventListener('beforeunload', this.onLeave) } // Notify user if they navigate away while the form is modified. - onLeave(e) { + onLeave(e = {}) { if (this.isChanged()) { return e.returnValue = 'You haven\'t posted your message. If you leave this page, your message will not be saved. Are you sure you want to leave?' } @@ -345,6 +340,7 @@ class FeedView extends React.Component { const {currentUser, project, currentMemberRole, isCreatingFeed, error } = this.props const { feeds } = this.state const showDraftSpec = project.status === PROJECT_STATUS_DRAFT && currentMemberRole === PROJECT_ROLE_CUSTOMER + const onLeaveMessage = this.onLeave() || '' const renderFeed = (item) => { if ((item.spec || item.sendForReview) && !showDraftSpec) { @@ -380,6 +376,10 @@ class FeedView extends React.Component { } return (
+ !props.isLoading) -const EnhancedFeedView = withRouter(enhance(FeedView)) +const EnhancedFeedView = enhance(FeedView) class FeedContainer extends React.Component { diff --git a/src/projects/detail/containers/MessagesContainer.js b/src/projects/detail/containers/MessagesContainer.js index b561c11d0..7c14b43e2 100644 --- a/src/projects/detail/containers/MessagesContainer.js +++ b/src/projects/detail/containers/MessagesContainer.js @@ -1,6 +1,6 @@ import _ from 'lodash' import React from 'react' -import { withRouter } from 'react-router' +import { Prompt, withRouter } from 'react-router-dom' import { connect } from 'react-redux' import update from 'react-addons-update' import MessageList from '../../../components/MessageList/MessageList' @@ -57,9 +57,7 @@ class MessagesView extends React.Component { } componentDidMount() { - const routeLeaveHook = this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) window.addEventListener('beforeunload', this.onLeave) - this.setState({ routeLeaveHook }) } componentWillMount() { @@ -72,13 +70,10 @@ class MessagesView extends React.Component { componentWillUnmount() { window.removeEventListener('beforeunload', this.onLeave) - if (this.state.routeLeaveHook) { - this.state.routeLeaveHook() - } } // Notify user if they navigate away while the form is modified. - onLeave(e) { + onLeave(e = {}) { if (this.isChanged()) { return e.returnValue = 'You haven\'t posted your message. If you leave this page, your message will not be saved. Are you sure you want to leave?' } @@ -275,7 +270,7 @@ class MessagesView extends React.Component { return item }) }, () => { - this.props.router.push(`/projects/${this.props.project.id}/discussions/${thread.id}`) + this.props.history.push(`/projects/${this.props.project.id}/discussions/${thread.id}`) }) } @@ -418,6 +413,7 @@ class MessagesView extends React.Component { const {threads, isCreateNewMessage, showEmptyState, scrollPosition} = this.state const { currentUser, isCreatingFeed, currentMemberRole, error } = this.props const activeThread = threads.filter((item) => item.isActive)[0] + const onLeaveMessage = this.onLeave() || '' const renderRightPanel = () => { if (!!currentMemberRole && (isCreateNewMessage || !threads.length)) { return ( @@ -457,6 +453,10 @@ class MessagesView extends React.Component { return ( +
m.userId === this.props.currentUser.userId) === -1 ) { // navigate to project listing - this.props.router.push('/projects/') + this.props.history.push('/projects/') } } diff --git a/src/projects/list/components/Projects/Projects.jsx b/src/projects/list/components/Projects/Projects.jsx index 8919c6941..95bfdf306 100755 --- a/src/projects/list/components/Projects/Projects.jsx +++ b/src/projects/list/components/Projects/Projects.jsx @@ -1,9 +1,10 @@ import React, { Component } from 'react' import { connect } from 'react-redux' -import { withRouter } from 'react-router' +import { withRouter } from 'react-router-dom' import ProjectsView from './ProjectsView' import { loadProjects } from '../../../actions/loadProjects' import _ from 'lodash' +import querystring from 'query-string' class Projects extends Component { constructor(props) { @@ -77,9 +78,9 @@ class Projects extends Component { routeWithParams(criteria, page) { // remove any null values criteria = _.pickBy(criteria, _.identity) - this.props.router.push({ - pathname: '/projects/', - query: _.assign({}, criteria, { page }) + this.props.history.push({ + pathname: '/projects', + search: '?' + querystring.stringify(_.assign({}, criteria, { page })) }) this.props.loadProjects(criteria, page) } diff --git a/src/projects/list/components/Projects/ProjectsView.jsx b/src/projects/list/components/Projects/ProjectsView.jsx index fd27f4556..b6cc5f750 100644 --- a/src/projects/list/components/Projects/ProjectsView.jsx +++ b/src/projects/list/components/Projects/ProjectsView.jsx @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react' import _ from 'lodash' -import { Link } from 'react-router' +import { Link } from 'react-router-dom' import { branch, renderComponent, compose } from 'recompose' import moment from 'moment' import classNames from 'classnames' diff --git a/src/projects/list/components/ProjectsTopBar/ProjectsTopBar.js b/src/projects/list/components/ProjectsTopBar/ProjectsTopBar.js index c48f10803..155085fb6 100644 --- a/src/projects/list/components/ProjectsTopBar/ProjectsTopBar.js +++ b/src/projects/list/components/ProjectsTopBar/ProjectsTopBar.js @@ -1,12 +1,12 @@ import React from 'react' import './ProjectsTopBar.scss' -import {Link} from 'react-router' +import {Link} from 'react-router-dom' const ProjectsTopBar = () => (
- + New Project + + New Project All Projects
diff --git a/src/projects/routes.jsx b/src/projects/routes.jsx index 4dd4c7f0f..a754eddb2 100644 --- a/src/projects/routes.jsx +++ b/src/projects/routes.jsx @@ -1,6 +1,7 @@ import React from 'react' -import { Route, IndexRoute } from 'react-router' - +import { Route, Switch } from 'react-router-dom' +import { withProps } from 'recompose' +import App from '../components/App/App' import ProjectLayout from './ProjectLayout' import Projects from './list/components/Projects/Projects' import TopBarContainer from '../components/TopBar/TopBarContainer' @@ -12,18 +13,38 @@ import ProjectMessages from './detail/Messages' import SpecificationContainer from './detail/containers/SpecificationContainer' import { requiresAuthentication } from '../components/AuthenticatedComponent' +const renderApp = (topbar, content) => () => ( + +) + +const ProjectLayoutWithAuth = requiresAuthentication(ProjectLayout) + +// NOTE: +// we cannot move up ProjectDetail component +// we have to keep it like it's done below because +// Dashboard, SpecificationContainer and ProjectMessages have to be immediate children +// of ProjectDetail component because ProjectDetail updates children props by React.Children method +const ProjectDetailWithAuth = withProps({ main: + + } /> + } /> + } /> + } /> + +})(ProjectLayoutWithAuth) + +const ProjectsWithAuth = withProps({ main: })(ProjectLayoutWithAuth) const projectRoutes = ( - , content: requiresAuthentication(ProjectLayout)}}> - // TODO add project topbar - - - - // - - - - + ( + + , )} /> + , )} /> + + )} + /> ) export default projectRoutes diff --git a/src/reports/routes.jsx b/src/reports/routes.jsx index e82d55ddc..fb6b2d01b 100644 --- a/src/reports/routes.jsx +++ b/src/reports/routes.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { Route } from 'react-router' +import { Route } from 'react-router-dom' import ReportDashboard from './components/dashboard/ReportDashboard' diff --git a/src/routes.jsx b/src/routes.jsx index 1d0c76ab2..9de14caf7 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { Route, IndexRoute, browserHistory } from 'react-router' +import { Route, Switch, withRouter } from 'react-router-dom' import { withProps } from 'recompose' import App from './components/App/App' import Home from './components/Home/Home' @@ -10,112 +10,136 @@ import TopBarContainer from './components/TopBar/TopBarContainer' import ProjectsToolBar from './components/TopBar/ProjectsToolBar' import RedirectComponent from './components/RedirectComponent' import CreateContainer from './projects/create/containers/CreateContainer' -import { findCategory, findProductCategory } from './config/projectWizard' +import LoadingIndicator from './components/LoadingIndicator/LoadingIndicator' import {ACCOUNTS_APP_LOGIN_URL, PROJECT_FEED_TYPE_PRIMARY, PROJECT_FEED_TYPE_MESSAGES } from './config/constants' import { getTopic } from './api/messages' import { getFreshToken } from 'tc-accounts' -// import reportsListRoutes from './reports/routes.jsx' +import { TCEmitter } from './helpers' +import { EVENT_ROUTE_CHANGE } from './config/constants' + +const onRouteChange = (pathname) => { + TCEmitter.emit(EVENT_ROUTE_CHANGE, pathname) -// Tracking -browserHistory.listen(location => { if (window.analytics) { - if (/^projects\/$/.test(location.pathname)) { + if (/^projects\/$/.test(pathname)) { window.analytics.page('Project Listings') - } else if (/^projects\/\d+\/?$/.test(location.pathname)) { + } else if (/^projects\/\d+\/?$/.test(pathname)) { window.analytics.page('Project Dashboard') - } else if (/^projects\/\d+\/discussions\/?$/.test(location.pathname)) { + } else if (/^projects\/\d+\/discussions\/?$/.test(pathname)) { window.analytics.page('Project Discussions') - } else if (/^projects\/\d+\/specification\/?$/.test(location.pathname)) { + } else if (/^projects\/\d+\/specification\/?$/.test(pathname)) { window.analytics.page('Project Specification') - } else if (/^\/$/.test(location.pathname)) { + } else if (/^\/$/.test(pathname)) { window.analytics.page('Connect Home') - } else if (/^new-project\/$/.test(location.pathname)) { + } else if (/^new-project\/$/.test(pathname)) { window.analytics.page('New Project : Select Product') - } else if (/^new-project\/incomplete$/.test(location.pathname)) { + } else if (/^new-project\/incomplete$/.test(pathname)) { window.analytics.page('New Project : Incomplete Project') - } else if (/^new-project\/[a-zA-Z0-9\_]+$/.test(location.pathname)) { + } else if (/^new-project\/[a-zA-Z0-9\_]+$/.test(pathname)) { window.analytics.page('New Project : Project Details') } } -}) - -const LoginRedirect = withProps({ - redirectTo: `${ACCOUNTS_APP_LOGIN_URL}?retUrl=${window.location.protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}` -})(RedirectComponent) +} -const redirectToConnect = (nextState, replace, callback) => { - if(window.location.hostname.indexOf('connectv2') === 0) { +const redirectToConnectIfNeed = () => { + if (window.location.hostname.indexOf('connectv2') === 0) { window.location.assign(window.location.href.replace('connectv2', 'connect')) return } - callback() } -const redirectToProject = (nextState, replace, callback) => { - const feedId = nextState.params.feedId - getFreshToken().then(() => { - getTopic(feedId).then(resp => { - if (resp.topics && resp.topics.length > 0) { - const topic = resp.topics[0] - const projectId = topic.referenceId - if (topic.tag === PROJECT_FEED_TYPE_PRIMARY) { - replace(`/projects/${projectId}/`) - } else if (topic.tag === PROJECT_FEED_TYPE_MESSAGES) { - replace({ - pathname: `/projects/${projectId}/discussions/${topic.id}` - }) - } else { - replace('/projects') +const LoginRedirect = withProps({ + redirectTo: `${ACCOUNTS_APP_LOGIN_URL}?retUrl=${window.location.protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}` +})(RedirectComponent) + +class RedirectToProject extends React.Component { + componentWillMount() { + const { match, history } = this.props + const feedId = match.params.feedId + getFreshToken().then(() => { + getTopic(feedId).then(resp => { + if (resp.topics && resp.topics.length > 0) { + const topic = resp.topics[0] + const projectId = topic.referenceId + if (topic.tag === PROJECT_FEED_TYPE_PRIMARY) { + history.replace(`/projects/${projectId}/`) + } else if (topic.tag === PROJECT_FEED_TYPE_MESSAGES) { + history.replace({ + pathname: `/projects/${projectId}/discussions/${topic.id}` + }) + } else { + history.replace('/projects') + } } - } - callback() + }) + .catch(() => { + history.replace('/projects') + }) + }).catch(() => { + // FIXME should we include hash, search etc + const redirectBackToUrl = window.location.origin + '/' + match.location.pathname + const newLocation = ACCOUNTS_APP_LOGIN_URL + '?retUrl=' + redirectBackToUrl + window.location = newLocation }) - .catch(() => { - replace('/projects') - callback() - }) - }).catch(() => { - // FIXME should we include hash, search etc - const redirectBackToUrl = window.location.origin + '/' + nextState.location.pathname - const newLocation = ACCOUNTS_APP_LOGIN_URL + '?retUrl=' + redirectBackToUrl - window.location = newLocation - }) + } + + render() { + return + } } -const validateCreateProjectParams = (nextState, replace, callback) => { - const product = nextState.params.product - // first try the path param to be a project category - let productCategory = findCategory(product) - // if it is not a category, it should be a product and we should be able to find a category for it - productCategory = !productCategory ? findProductCategory(product) : productCategory - if (product && product.trim().length > 0 && !productCategory) { - // workaround to add URL for incomplete project confirmation step - // ideally we should have better URL naming which resolves each route with distinct patterns - if (product !== 'incomplete') { - replace('/404') +const topBarWithProjectsToolBar = + +const renderApp = (topbar, content) => () => ( + +) + +class Routes extends React.Component { + componentWillMount() { + redirectToConnectIfNeed() + } + + componentDidMount() { + const pathname = this.props.location.pathname + onRouteChange(pathname) + } + + componentWillReceiveProps(nextProps) { + const currentPathname = this.props.location.pathname + const nextPathname = nextProps.location.pathname + + if (currentPathname !== nextPathname) { + onRouteChange(nextPathname) } } - callback() + + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + window.scrollTo(0, 0) + } + } + + render() { + return ( + + )} /> + )} /> + )} /> + )} /> + )} /> + + + {/* Handle /projects/* routes */} + {projectRoutes} + {/* {reportsListRoutes} */} + + )} /> + )} /> + )} /> + + ) + } } -const renderTopBarWithProjectsToolBar = (props) => - -export default ( - window.scrollTo(0, 0)} component={ App } onEnter={ redirectToConnect }> - - - - - - - - {/* Handle /projects/* routes */} - {projectRoutes} - {/* {reportsListRoutes} */} - - }} /> - }} /> - }} /> - -) +export default withRouter(Routes)