From 0afddee243f9c81576de981d66e1495118fe708e Mon Sep 17 00:00:00 2001
From: Alf Eaton <eaton.alf@gmail.com>
Date: Mon, 4 Sep 2017 10:35:58 +0100
Subject: [PATCH] Add initial review component

---
 packages/component-review/package.json        |  64 ++++++++++
 .../component-review/src/components/Review.js |  46 +++++++
 .../src/components/Review.local.scss          |   0
 .../component-review/src/components/Review.md |  50 ++++++++
 .../src/components/ReviewLayout.js            |  31 +++++
 .../src/components/ReviewLayout.local.scss    |  14 +++
 .../src/components/ReviewPage.js              |  86 ++++++++++++++
 .../component-review/src/components/index.js  |   1 +
 packages/component-review/src/index.js        |   7 ++
 .../component-review/src/lib/validators.js    |  34 ++++++
 .../component-review/styleguide.config.js     |  21 ++++
 packages/component-review/webpack.config.js   | 112 ++++++++++++++++++
 .../xpub-collabra/app/config/journal/index.js |   1 +
 .../app/config/journal/recommendations.js     |  14 +++
 packages/xpub-collabra/app/routes.js          |  17 ++-
 packages/xpub-collabra/config/components.json |   1 +
 packages/xpub-collabra/package.json           |   1 +
 17 files changed, 496 insertions(+), 4 deletions(-)
 create mode 100644 packages/component-review/package.json
 create mode 100644 packages/component-review/src/components/Review.js
 create mode 100644 packages/component-review/src/components/Review.local.scss
 create mode 100644 packages/component-review/src/components/Review.md
 create mode 100644 packages/component-review/src/components/ReviewLayout.js
 create mode 100644 packages/component-review/src/components/ReviewLayout.local.scss
 create mode 100644 packages/component-review/src/components/ReviewPage.js
 create mode 100644 packages/component-review/src/components/index.js
 create mode 100644 packages/component-review/src/index.js
 create mode 100644 packages/component-review/src/lib/validators.js
 create mode 100644 packages/component-review/styleguide.config.js
 create mode 100644 packages/component-review/webpack.config.js
 create mode 100644 packages/xpub-collabra/app/config/journal/recommendations.js

diff --git a/packages/component-review/package.json b/packages/component-review/package.json
new file mode 100644
index 000000000..0062460cd
--- /dev/null
+++ b/packages/component-review/package.json
@@ -0,0 +1,64 @@
+{
+  "name": "pubsweet-component-xpub-review",
+  "version": "0.0.2",
+  "main": "src",
+  "author": "Collaborative Knowledge Foundation",
+  "license": "MIT",
+  "files": [
+    "src",
+    "dist"
+  ],
+  "dependencies": {
+    "classnames": "^2.2.5",
+    "lodash": "^4.17.4",
+    "prop-types": "^15.5.10",
+    "pubsweet-client": "git+https://gitlab.coko.foundation/pubsweet/pubsweet-client.git",
+    "pubsweet-component-wax": "^0.1.0",
+    "pubsweet-component-xpub-app": "^0.0.2",
+    "react": "^15.6.1",
+    "react-dom": "^15.6.1",
+    "react-redux": "^5.0.2",
+    "react-router": "^3.0.5",
+    "react-router-redux": "^4.0.7",
+    "recompose": "^0.25.0",
+    "redux": "^3.6.0",
+    "redux-form": "^7.0.3",
+    "striptags": "^3.1.0",
+    "xpub-edit": "^0.0.2",
+    "xpub-selectors": "^0.0.2",
+    "xpub-ui": "^0.0.2"
+  },
+  "devDependencies": {
+    "babel-core": "^6.26.0",
+    "babel-loader": "^7.1.2",
+    "babel-preset-env": "^1.6.0",
+    "babel-preset-react": "^6.24.1",
+    "babel-preset-stage-2": "^6.24.1",
+    "css-loader": "^0.28.4",
+    "faker": "^4.1.0",
+    "file-loader": "^0.11.2",
+    "node-sass": "^4.5.3",
+    "react-styleguidist": "^6.0.8",
+    "sass-loader": "^6.0.6",
+    "style-loader": "^0.18.2",
+    "webpack": "^3.5.5",
+    "webpack-node-externals": "^1.6.0",
+    "xpub-styleguide": "^0.0.2"
+  },
+  "peerDependencies": {
+    "prop-types": "^15.5.10",
+    "pubsweet-client": "git+https://gitlab.coko.foundation/pubsweet/pubsweet-client.git",
+    "react": "^15.6.1",
+    "react-dom": "^15.6.1",
+    "react-redux": "^5.0.2",
+    "react-router": "^3.0.5"
+  },
+  "scripts": {
+    "styleguide": "styleguidist server",
+    "styleguide:build": "styleguidist build",
+    "clean": "rimraf dist",
+    "lint": "eslint src",
+    "prebuild": "npm run clean && npm run lint",
+    "build": "webpack --progress --profile"
+  }
+}
diff --git a/packages/component-review/src/components/Review.js b/packages/component-review/src/components/Review.js
new file mode 100644
index 000000000..39d86e47b
--- /dev/null
+++ b/packages/component-review/src/components/Review.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import { Button } from 'xpub-ui'
+import { NoteEditor } from 'xpub-edit'
+import { Field } from 'redux-form'
+import { required } from '../lib/validators'
+import { RadioGroup, ValidatedField } from 'xpub-ui'
+import classes from './Review.local.scss'
+
+const Review = ({ journal, review, valid, pristine, submitting, handleSubmit, uploadFile }) => (
+  <form onSubmit={handleSubmit}>
+    <div className={classes.section}>
+      <Field
+        name="note"
+        validate={[required]}
+        component={props =>
+          <ValidatedField {...props.meta}>
+            <NoteEditor
+              placeholder="Enter your review…"
+              title="Review"
+              {...props.input}/>
+          </ValidatedField>
+        }/>
+    </div>
+
+    <div className={classes.section}>
+      <Field
+        name="recommendation"
+        validate={[required]}
+        component={props =>
+          <ValidatedField {...props.meta}>
+            <RadioGroup
+              inline
+              options={journal.recommendations}
+              {...props.input}/>
+          </ValidatedField>
+        }/>
+    </div>
+
+    <div>
+      {/*<Button type="button" onClick={handleSave}>Save</Button>*/}
+      <Button type="submit" primary>Submit</Button>
+    </div>
+  </form>
+)
+
+export default Review
diff --git a/packages/component-review/src/components/Review.local.scss b/packages/component-review/src/components/Review.local.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/component-review/src/components/Review.md b/packages/component-review/src/components/Review.md
new file mode 100644
index 000000000..fcbda70b9
--- /dev/null
+++ b/packages/component-review/src/components/Review.md
@@ -0,0 +1,50 @@
+A form for entering a review of a version of a project.
+
+```js
+const { reduxForm } = require('redux-form');
+
+const project = {
+  id: faker.random.uuid(),
+};
+
+const version = {
+  id: faker.random.uuid(),
+  metadata: {
+    keywords: ['foo', 'bar']
+  }
+};
+
+const review = {
+  id: faker.random.uuid(),
+  note: '<p>This is a review</p>',
+  recommendation: 'accept'
+};
+
+const journal = {
+  recommendations: [
+    {
+      value: 'accept',
+      label: 'Accept',
+    },
+    {
+      value: 'revise',
+      label: 'Revise',
+    },
+    {
+      value: 'reject',
+      label: 'Reject',
+    }
+  ]
+}
+
+const ReviewForm = reduxForm({ 
+  form: 'review',
+  onSubmit: values => console.log(values),
+  onChange: values => console.log(values)
+})(Review);
+
+<ReviewForm
+  version={version}
+  journal={journal}
+  initialValues={review}/>
+```
diff --git a/packages/component-review/src/components/ReviewLayout.js b/packages/component-review/src/components/ReviewLayout.js
new file mode 100644
index 000000000..01217b103
--- /dev/null
+++ b/packages/component-review/src/components/ReviewLayout.js
@@ -0,0 +1,31 @@
+import React from 'react'
+// import classnames from 'classnames'
+// import SimpleEditor from 'pubsweet-component-wax/src/SimpleEditor'
+import classes from './ReviewLayout.local.scss'
+import Review from './Review'
+
+const ReviewLayout = ({ journal, project, version, review, valid, pristine, submitting, handleSubmit, uploadFile }) => (
+  <div className={classes.root}>
+    <div className={classes.column}>
+      {/*<SimpleEditor
+        book={project}
+        fragment={version}
+      />*/}
+    </div>
+
+    <div className={classes.column}>
+      <table className={classes.metadata}>
+        <tbody>
+        <tr>
+          <th>Keywords</th>
+          <td>{version.metadata.keywords.join(',')}</td>
+        </tr>
+        </tbody>
+      </table>
+
+      <Review valid={valid} handleSubmit={handleSubmit} uploadFile={uploadFile}/>
+    </div>
+  </div>
+)
+
+export default ReviewLayout
diff --git a/packages/component-review/src/components/ReviewLayout.local.scss b/packages/component-review/src/components/ReviewLayout.local.scss
new file mode 100644
index 000000000..2c67a3dff
--- /dev/null
+++ b/packages/component-review/src/components/ReviewLayout.local.scss
@@ -0,0 +1,14 @@
+.root {
+  display: flex;
+  position: absolute;
+  top: 50px;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+}
+
+.column {
+  height: 100%;
+  overflow-y: auto;
+}
diff --git a/packages/component-review/src/components/ReviewPage.js b/packages/component-review/src/components/ReviewPage.js
new file mode 100644
index 000000000..45b7fee17
--- /dev/null
+++ b/packages/component-review/src/components/ReviewPage.js
@@ -0,0 +1,86 @@
+/* global CONFIG */
+
+import { debounce } from 'lodash'
+import { compose, withProps } from 'recompose'
+import { connect } from 'react-redux'
+import { push } from 'react-router-redux'
+import { reduxForm, SubmissionError } from 'redux-form'
+import actions from 'pubsweet-client/src/actions'
+import token from 'pubsweet-client/src/helpers/token'
+import { withJournal, ConnectPage } from 'pubsweet-component-xpub-app/src/components'
+import { selectCollection, selectFragment } from 'xpub-selectors'
+import Review from './Review'
+
+const onSubmit = (values, dispatch, props) => {
+  console.log('submit', values)
+
+  return dispatch(actions.updateFragment(props.project, {
+    id: props.review.id,
+    submitted: true, // TODO: current date?
+    ...values
+  })).then(() => {
+    // TODO: show "thanks for your review" message
+    dispatch(push(`/`))
+  }).catch(error => {
+    if (error.validationErrors) {
+      throw new SubmissionError()
+    }
+  })
+}
+
+const onChange = (values, dispatch, props) => {
+  console.log('change', values)
+
+  return dispatch(actions.updateFragment(props.project, {
+    id: props.review.id,
+    // submitted: false,
+    ...values
+  }))
+
+  // TODO: display a notification when saving/saving completes/saving fails
+}
+
+const uploadFile = file => dispatch => {
+  // TODO: import the endpoint URL from a client module
+  const API_ENDPOINT = CONFIG['pubsweet-server'].API_ENDPOINT
+
+  const data = new FormData()
+  data.append('file', file)
+
+  const request = new XMLHttpRequest()
+  request.open('POST', API_ENDPOINT + '/upload')
+  request.setRequestHeader('Authorization', 'Bearer ' + token())
+  request.setRequestHeader('Accept', 'text/plain') // the response is a URL
+  request.send(data)
+
+  return request
+}
+
+export default compose(
+  ConnectPage(params => [
+    actions.getCollection({ id: params.project }),
+    actions.getFragment({ id: params.project }, { id: params.version }),
+    actions.getFragment({ id: params.project }, { id: params.review }),
+  ]),
+  withJournal,
+  connect(
+    (state, ownProps) => ({
+      project: selectCollection(state, ownProps.params.project),
+      version: selectFragment(state, ownProps.params.version),
+      review: selectFragment(state, ownProps.params.review)
+    }),
+    {
+      uploadFile
+    }
+  ),
+  withProps(({ review }) => {
+    return {
+      initialValues: review
+    }
+  }),
+  reduxForm({
+    form: 'review',
+    onSubmit,
+    onChange: debounce(onChange, 1000)
+  })
+)(ReviewLayout)
diff --git a/packages/component-review/src/components/index.js b/packages/component-review/src/components/index.js
new file mode 100644
index 000000000..c79137444
--- /dev/null
+++ b/packages/component-review/src/components/index.js
@@ -0,0 +1 @@
+export { default } from './ReviewPage'
diff --git a/packages/component-review/src/index.js b/packages/component-review/src/index.js
new file mode 100644
index 000000000..0225ff41f
--- /dev/null
+++ b/packages/component-review/src/index.js
@@ -0,0 +1,7 @@
+module.exports = {
+  frontend: {
+    components: [
+      () => require('./components')
+    ]
+  }
+}
diff --git a/packages/component-review/src/lib/validators.js b/packages/component-review/src/lib/validators.js
new file mode 100644
index 000000000..444c5a6b3
--- /dev/null
+++ b/packages/component-review/src/lib/validators.js
@@ -0,0 +1,34 @@
+import striptags from 'striptags'
+
+export const required = value => {
+  return value ? undefined : 'Required'
+}
+
+export const minChars = min => {
+  const message = `Enter at least ${min} characters`
+
+  return value => {
+    const text = striptags(value)
+
+    if (!text || text.length < min) {
+      return message
+    }
+
+    return undefined
+  }
+}
+
+export const maxChars = max => {
+  const message = `Enter no more than ${max} characters`
+
+  return value => {
+    const text = striptags(value)
+
+    if (!text || text.length > max) {
+      return message
+    }
+
+    return undefined
+  }
+}
+
diff --git a/packages/component-review/styleguide.config.js b/packages/component-review/styleguide.config.js
new file mode 100644
index 000000000..1b16d0007
--- /dev/null
+++ b/packages/component-review/styleguide.config.js
@@ -0,0 +1,21 @@
+module.exports = {
+  title: 'xpub review style guide',
+  styleguideComponents: {
+    StyleGuideRenderer: require.resolve('xpub-styleguide/src/components/StyleGuideRenderer'),
+    Wrapper: require.resolve('xpub-styleguide/src/components/Wrapper')
+  },
+  context: {
+    faker: 'faker'
+  },
+  skipComponentsWithoutExample: true,
+  webpackConfig: require('./webpack.config.js'),
+  serverPort: 6065,
+  theme: {
+    fontFamily: {
+      base: '"Fira Sans", sans-serif'
+    },
+    color: {
+      link: 'cornflowerblue'
+    }
+  }
+}
diff --git a/packages/component-review/webpack.config.js b/packages/component-review/webpack.config.js
new file mode 100644
index 000000000..cc2081f4b
--- /dev/null
+++ b/packages/component-review/webpack.config.js
@@ -0,0 +1,112 @@
+process.env.BABEL_ENV = 'development'
+process.env.NODE_ENV = 'development'
+
+const path = require('path')
+const nodeExternals = require('webpack-node-externals')
+
+const include = [
+  path.join(__dirname, 'src'),
+  /xpub-[^/]+\/src/,
+]
+
+module.exports = {
+  entry: './src/index.js',
+  output: {
+    filename: 'index.js',
+    path: path.join(__dirname, 'dist'),
+  },
+  devtool: 'cheap-module-source-map',
+  externals: [nodeExternals({
+    whitelist: [/\.(?!js$).{1,5}$/i]
+  })],
+  resolve: {
+    symlinks: false
+  },
+  module: {
+    rules: [
+      {
+        oneOf: [
+          // ES6 modules
+          {
+            test: /\.js$/,
+            include,
+            loader: 'babel-loader',
+            options: {
+              presets: [
+                ['env', { modules: false }],
+                'react',
+                'stage-2'
+              ],
+              cacheDirectory: true,
+            },
+          },
+
+          // CSS modules
+          {
+            test: /\.local\.css$/,
+            include,
+            use: [
+              'style-loader',
+              {
+                loader: 'css-loader',
+                options: {
+                  modules: true,
+                }
+              }
+            ],
+          },
+
+          // SCSS modules
+          {
+            test: /\.local\.scss$/,
+            include,
+            use: [
+              'style-loader',
+              {
+                loader: 'css-loader',
+                options: {
+                  modules: true,
+                  importLoaders: 1
+                }
+              },
+              'sass-loader'
+            ],
+          },
+
+          // global CSS
+          {
+            test: /\.css$/,
+            use: [
+              'style-loader',
+              'css-loader'
+            ],
+          },
+
+          // global SCSS
+          {
+            test: /\.scss$/,
+            use: [
+              'style-loader',
+              {
+                loader: 'css-loader',
+                options: {
+                  importLoaders: 1
+                }
+              },
+              'sass-loader'
+            ],
+          },
+
+          // Files
+          {
+            exclude: [/\.js$/, /\.html$/, /\.json$/],
+            loader: 'file-loader',
+            options: {
+              name: 'static/media/[name].[hash:8].[ext]',
+            }
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/packages/xpub-collabra/app/config/journal/index.js b/packages/xpub-collabra/app/config/journal/index.js
index 1806bf3ca..2f85193a2 100644
--- a/packages/xpub-collabra/app/config/journal/index.js
+++ b/packages/xpub-collabra/app/config/journal/index.js
@@ -1,6 +1,7 @@
 export { default as metadata } from './metadata'
 export { default as declarations } from './declarations'
 export { default as decisions } from './decisions'
+export { default as recommendations } from './recommendations'
 export { default as sections } from './sections'
 export { default as articleSections } from './article-sections'
 export { default as articleTypes } from './article-types'
diff --git a/packages/xpub-collabra/app/config/journal/recommendations.js b/packages/xpub-collabra/app/config/journal/recommendations.js
new file mode 100644
index 000000000..d297f484d
--- /dev/null
+++ b/packages/xpub-collabra/app/config/journal/recommendations.js
@@ -0,0 +1,14 @@
+export default [
+  {
+    value: 'accept',
+    label: 'Accept',
+  },
+  {
+    value: 'revise',
+    label: 'Revise',
+  },
+  {
+    value: 'reject',
+    label: 'Reject',
+  }
+]
diff --git a/packages/xpub-collabra/app/routes.js b/packages/xpub-collabra/app/routes.js
index 81a149c4e..a3501e383 100644
--- a/packages/xpub-collabra/app/routes.js
+++ b/packages/xpub-collabra/app/routes.js
@@ -3,10 +3,18 @@ import { Redirect, Route } from 'react-router'
 import loadable from 'loadable-components'
 import { App } from 'pubsweet-component-xpub-app/src/components'
 import { AuthenticatedPage, SignupPage, LoginPage, LogoutPage } from 'pubsweet-component-xpub-authentication/src/components'
-import DashboardPage from 'pubsweet-component-xpub-dashboard/src/components'
-import SubmitPage from 'pubsweet-component-xpub-submit/src/components'
-// import ManuscriptPage from 'pubsweet-component-xpub-manuscript/src/components'
-const ManuscriptPage = loadable(() => import('pubsweet-component-xpub-manuscript/src/components'))
+
+const DashboardPage = loadable(() =>
+  import('pubsweet-component-xpub-dashboard/src/components'))
+
+const SubmitPage = loadable(() =>
+  import('pubsweet-component-xpub-submit/src/components'))
+
+const ManuscriptPage = loadable(() =>
+  import('pubsweet-component-xpub-manuscript/src/components'))
+
+const ReviewPage = loadable(() =>
+  import('pubsweet-component-xpub-review/src/components'))
 
 export default (
   <Route>
@@ -17,6 +25,7 @@ export default (
         <Route path="dashboard" component={DashboardPage}/>
         <Route path="projects/:project/version/:version/submit" component={SubmitPage}/>
         <Route path="projects/:project/version/:version/manuscript" component={ManuscriptPage}/>
+        <Route path="projects/:project/version/:version/review/:review" component={ReviewPage}/>
       </Route>
 
       <Route path="signup" component={SignupPage}/>
diff --git a/packages/xpub-collabra/config/components.json b/packages/xpub-collabra/config/components.json
index 1171bd253..fe6af5e3c 100644
--- a/packages/xpub-collabra/config/components.json
+++ b/packages/xpub-collabra/config/components.json
@@ -3,6 +3,7 @@
   "pubsweet-component-xpub-authentication",
   "pubsweet-component-xpub-dashboard",
   "pubsweet-component-xpub-manuscript",
+  "pubsweet-component-xpub-review",
   "pubsweet-component-xpub-submit",
   "pubsweet-component-ink-frontend",
   "pubsweet-component-ink-backend"
diff --git a/packages/xpub-collabra/package.json b/packages/xpub-collabra/package.json
index acae52041..fc9a8da7b 100644
--- a/packages/xpub-collabra/package.json
+++ b/packages/xpub-collabra/package.json
@@ -21,6 +21,7 @@
     "pubsweet-component-xpub-authentication": "^0.0.2",
     "pubsweet-component-xpub-dashboard": "^0.0.2",
     "pubsweet-component-xpub-manuscript": "^0.0.2",
+    "pubsweet-component-xpub-review": "^0.0.2",
     "pubsweet-component-xpub-submit": "^0.0.2",
     "pubsweet-server": "^1.0.0-alpha.2",
     "pubsweet-theme-plugin": "^0.0.1",
-- 
GitLab