From 038d94b649f94cb8b7e62a5001990ad7a84713f0 Mon Sep 17 00:00:00 2001
From: Alexandru Munteanu <alexandru.munteanu@thinslices.com>
Date: Tue, 6 Feb 2018 13:30:21 +0200
Subject: [PATCH] Refactor authors reducer

---
 .../src/components/FileSection.js             |  9 +--
 .../src/components/FileSection.local.scss     | 45 --------------
 .../src/components/WizardStep.js              |  1 -
 .../src/components/AuthorList/AuthorAdder.js  | 21 +++++--
 .../src/components/AuthorList/AuthorEditor.js | 53 ++++++++++-------
 .../src/components/UIComponents/Spinner.js    | 13 ++++
 .../UIComponents/Spinner.local.scss           | 44 ++++++++++++++
 .../src/components/UIComponents/index.js      |  1 +
 .../src/components/index.js                   |  2 +-
 .../components-faraday/src/redux/authors.js   | 59 ++++++++++++++++---
 10 files changed, 166 insertions(+), 82 deletions(-)
 create mode 100644 packages/components-faraday/src/components/UIComponents/Spinner.js
 create mode 100644 packages/components-faraday/src/components/UIComponents/Spinner.local.scss

diff --git a/packages/component-wizard/src/components/FileSection.js b/packages/component-wizard/src/components/FileSection.js
index 5f3726de3..f495196cc 100644
--- a/packages/component-wizard/src/components/FileSection.js
+++ b/packages/component-wizard/src/components/FileSection.js
@@ -5,7 +5,10 @@ import { Icon } from '@pubsweet/ui'
 import { DropTarget } from 'react-dnd'
 import { NativeTypes } from 'react-dnd-html5-backend'
 import { compose, getContext, withHandlers, withState } from 'recompose'
-import { SortableList } from 'pubsweet-components-faraday/src/components'
+import {
+  SortableList,
+  Spinner,
+} from 'pubsweet-components-faraday/src/components'
 
 import FileItem from './FileItem'
 import FilePicker from './FilePicker'
@@ -73,9 +76,7 @@ const FileSection = ({
                 </div>
               </FilePicker>
             ) : (
-              <div className={classnames(classes.rotate, classes.icon)}>
-                <Icon size={16}>loader</Icon>
-              </div>
+              <Spinner />
             )}
           </div>
           <span className={classnames(classes.error)}>{error}</span>
diff --git a/packages/component-wizard/src/components/FileSection.local.scss b/packages/component-wizard/src/components/FileSection.local.scss
index 6f1429c74..709f425a0 100644
--- a/packages/component-wizard/src/components/FileSection.local.scss
+++ b/packages/component-wizard/src/components/FileSection.local.scss
@@ -60,48 +60,3 @@
 .is-over {
   background-color: #ddd;
 }
-
-@keyframes rotating {
-  from {
-    -o-transform: rotate(0deg);
-    -ms-transform: rotate(0deg);
-    -moz-transform: rotate(0deg);
-    -webkit-transform: rotate(0deg);
-    transform: rotate(0deg);
-  }
-
-  to {
-    -o-transform: rotate(360deg);
-    -ms-transform: rotate(360deg);
-    -moz-transform: rotate(360deg);
-    -webkit-transform: rotate(360deg);
-    transform: rotate(360deg);
-  }
-}
-
-@-webkit-keyframes rotating {
-  from {
-    -webkit-transform: rotate(0deg);
-    transform: rotate(0deg);
-  }
-
-  to {
-    -webkit-transform: rotate(360deg);
-    transform: rotate(360deg);
-  }
-}
-
-.rotate {
-  -webkit-animation: rotating 1.5s linear infinite;
-  -moz-animation: rotating 1.5s linear infinite;
-  -ms-animation: rotating 1.5s linear infinite;
-  -o-animation: rotating 1.5s linear infinite;
-  animation: rotating 1.5s linear infinite;
-}
-
-.icon {
-  align-items: center;
-  display: flex;
-  justify-content: center;
-  margin: 0 5px 0 0;
-}
diff --git a/packages/component-wizard/src/components/WizardStep.js b/packages/component-wizard/src/components/WizardStep.js
index 4c369eeed..ff5bdc3b9 100644
--- a/packages/component-wizard/src/components/WizardStep.js
+++ b/packages/component-wizard/src/components/WizardStep.js
@@ -65,7 +65,6 @@ export default ({
             )
           },
         )}
-
       <div className={classnames(classes.buttons)}>
         <Button onClick={isFirst ? () => history.push('/') : prevStep}>
           {isFirst
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
index 30a532678..a3380004f 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
@@ -7,7 +7,9 @@ import { reduxForm } from 'redux-form'
 import { compose, withProps } from 'recompose'
 import { selectCurrentUser } from 'xpub-selectors'
 
+import { Spinner } from '../UIComponents/'
 import classes from './AuthorList.local.scss'
+import { getAuthorFetching } from '../../redux/authors'
 import { MenuItem, ValidatedTextField } from './FormItems'
 
 const countries = [
@@ -22,7 +24,13 @@ const emailRegex = new RegExp(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/)
 const emailValidator = value =>
   emailRegex.test(value) ? undefined : 'Invalid email'
 
-const AuthorAdder = ({ authors, editMode, setEditMode, handleSubmit }) => (
+const AuthorAdder = ({
+  authors,
+  editMode,
+  setEditMode,
+  handleSubmit,
+  isFetching,
+}) => (
   <div className={classnames(classes.adder)}>
     <Button onClick={setEditMode(true)} primary>
       {authors.length === 0 ? '+ Add submitting author' : '+ Add author'}
@@ -54,9 +62,13 @@ const AuthorAdder = ({ authors, editMode, setEditMode, handleSubmit }) => (
         </div>
         <div className={classnames(classes['form-buttons'])}>
           <Button onClick={setEditMode(false)}>Cancel</Button>
-          <Button onClick={handleSubmit} primary>
-            Save
-          </Button>
+          {!isFetching ? (
+            <Button onClick={handleSubmit} primary>
+              Save
+            </Button>
+          ) : (
+            <Spinner />
+          )}
         </div>
       </div>
     )}
@@ -66,6 +78,7 @@ const AuthorAdder = ({ authors, editMode, setEditMode, handleSubmit }) => (
 export default compose(
   connect(state => ({
     currentUser: selectCurrentUser(state),
+    isFetching: getAuthorFetching(state),
   })),
   withProps(({ currentUser: { admin, username, email }, authors }) => {
     if (!admin && authors.length === 0) {
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorEditor.js b/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
index 41533434e..10f4f5a81 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
@@ -1,8 +1,12 @@
 import React from 'react'
 import classnames from 'classnames'
+import { compose } from 'recompose'
 import { Button } from '@pubsweet/ui'
+import { connect } from 'react-redux'
 import { reduxForm } from 'redux-form'
 
+import { Spinner } from '../UIComponents'
+import { getAuthorFetching } from '../../redux/authors'
 import { ValidatedTextField, MenuItem } from './FormItems'
 
 import classes from './AuthorList.local.scss'
@@ -19,7 +23,7 @@ const emailRegex = new RegExp(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/)
 const emailValidator = value =>
   emailRegex.test(value) ? undefined : 'Invalid email'
 
-const AuthorEdit = ({ setAuthorEdit, handleSubmit }) => (
+const AuthorEdit = ({ isFetching, setAuthorEdit, handleSubmit }) => (
   <div className={classnames(classes['editor-body'])}>
     <div className={classnames(classes.row)}>
       <ValidatedTextField isRequired label="First name" name="edit.firstName" />
@@ -44,26 +48,35 @@ const AuthorEdit = ({ setAuthorEdit, handleSubmit }) => (
 
     <div className={classnames(classes['form-buttons'])}>
       <Button onClick={setAuthorEdit(-1)}>Cancel</Button>
-      <Button onClick={handleSubmit} primary>
-        Save
-      </Button>
+      {!isFetching ? (
+        <Button onClick={handleSubmit} primary>
+          Save
+        </Button>
+      ) : (
+        <Spinner />
+      )}
     </div>
   </div>
 )
 
-export default reduxForm({
-  form: 'edit',
-  onSubmit: (
-    values,
-    dispatch,
-    { setAuthorEdit, setAuthors, authors, index, changeForm },
-  ) => {
-    const newAuthors = [
-      ...authors.slice(0, index),
-      values.edit,
-      ...authors.slice(index + 1),
-    ]
-    setAuthorEdit(-1)()
-    setAuthors(newAuthors)
-  },
-})(AuthorEdit)
+export default compose(
+  connect(state => ({
+    isFetching: getAuthorFetching(state),
+  })),
+  reduxForm({
+    form: 'edit',
+    onSubmit: (
+      values,
+      dispatch,
+      { setAuthorEdit, setAuthors, authors, index, changeForm },
+    ) => {
+      const newAuthors = [
+        ...authors.slice(0, index),
+        values.edit,
+        ...authors.slice(index + 1),
+      ]
+      setAuthorEdit(-1)()
+      setAuthors(newAuthors)
+    },
+  }),
+)(AuthorEdit)
diff --git a/packages/components-faraday/src/components/UIComponents/Spinner.js b/packages/components-faraday/src/components/UIComponents/Spinner.js
new file mode 100644
index 000000000..42c828c4b
--- /dev/null
+++ b/packages/components-faraday/src/components/UIComponents/Spinner.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import classnames from 'classnames'
+import { Icon } from '@pubsweet/ui'
+
+import classes from './Spinner.local.scss'
+
+const Spinner = () => (
+  <div className={classnames(classes.rotate, classes.icon)}>
+    <Icon size={16}>loader</Icon>
+  </div>
+)
+
+export default Spinner
diff --git a/packages/components-faraday/src/components/UIComponents/Spinner.local.scss b/packages/components-faraday/src/components/UIComponents/Spinner.local.scss
new file mode 100644
index 000000000..b82f01d0e
--- /dev/null
+++ b/packages/components-faraday/src/components/UIComponents/Spinner.local.scss
@@ -0,0 +1,44 @@
+@keyframes rotating {
+  from {
+    -o-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -moz-transform: rotate(0deg);
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+
+  to {
+    -o-transform: rotate(360deg);
+    -ms-transform: rotate(360deg);
+    -moz-transform: rotate(360deg);
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+@-webkit-keyframes rotating {
+  from {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+
+  to {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+.rotate {
+  -webkit-animation: rotating 1.8s linear infinite;
+  -moz-animation: rotating 1.8s linear infinite;
+  -ms-animation: rotating 1.8s linear infinite;
+  -o-animation: rotating 1.8s linear infinite;
+  animation: rotating 1.8s linear infinite;
+}
+
+.icon {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  margin: 0 5px 0 0;
+}
diff --git a/packages/components-faraday/src/components/UIComponents/index.js b/packages/components-faraday/src/components/UIComponents/index.js
index 8c2c826a2..0f79f62b6 100644
--- a/packages/components-faraday/src/components/UIComponents/index.js
+++ b/packages/components-faraday/src/components/UIComponents/index.js
@@ -1,2 +1,3 @@
 export { default as Logo } from './Logo'
+export { default as Spinner } from './Spinner'
 export { default as Dropdown } from './Dropdown'
diff --git a/packages/components-faraday/src/components/index.js b/packages/components-faraday/src/components/index.js
index da51b2a17..0415e239b 100644
--- a/packages/components-faraday/src/components/index.js
+++ b/packages/components-faraday/src/components/index.js
@@ -1,5 +1,5 @@
 export { default as SortableList } from './SortableList/SortableList'
 export { default as AuthorList } from './AuthorList/AuthorList'
 
-export { Dropdown, Logo } from './UIComponents'
+export { Dropdown, Logo, Spinner } from './UIComponents'
 export { DragHandle } from './AuthorList/FormItems'
diff --git a/packages/components-faraday/src/redux/authors.js b/packages/components-faraday/src/redux/authors.js
index 5d78978e7..8a9e3a9c7 100644
--- a/packages/components-faraday/src/redux/authors.js
+++ b/packages/components-faraday/src/redux/authors.js
@@ -1,19 +1,64 @@
+import { get } from 'lodash'
 import * as api from 'pubsweet-client/src/helpers/api'
 
 // constants
-export const SET_AUTHORS = 'authors/SET_AUTHORS'
+const REQUEST = 'authors/REQUEST'
+const FAILURE = 'authors/FAILURE'
+const SUCCESS = 'authors/SUCCESS'
 
 // actions
-export const addAuthor = (author, collectionId, fragmentId) => dispatch =>
-  api.create(
-    `/collections/${collectionId}/fragments/${fragmentId}/authors`,
-    author,
-  )
+export const authorRequest = () => ({
+  type: REQUEST,
+})
+
+export const authorFaiure = error => ({
+  type: FAILURE,
+  error,
+})
+
+export const authorSuccess = () => ({
+  type: SUCCESS,
+})
+
+export const addAuthor = (author, collectionId, fragmentId) => dispatch => {
+  dispatch(authorRequest())
+  return api
+    .create(
+      `/collections/${collectionId}/fragments/${fragmentId}/authors`,
+      author,
+    )
+    .then(author => {
+      dispatch(authorSuccess())
+      return author
+    })
+    .catch(err => dispatch(authorFaiure(err)))
+}
 
 // selectors
+export const getFragmentAuthors = (state, fragmentId) =>
+  get(state, `authors.${fragmentId}`) || []
+
+export const getAuthorFetching = state => state.authors.isFetching
+export const getAuthorError = state => state.authors.error
+
+const initialState = { isFetching: false, error: null }
 
-export default (state = {}, action) => {
+export default (state = initialState, action) => {
   switch (action.type) {
+    case 'UPDATE_FRAGMENT_REQUEST':
+    case REQUEST:
+      return {
+        ...initialState,
+        isFetching: true,
+      }
+    case FAILURE:
+      return {
+        ...initialState,
+        error: action.error,
+      }
+    case 'UPDATE_FRAGMENT_SUCCESS':
+    case SUCCESS:
+      return initialState
     default:
       return state
   }
-- 
GitLab