From fc58ad6a46ab6ac1b44cb0eb7aebfddfa542e3f9 Mon Sep 17 00:00:00 2001
From: Jure Triglav <juretriglav@gmail.com>
Date: Thu, 13 Aug 2020 15:15:10 +0200
Subject: [PATCH] feat: improve file handling in review and decision component

---
 .../src/components/DecisionPage.js            | 201 +++------
 .../src/components/ReviewPage.js              | 401 ++++--------------
 .../src/components/ReviewersPage.js           |  20 +-
 .../components/assignEditors/AssignEditor.js  |   8 +-
 .../src/components/decision/DecisionForm.js   | 132 +++---
 .../components/decision/DecisionReviews.js    |   2 +-
 .../src/components/review/Review.js           |  18 +-
 .../src/components/review/ReviewForm.js       | 211 ++++-----
 .../src/components/review/util.js             |  46 +-
 .../src/components/reviewers/Reviewers.js     |   4 +-
 10 files changed, 359 insertions(+), 684 deletions(-)

diff --git a/app/components/component-review/src/components/DecisionPage.js b/app/components/component-review/src/components/DecisionPage.js
index cbb412e57f..09ba62e323 100644
--- a/app/components/component-review/src/components/DecisionPage.js
+++ b/app/components/component-review/src/components/DecisionPage.js
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useRef, useEffect } from 'react'
 import moment from 'moment'
 
 import { Tabs } from '@pubsweet/ui'
@@ -15,7 +15,6 @@ import { AdminSection, Columns, Manuscript, Chat } from './style'
 
 import { Spinner } from '../../../shared'
 
-import { getCommentContent } from './review/util'
 import MessageContainer from '../../../component-chat/src'
 
 const addEditor = (manuscript, label) => ({
@@ -24,23 +23,34 @@ const addEditor = (manuscript, label) => ({
   label,
 })
 
+const commentFields = `
+  id
+  commentType
+  content
+  files {
+    id
+    created
+    label
+    filename
+    fileType
+    mimeType
+    size
+    url
+  }
+`
+
 const reviewFields = `
   id
   created
   updated
-  comments {
-    type
-    content
-    files {
-      id
-      created
-      label
-      filename
-      fileType
-      mimeType
-      size
-      url
-    }
+  decisionComment {
+    ${commentFields}
+  }
+  reviewComment {
+    ${commentFields}
+  }
+  confidentialComment {
+    ${commentFields}
   }
   isDecision
   recommendation
@@ -71,9 +81,8 @@ const fragmentFields = `
     id
     name
     role
-    object {
-      objectId
-      objectType
+    manuscript {
+      id
     }
     members {
       id
@@ -153,29 +162,6 @@ const updateReviewMutationQuery = gql`
   }
 `
 
-const uploadReviewFilesMutation = gql`
-  mutation($file: Upload!) {
-    upload(file: $file) {
-      url
-    }
-  }
-`
-
-// const createFileMutation = gql`
-//   mutation($file: Upload!) {
-//     createFile(file: $file) {
-//       id
-//       created
-//       label
-//       filename
-//       fileType
-//       mimeType
-//       size
-//       url
-//     }
-//   }
-// `
-
 const makeDecisionMutation = gql`
   mutation($id: ID!, $decision: String) {
     makeDecision(id: $id, decision: $decision) {
@@ -184,41 +170,6 @@ const makeDecisionMutation = gql`
     }
   }
 `
-
-// const updateCacheForFileCreation = (proxy, { data: { createFile } }) => {
-//   const data = proxy.readQuery({
-//     query,
-//     variables: {
-//       id: match.params.version,
-//     },
-//   })
-
-//   data.manuscript.reviews.map(review => {
-//     if (review.id === file.objectId) {
-//       review.comments.map(comment => {
-//         if (comment.type === createFile.fileType) {
-//           comment.files = [createFile]
-//         }
-//         return comment
-//       })
-//     }
-//     return review
-//   })
-
-//   proxy.writeQuery({ query, data })
-// }
-
-// const createFile = file => {
-
-//   mutate({
-//     variables: {
-//       file,
-//     },
-//     update:
-// },
-
-//
-
 const dateLabel = date => moment(date).format('YYYY-MM-DD')
 
 const decisionSections = ({
@@ -335,21 +286,33 @@ const decisionSections = ({
 
 const DecisionPage = ({ match }) => {
   // Hooks from the old world
-  const [makeDecision] = useMutation(makeDecisionMutation, {
-    // refetchQueries: [query],
-  })
+  const [makeDecision] = useMutation(makeDecisionMutation)
   const [updateReviewMutation] = useMutation(updateReviewMutationQuery)
 
-  // File upload
-  const [uploadReviewFiles] = useMutation(uploadReviewFilesMutation)
-
   const { loading, error, data } = useQuery(query, {
     variables: {
       id: match.params.version,
     },
-    fetchPolicy: 'network-only',
+    // fetchPolicy: 'cache-and-network',
   })
 
+  const reviewOrInitial = manuscript =>
+    (manuscript &&
+      manuscript.reviews &&
+      manuscript.reviews.find(review => review.isDecision)) || {
+      comments: [],
+      isDecision: true,
+      recommendation: null,
+    }
+
+  // Find an existing review or create a placeholder, and hold a ref to it
+  const existingReview = useRef(reviewOrInitial(data?.manuscript))
+
+  // Update the value of that ref if the manuscript object changes
+  useEffect(() => {
+    existingReview.current = reviewOrInitial(data?.manuscript)
+  }, [data?.manuscript?.reviews])
+
   if (loading) return <Spinner />
   if (error) return `Error! ${error.message}`
 
@@ -361,41 +324,21 @@ const DecisionPage = ({ match }) => {
     channelId = manuscript.channels.find(c => c.type === 'editorial').id
   }
 
-  const uploadFile = (file, updateReview, type) =>
-    uploadReviewFiles({
-      variables: {
-        file,
-      },
-    }).then(({ data }) => {
-      // const newFile = {
-      //   url: data.upload.url,
-      //   filename: file.name,
-      //   size: file.size,
-      //   object: 'Review',
-      //   objectId: updateReview.id,
-      //   fileType: type,
-      // }
-      // createFile(newFile)
-    })
-
-  const updateReview = (data, file) => {
+  const updateReview = review => {
     const reviewData = {
-      isDecision: true,
+      recommendation: review.recommendation,
       manuscriptId: manuscript.id,
+      isDecision: true,
+      decisionComment: review.decisionComment && {
+        id: existingReview.current.decisionComment?.id,
+        commentType: 'decision',
+        content: review.decisionComment.content,
+      },
     }
 
-    if (data.comment) {
-      reviewData.comments = [data.comment]
-    }
-
-    if (data.recommendation) {
-      reviewData.recommendation = data.recommendation
-    }
-
-    const review = manuscript.reviews.find(review => review.isDecision) || {}
     return updateReviewMutation({
       variables: {
-        id: review.id || undefined,
+        id: existingReview.current.id || undefined,
         input: reviewData,
       },
       update: (cache, { data: { updateReview } }) => {
@@ -427,13 +370,6 @@ const DecisionPage = ({ match }) => {
       },
     })
   }
-
-  const initialValues = (manuscript.reviews &&
-    manuscript.reviews.find(review => review.isDecision)) || {
-    comments: [],
-    recommendation: null,
-  }
-
   // const editorSectionsResult = editorSections({ manuscript })
 
   return (
@@ -441,7 +377,8 @@ const DecisionPage = ({ match }) => {
       <Manuscript>
         <Formik
           displayName="decision"
-          initialValues={initialValues}
+          enableReinitialize
+          initialValues={existingReview}
           // isInitialValid={({ manuscript }) => {
           //   const rv =
           //     manuscript.reviews.find(review => review.isDecision) || {}
@@ -459,17 +396,17 @@ const DecisionPage = ({ match }) => {
               },
             })
           }}
-          validate={(values, props) => {
-            const errors = {}
-            if (getCommentContent(values, 'note') === '') {
-              errors.comments = 'Required'
-            }
-
-            if (values.recommendation === null) {
-              errors.recommendation = 'Required'
-            }
-            return errors
-          }}
+          // validate={(values, props) => {
+          //   const errors = {}
+          //   if (values.decisionComment?.content === '') {
+          //     errors.decisionComment = 'Required'
+          //   }
+
+          //   if (values.recommendation === null) {
+          //     errors.recommendation = 'Required'
+          //   }
+          //   return errors
+          // }}
         >
           {props => (
             // Temp
@@ -488,7 +425,6 @@ const DecisionPage = ({ match }) => {
                     handleSubmit: props.handleSubmit,
                     isValid: props.isValid,
                     updateReview,
-                    uploadFile,
                   })[decisionSections.length - 1].key
                 }
                 sections={decisionSections({
@@ -496,7 +432,6 @@ const DecisionPage = ({ match }) => {
                   handleSubmit: props.handleSubmit,
                   isValid: props.isValid,
                   updateReview,
-                  uploadFile,
                 })}
                 title="Versions"
               />
diff --git a/app/components/component-review/src/components/ReviewPage.js b/app/components/component-review/src/components/ReviewPage.js
index d9c0e28f99..399c0a4ddc 100644
--- a/app/components/component-review/src/components/ReviewPage.js
+++ b/app/components/component-review/src/components/ReviewPage.js
@@ -1,30 +1,37 @@
-import React from 'react'
+import React, { useRef, useEffect } from 'react'
 import { useMutation, useQuery } from '@apollo/client'
 import gql from 'graphql-tag'
 import { Formik } from 'formik'
 // import { cloneDeep } from 'lodash'
-import { getCommentContent } from './review/util'
 import ReviewLayout from '../components/review/ReviewLayout'
 import { Spinner } from '../../../shared'
 import useCurrentUser from '../../../../hooks/useCurrentUser'
 
+const commentFields = `
+id
+commentType
+content
+files {
+  id
+  created
+  label
+  filename
+  fileType
+  mimeType
+  size
+  url
+}
+`
+
 const reviewFields = `
   id
   created
   updated
-  comments {
-    type
-    content
-    files {
-      id
-      created
-      label
-      filename
-      fileType
-      mimeType
-      size
-      url
-    }
+  reviewComment {
+    ${commentFields}
+  }
+  confidentialComment {
+    ${commentFields}
   }
   isDecision
   recommendation
@@ -34,23 +41,6 @@ const reviewFields = `
   }
 `
 
-// const teamFields = `
-//   id
-//   name
-//   role
-//   object {
-//     objectId
-//     objectType
-//   }
-//   members {
-//     id
-//     user {
-//       id
-//       username
-//     }
-//   }
-// `
-
 const fragmentFields = `
   id
   created
@@ -72,9 +62,8 @@ const fragmentFields = `
     id
     name
     role
-    object {
-      objectId
-      objectType
+    manuscript {
+      id
     }
     members {
       id
@@ -163,87 +152,39 @@ const updateReviewMutationQuery = gql`
   }
 `
 
-const uploadReviewFilesMutation = gql`
-  mutation($file: Upload!) {
-    upload(file: $file) {
-      url
-    }
-  }
-`
-
-const createFileMutation = gql`
-  mutation($file: Upload!) {
-    createFile(file: $file) {
-      id
-      created
-      label
-      filename
-      fileType
-      mimeType
-      size
-      url
-    }
-  }
-`
-
 export default ({ match, ...props }) => {
   const currentUser = useCurrentUser()
   const [updateReviewMutation] = useMutation(updateReviewMutationQuery)
   const [completeReview] = useMutation(completeReviewMutation)
 
-  // File upload
-  // const [uploadReviewFiles] = useMutation(uploadReviewFilesMutation)
-
-  const [createFileM] = useMutation(createFileMutation)
-  const createFile = file =>
-    createFileM({
-      variables: {
-        file,
-      },
-      update: (proxy, { data: { createFile } }) => {
-        const data = proxy.readQuery({
-          query,
-          variables: {
-            id: match.params.version,
-          },
-        })
-
-        data.manuscript.reviews.map(review => {
-          if (review.id === file.objectId) {
-            review.comments.map(comment => {
-              if (comment.type === createFile.fileType) {
-                comment.files = [createFile]
-              }
-              return comment
-            })
-          }
-          return review
-        })
-
-        proxy.writeQuery({ query, data })
-      },
-    })
-
   const { loading, error, data } = useQuery(query, {
     variables: {
       id: match.params.version,
     },
-    fetchPolicy: 'network-only',
   })
 
+  const reviewOrInitial = manuscript =>
+    (manuscript &&
+      manuscript.reviews &&
+      manuscript.reviews.find(
+        review => review?.user?.id === currentUser.id && !review.isDecision,
+      )) ||
+    {}
+
+  // Find an existing review or create a placeholder, and hold a ref to it
+  const existingReview = useRef(reviewOrInitial(data?.manuscript))
+
+  // Update the value of that ref if the manuscript object changes
+  useEffect(() => {
+    existingReview.current = reviewOrInitial(data?.manuscript)
+  }, [data?.manuscript?.reviews])
+
   if (loading) return <Spinner />
   if (error) return `Error! ${error.message}`
 
   const { manuscript } = data
   const channelId = manuscript.channels.find(c => c.type === 'editorial').id
 
-  const review =
-    (manuscript.reviews &&
-      manuscript.reviews.find(
-        review => review.user.id === currentUser.id && !review.isDecision,
-      )) ||
-    {}
-
   // eslint-disable-next-line
   const status = (
     (
@@ -253,61 +194,55 @@ export default ({ match, ...props }) => {
   ).status
 
   const updateReview = (review, file) => {
-    ;(review.comments || []).map(comment => {
-      delete comment.files
-      delete comment.__typename
-      return comment
-    })
-
     const reviewData = {
       recommendation: review.recommendation,
-      comments: review.comments,
       manuscriptId: manuscript.id,
+      reviewComment: review.reviewComment && {
+        id: existingReview.current.reviewComment?.id,
+        commentType: 'review',
+        content: review.reviewComment.content,
+      },
+      confidentialComment: review.confidentialComment && {
+        id: existingReview.current.confidentialComment?.id,
+        commentType: 'confidential',
+        content: review.confidentialComment.content,
+      },
     }
 
     return updateReviewMutation({
       variables: {
-        id: review.id || undefined,
+        id: existingReview.current.id || undefined,
         input: reviewData,
       },
-      // update: (proxy, { data: { updateReview } }) => {
-      //   const data = JSON.parse(
-      //     JSON.stringify(
-      //       proxy.readQuery({
-      //         query,
-      //         variables: {
-      //           id: manuscript.id,
-      //         },
-      //       }),
-      //     ),
-      //   )
-      //   let reviewIndex = data.manuscript.reviews.findIndex(
-      //     review => review.id === updateReview.id,
-      //   )
-      //   reviewIndex = reviewIndex < 0 ? 0 : reviewIndex
-      //   data.manuscript.reviews[reviewIndex] = updateReview
-      //   proxy.writeQuery({ query, data })
-      // },
-    })
-  }
+      update: (cache, { data: { updateReview } }) => {
+        cache.modify({
+          id: cache.identify(manuscript),
+          fields: {
+            reviews(existingReviewRefs = [], { readField }) {
+              const newReviewRef = cache.writeFragment({
+                data: updateReview,
+                fragment: gql`
+                  fragment NewReview on Review {
+                    id
+                  }
+                `,
+              })
+
+              if (
+                existingReviewRefs.some(
+                  ref => readField('id', ref) === updateReview.id,
+                )
+              ) {
+                return existingReviewRefs
+              }
 
-  const uploadFile = (file, updateReview, type) =>
-    uploadReviewFilesMutation({
-      variables: {
-        file,
+              return [...existingReviewRefs, newReviewRef]
+            },
+          },
+        })
       },
-    }).then(({ data }) => {
-      const newFile = {
-        url: data.upload.url,
-        filename: file.name,
-        mimeType: file.type,
-        size: file.size,
-        object: 'Review',
-        objectId: updateReview.id,
-        fileType: type,
-      }
-      createFile(newFile)
     })
+  }
 
   const handleSubmit = async ({ reviewId, history }) => {
     await completeReview({
@@ -324,7 +259,7 @@ export default ({ match, ...props }) => {
       initialValues={
         (manuscript.reviews &&
           manuscript.reviews.find(
-            review => review.user.id === currentUser.id && !review.isDecision,
+            review => review?.user?.id === currentUser.id && !review.isDecision,
           )) || {
           id: null,
           comments: [],
@@ -332,12 +267,15 @@ export default ({ match, ...props }) => {
         }
       }
       onSubmit={values =>
-        handleSubmit({ reviewId: review.id, history: props.history })
+        handleSubmit({
+          reviewId: existingReview.current.id,
+          history: props.history,
+        })
       }
       validateOnMount={review => {
         if (!review.id) return false
         const hasRecommendation = review.recommendation !== null
-        const comment = getCommentContent(review, 'note')
+        const comment = review.decisionComment?.content
         const isCommented = comment !== null && comment !== ''
 
         return isCommented && hasRecommendation
@@ -348,183 +286,12 @@ export default ({ match, ...props }) => {
           channelId={channelId}
           currentUser={currentUser}
           manuscript={manuscript}
-          review={review}
+          review={existingReview}
           status={status}
           updateReview={updateReview}
-          uploadFile={uploadFile}
           {...formikProps}
         />
       )}
     </Formik>
   )
 }
-
-// export default compose(
-// graphql(query, {
-//   options: ({ match }) => ({
-//     variables: {
-//       id: match.params.version,
-//     },
-//   }),
-// }),
-// graphql(uploadReviewFilesMutation, { name: 'uploadReviewFilesMutation' }),
-// graphql(updateReviewMutation, { name: 'updateReviewMutation' }),
-// graphql(updateTeam, { name: 'updateTeam' }),
-// graphql(createFileMutation, {
-//   props: ({ mutate, ownProps: { match } }) => ({
-//     createFile: file => {
-//       mutate({
-//         variables: {
-//           file,
-//         },
-//         update: (proxy, { data: { createFile } }) => {
-//           const data = proxy.readQuery({
-//             query,
-//             variables: {
-//               id: match.params.version,
-//             },
-//           })
-
-//           data.manuscript.reviews.map(review => {
-//             if (review.id === file.objectId) {
-//               review.comments.map(comment => {
-//                 if (comment.type === createFile.fileType) {
-//                   comment.files = [createFile]
-//                 }
-//                 return comment
-//               })
-//             }
-//             return review
-//           })
-
-//           proxy.writeQuery({ query, data })
-//         },
-//       })
-//     },
-//   }),
-// }),
-// withLoader(),
-// withProps(
-//   ({
-//     manuscript,
-//     currentUser,
-//     match: {
-//       params: { journal },
-//     },
-//     updateReviewMutation,
-//     uploadReviewFilesMutation,
-//     updateTeam,
-//     createFile,
-//   }) => ({
-// journal: { id: journal },
-// review:
-//   manuscript.reviews.find(
-//     review => review.user.id === currentUser.id && !review.isDecision,
-//   ) || {},
-// status: (
-//   (
-//     (manuscript.teams.find(team => team.role === 'reviewer') || {})
-//       .status || []
-//   ).find(status => status.user === currentUser.id) || {}
-// ).status,
-// updateReview: (review, file) => {
-//   ;(review.comments || []).map(comment => {
-//     delete comment.files
-//     delete comment.__typename
-//     return comment
-//   })
-
-//   const reviewData = {
-//     recommendation: review.recommendation,
-//     comments: review.comments,
-//     manuscriptId: manuscript.id,
-//   }
-
-//   return updateReviewMutation({
-//     variables: {
-//       id: review.id || undefined,
-//       input: reviewData,
-//     },
-//     update: (proxy, { data: { updateReview } }) => {
-//       const data = JSON.parse(
-//         JSON.stringify(
-//           proxy.readQuery({
-//             query,
-//             variables: {
-//               id: manuscript.id,
-//             },
-//           }),
-//         ),
-//       )
-//       let reviewIndex = data.manuscript.reviews.findIndex(
-//         review => review.id === updateReview.id,
-//       )
-//       reviewIndex = reviewIndex < 0 ? 0 : reviewIndex
-//       data.manuscript.reviews[reviewIndex] = updateReview
-//       proxy.writeQuery({ query, data })
-//     },
-//   })
-// },
-// uploadFile: (file, updateReview, type) =>
-//   uploadReviewFilesMutation({
-//     variables: {
-//       file,
-//     },
-//   }).then(({ data }) => {
-//     const newFile = {
-//       url: data.upload.url,
-//       filename: file.name,
-//       mimeType: file.type,
-//       size: file.size,
-//       object: 'Review',
-//       objectId: updateReview.id,
-//       fileType: type,
-//     }
-//     createFile(newFile)
-//   }),
-//     completeReview: history => {
-//       const team = cloneDeep(manuscript.teams).find(
-//         team => team.role === 'reviewer',
-//       )
-//       team.members = team.members.map(m => {
-//         if (m.user.id === currentUser.id) {
-//           return { user: { id: m.user.id }, status: 'completed' }
-//         }
-//         return { user: { id: m.user.id }, status: m.status }
-//       })
-
-//       updateTeam({
-//         variables: {
-//           id: team.id,
-//           input: {
-//             members: team.members,
-//           },
-//         },
-//       }).then(() => {
-//         history.push('/dashboard')
-//       })
-//     },
-//   }),
-// ),
-//   withFormik({
-//     mapPropsToValues: props =>
-//       props.manuscript.reviews.find(
-//         review => review.user.id === props.currentUser.id && !review.isDecision,
-//       ) || {
-//         id: null,
-//         comments: [],
-//         recommendation: null,
-//       },
-//     isInitialValid: ({ review }) => {
-//       if (!review.id) return false
-//       const hasRecommendation = review.recommendation !== null
-//       const comment = getCommentContent(review, 'note')
-//       const isCommented = comment !== null && comment !== ''
-
-//       return isCommented && hasRecommendation
-//     },
-//     displayName: 'review',
-//     handleSubmit: (props, { props: { completeReview, history } }) =>
-//       completeReview(history),
-//   }),
-// )(ReviewLayout)
diff --git a/app/components/component-review/src/components/ReviewersPage.js b/app/components/component-review/src/components/ReviewersPage.js
index 33ae162f81..3c60c1b671 100644
--- a/app/components/component-review/src/components/ReviewersPage.js
+++ b/app/components/component-review/src/components/ReviewersPage.js
@@ -9,9 +9,8 @@ const teamFields = `
   id
   role
   name
-  object {
-    objectId
-    objectType
+  manuscript {
+    id
   }
   members {
     id
@@ -46,17 +45,6 @@ const fragmentFields = `
     open
     recommendation
     created
-    comments {
-      type
-      content
-      files {
-        fileType
-        id
-        label
-        url
-        filename
-      }
-    }
     user {
       id
       username
@@ -114,8 +102,8 @@ const ReviewersPage = ({ match, history }) => {
     update: (cache, { data: { addReviewer } }) => {
       cache.modify({
         id: cache.identify({
-          __typename: addReviewer.object.objectType,
-          id: addReviewer.object.objectId,
+          __typename: 'Manuscript',
+          id: addReviewer.manuscript.id,
         }),
         fields: {
           teams(existingTeamRefs = []) {
diff --git a/app/components/component-review/src/components/assignEditors/AssignEditor.js b/app/components/component-review/src/components/assignEditors/AssignEditor.js
index 067a3f2ca8..d90c4328bc 100644
--- a/app/components/component-review/src/components/assignEditors/AssignEditor.js
+++ b/app/components/component-review/src/components/assignEditors/AssignEditor.js
@@ -16,9 +16,8 @@ const teamFields = `
   id
   name
   role
-  object {
-    objectId
-    objectType
+  manuscript {
+    id
   }
   members {
     id
@@ -113,8 +112,7 @@ export default compose(
     props: ({ mutate, ownProps }) => {
       const createTeam = (userId, teamRole) => {
         const input = {
-          objectId: ownProps.manuscript.id,
-          objectType: 'Manuscript',
+          manuscriptId: ownProps.manuscript.id,
           name:
             teamRole === 'seniorEditor' ? 'Senior Editor' : 'Handling Editor',
           role: teamRole,
diff --git a/app/components/component-review/src/components/decision/DecisionForm.js b/app/components/component-review/src/components/decision/DecisionForm.js
index fb0b035a64..6a501deada 100644
--- a/app/components/component-review/src/components/decision/DecisionForm.js
+++ b/app/components/component-review/src/components/decision/DecisionForm.js
@@ -1,23 +1,19 @@
 import React, { useContext } from 'react'
 import { NoteEditor } from 'xpub-edit'
-import { cloneDeep, omit } from 'lodash'
+// import { cloneDeep, omit } from 'lodash'
 import { Field } from 'formik'
 import {
   Button,
-  Flexbox,
+  // Flexbox,
   RadioGroup,
-  UploadButton,
-  UploadingFile,
+  // UploadButton,
+  // UploadingFile,
 } from '@pubsweet/ui'
 import { JournalContext } from '../../../../xpub-journal/src'
 import { required } from '../../../../xpub-validators/src'
+import { FilesUpload } from '../../../../shared'
 
-import {
-  getCommentFiles,
-  getCommentContent,
-  stripHtml,
-  createComments,
-} from '../review/util'
+import { reviewWithComment } from '../review/util'
 
 import {
   Container,
@@ -28,21 +24,45 @@ import {
   SectionAction,
 } from '../style'
 
-const NoteDecision = ({ updateReview, uploadFile }) => (
+const NoteDecision = ({ updateReview }) => (
   <>
-    <Field
+    <Field key="noteField" name="decisionComment">
+      {formikBag => (
+        <>
+          <p>NoteDecision: {JSON.stringify(formikBag.form.values)}</p>
+          <NoteInput updateReview={updateReview} {...formikBag} />
+          <FilesUpload
+            containerId={formikBag.field.value?.id}
+            containerName="reviewComment"
+            fieldName="decisionComment.files"
+            initializeContainer={async () => {
+              const review = reviewWithComment({
+                commentType: 'decision',
+                isDecision: true,
+                values: formikBag.form.values,
+                name: 'decisionComment',
+              })
+              const { data } = await updateReview(review)
+              return data.updateReview.decisionComment.id
+            }}
+          />
+        </>
+      )}
+    </Field>
+    {/* <Field
       component={NoteInput}
       key="commentinput"
       name="comments"
       updateReview={updateReview}
       validate={required}
     />
-    <Field
-      component={AttachmentsInput('note')}
+    <FilesUpload
+      objectType="Review"
+      objectId=
       key="attachmentinput"
+      fileType="note"
       updateReview={updateReview}
-      uploadFile={uploadFile}
-    />
+    /> */}
   </>
 )
 
@@ -51,65 +71,35 @@ const NoteInput = ({
   form: { values, setFieldValue },
   updateReview,
 }) => (
-  <NoteEditor
-    key="note-input"
-    onBlur={() => {}}
-    onChange={value => {
-      const { updateIndex, comment } = createComments(
-        values,
-        {
-          type: 'note',
-          content: stripHtml(value),
-        },
-        'note',
-      )
+  // const review = useState()
 
-      setFieldValue(`comments.${updateIndex}`, comment)
-      updateReview(
-        cloneDeep(omit({ comment }, ['comment.files', 'comment.__typename'])),
-      )
-    }}
-    placeholder="Write/paste your decision letter here, or upload it using the upload button on the right."
-    value={getCommentContent({ comments: field.value }, 'note')}
-  />
-)
+  // const review = useRef({})
 
-const AttachmentsInput = type => ({
-  field,
-  form: { values, setFieldValue },
-  updateReview,
-  uploadFile,
-}) => (
-  <>
-    <UploadButton
-      buttonText="↑ Upload files"
-      key="note-attachment"
-      onChange={event => {
-        const val = event.target.files[0]
-        const file = cloneDeep(val)
-        file.filename = val.name
-        file.type = type
-
-        const { updateIndex, comment } = createComments(
-          field.value,
-          { files: [file] },
-          type,
-        )
+  // useEffect(() => {
+  //   review.current = reviewWithComment({
+  //     id: values.decisionComment?.id,
+  //     values,
+  //     commentType: 'decision',
+  //     name: 'decisionComment',
+  //     isDecision: true,
+  //   })
+  // }, [values])
 
-        setFieldValue(`comments.${updateIndex}.files`, comment.files)
-
-        updateReview({}).then(({ data: { updateReview } }) => {
-          uploadFile(val, updateReview, type)
+  // console.log('Rendering', review.current)
+  <>
+    <NoteEditor
+      data-testid="decisionComment"
+      key="note-input"
+      onBlur={() => {}}
+      onChange={value => {
+        // review.current.decisionComment.content = value
+        updateReview({
+          decisionComment: { content: value },
         })
       }}
+      placeholder="Write/paste your decision letter here, or upload it using the upload button on the right."
+      value={field.value?.content || ''}
     />
-    <Flexbox>
-      {getCommentFiles(field.value, 'note').map(val => {
-        const file = cloneDeep(val)
-        file.name = file.filename
-        return <UploadingFile file={file} key={file.name} uploaded />
-      })}
-    </Flexbox>
   </>
 )
 
@@ -133,14 +123,14 @@ const RecommendationInput = ({
   )
 }
 
-const DecisionForm = ({ handleSubmit, uploadFile, updateReview, isValid }) => (
+const DecisionForm = ({ handleSubmit, updateReview, isValid }) => (
   <Container key="decisionform">
     <form onSubmit={handleSubmit}>
       <SectionHeader>
         <Title>Decision</Title>
       </SectionHeader>
       <SectionRow key="note">
-        <NoteDecision updateReview={updateReview} uploadFile={uploadFile} />
+        <NoteDecision updateReview={updateReview} />
       </SectionRow>
       <SectionRowGrid>
         <Field
diff --git a/app/components/component-review/src/components/decision/DecisionReviews.js b/app/components/component-review/src/components/decision/DecisionReviews.js
index b7bcab3bd4..5e689ae818 100644
--- a/app/components/component-review/src/components/decision/DecisionReviews.js
+++ b/app/components/component-review/src/components/decision/DecisionReviews.js
@@ -15,7 +15,7 @@ const getCompletedReviews = (manuscript, currentUser) => {
   if (!team.members) {
     return null
   }
-  const currentMember = team.members.find(m => m.user.id === currentUser.id)
+  const currentMember = team.members.find(m => m.user?.id === currentUser?.id)
   return currentMember && currentMember.status
 }
 
diff --git a/app/components/component-review/src/components/review/Review.js b/app/components/component-review/src/components/review/Review.js
index e8e3c65dc6..596489beeb 100644
--- a/app/components/component-review/src/components/review/Review.js
+++ b/app/components/component-review/src/components/review/Review.js
@@ -3,7 +3,6 @@ import styled from 'styled-components'
 import { NoteViewer } from 'xpub-edit'
 import { Attachment } from '@pubsweet/ui'
 import { th, grid } from '@pubsweet/ui-toolkit'
-import { getCommentFiles } from './util'
 
 const Heading = styled.div``
 const Note = styled.div`
@@ -23,17 +22,12 @@ const filesToAttachment = file => ({
   url: file.url,
 })
 
-const findComments = (review = {}, type) => {
-  const comments = review.comments || []
-  return comments.find(comment => comment.type === type)
-}
-
 const ReviewComments = (review, type) => (
   <Note>
     <Content>
-      <NoteViewer value={findComments(review, type).content} />
+      <NoteViewer value={review[`${type}Comment`].content} />
     </Content>
-    {getCommentFiles(review, type).map(attachment => (
+    {review[`${type}Comment`].files.map(attachment => (
       <Attachment
         file={filesToAttachment(attachment)}
         key={attachment.url}
@@ -45,14 +39,14 @@ const ReviewComments = (review, type) => (
 
 const Review = ({ review }) => (
   <Container>
-    {findComments(review, 'note') && (
+    {review.reviewComment && (
       <div>
-        <Heading>Note</Heading>
+        <Heading>Review</Heading>
 
-        {ReviewComments(review, 'note')}
+        {ReviewComments(review, 'review')}
       </div>
     )}
-    {findComments(review, 'confidential') && (
+    {review.confidentialComment && (
       <div>
         <Heading>Confidential</Heading>
 
diff --git a/app/components/component-review/src/components/review/ReviewForm.js b/app/components/component-review/src/components/review/ReviewForm.js
index 006bf2dbef..d681506501 100644
--- a/app/components/component-review/src/components/review/ReviewForm.js
+++ b/app/components/component-review/src/components/review/ReviewForm.js
@@ -1,17 +1,10 @@
 import React, { useContext } from 'react'
-import { cloneDeep, set } from 'lodash'
 import { Field } from 'formik'
 import { NoteEditor } from 'xpub-edit'
-import {
-  Button,
-  Flexbox,
-  RadioGroup,
-  UploadButton,
-  UploadingFile,
-} from '@pubsweet/ui'
+import { Button, RadioGroup } from '@pubsweet/ui'
 
 import { JournalContext } from '../../../../xpub-journal/src'
-import { getCommentFiles, stripHtml, createComments } from './util'
+import { reviewWithComment } from './util'
 import {
   AdminSection,
   Container,
@@ -21,46 +14,47 @@ import {
   SectionRow,
   SectionAction,
 } from '../style'
+import { FilesUpload } from '../../../../shared'
 
-const AttachmentsInput = ({
-  field,
-  form: { values },
-  updateReview,
-  uploadFile,
-  type,
-}) => (
-  <>
-    <UploadButton
-      buttonText="↑ Upload files"
-      onChange={event => {
-        const val = event.target.files[0]
-        const file = cloneDeep(val)
-        file.filename = val.name
-        file.type = type
+// const AttachmentsInput = ({
+//   field,
+//   form: { values },
+//   updateReview,
+//   uploadFile,
+//   type,
+// }) => (
+//   <>
+//     <UploadButton
+//       buttonText="↑ Upload files"
+//       onChange={event => {
+//         const val = event.target.files[0]
+//         const file = cloneDeep(val)
+//         file.filename = val.name
+//         file.type = type
 
-        const { updateIndex, comment } = createComments(
-          values,
-          { files: [file] },
-          type,
-        )
+//         const { updateIndex, comment } = createComments(
+//           values,
+//           { files: [file] },
+//           type,
+//         )
 
-        const data = cloneDeep(values)
-        set(data, `comments.${updateIndex}`, comment)
+//         const data = cloneDeep(values)
+//         set(data, `comments.${updateIndex}`, comment)
 
-        updateReview(data).then(({ data: { updateReview } }) => {
-          uploadFile(val, updateReview, type)
-        })
-      }}
-    />
-    <Flexbox>
-      {getCommentFiles(values, type).map(val => {
-        const file = cloneDeep(val)
-        file.name = file.filename
-        return <UploadingFile file={file} key={file.name} uploaded />
-      })}
-    </Flexbox>
-  </>
-)
+//         updateReview(data).then(({ data: { updateReview } }) => {
+//           uploadFile(val, updateReview, type)
+//         })
+//       }}
+//     />
+//     <Flexbox>
+//       {getCommentFiles(values, type).map(val => {
+//         const file = cloneDeep(val)
+//         file.name = file.filename
+//         return <UploadingFile file={file} key={file.name} uploaded />
+//       })}
+//     </Flexbox>
+//   </>
+// )
 
 const NoteInput = ({
   field,
@@ -69,26 +63,22 @@ const NoteInput = ({
   ...rest
 }) => (
   <NoteEditor
+    data-testid="reviewComment"
     key="note-comment"
     placeholder="Enter your review…"
     title="Comments to the Author"
     {...field}
     onBlur={value => {
-      const { comment } = createComments(
-        values,
-        {
-          type: 'note',
-          content: stripHtml(value),
-        },
-        'note',
-      )
-
-      setFieldValue(`comments.0`, comment)
-      const data = cloneDeep(values)
-      set(data, `comments.0`, comment)
-      updateReview(data)
+      // const review = reviewWithComment({
+      //   id: values.reviewComment?.id,
+      //   value,
+      //   values,
+      //   commentType: 'review',
+      //   name: 'reviewComment',
+      // })
+      updateReview({ reviewComment: { content: value } })
     }}
-    value={field.value || ''}
+    value={field.value?.content || ''}
   />
 )
 
@@ -98,26 +88,22 @@ const ConfidentialInput = ({
   updateReview,
 }) => (
   <NoteEditor
+    data-testid="confidentialComment"
     key="confidential-comment"
     placeholder="Enter a confidential note to the editor (optional)…"
     title="Confidential Comments to Editor (Optional)"
     {...field}
     onBlur={value => {
-      const { comment } = createComments(
-        values,
-        {
-          type: 'confidential',
-          content: stripHtml(value),
-        },
-        'confidential',
-      )
-
-      setFieldValue(`comments.1`, comment)
-      const data = cloneDeep(values)
-      set(data, `comments.1`, comment)
-      updateReview(data)
+      // const review = reviewWithComment({
+      //   id: values.confidentialComment?.id,
+      //   value,
+      //   values,
+      //   commentType: 'confidential',
+      //   name: 'confidentialComment',
+      // })
+      updateReview({ confidentialComment: { content: value } })
     }}
-    value={field.value || ''}
+    value={field.value?.content || ''}
   />
 )
 
@@ -127,39 +113,76 @@ const RecommendationInput = ({ field, form: { values }, updateReview }) => {
     <RadioGroup
       inline
       {...field}
+      data-testid="recommendation"
       onChange={val => {
-        const data = cloneDeep(values)
-        set(data, 'recommendation', val)
-        updateReview(data)
+        updateReview({ recommendation: val })
       }}
       options={journal.recommendations}
     />
   )
 }
-const ReviewComment = props => (
+const ReviewComment = ({ updateReview }) => (
   <>
     <AdminSection>
       <div name="note">
-        <Field key="noteField" name="comments.0.content">
-          {extraProps => <NoteInput {...props} {...extraProps} />}
-        </Field>
-        <Field
-          component={extraProps => (
-            <AttachmentsInput type="note" {...props} {...extraProps} />
+        <Field key="noteField" name="reviewComment">
+          {formikBag => (
+            <>
+              <NoteInput updateReview={updateReview} {...formikBag} />
+              <FilesUpload
+                containerId={formikBag.field.value?.id}
+                containerName="reviewComment"
+                fieldName="reviewComment.files"
+                initializeContainer={async () => {
+                  // If the container for the uploaded files is not present,
+                  // we have to create it. InitializeContainer will be called
+                  // if containerId is undefined
+                  const review = reviewWithComment({
+                    commentType: 'review',
+                    values: formikBag.form.values,
+                    name: 'reviewComment',
+                  })
+                  // This is an upsert
+                  const { data } = await updateReview(review)
+                  // And we the return the file container id, so
+                  // that we have somewhere to attach uploaded files
+                  return data.updateReview.reviewComment.id
+                }}
+              />
+            </>
           )}
-        />
+        </Field>
       </div>
     </AdminSection>
     <AdminSection>
       <div name="confidential">
-        <Field key="confidentialField" name="comments.1.content">
-          {extraProps => <ConfidentialInput {...props} {...extraProps} />}
-        </Field>
-        <Field
-          component={extraProps => (
-            <AttachmentsInput type="confidential" {...props} {...extraProps} />
+        <Field key="confidentialField" name="confidentialComment">
+          {formikBag => (
+            <>
+              <ConfidentialInput updateReview={updateReview} {...formikBag} />
+              <FilesUpload
+                containerId={formikBag.field.value?.id}
+                containerName="reviewComment"
+                fieldName="confidentialComment.files"
+                initializeContainer={async () => {
+                  // If the container for the uploaded files is not present,
+                  // we have to create it. InitializeContainer will be called
+                  // if containerId is undefined
+                  const review = reviewWithComment({
+                    commentType: 'confidential',
+                    values: formikBag.form.values,
+                    name: 'confidentialComment',
+                  })
+                  // This is an upsert
+                  const { data } = await updateReview(review)
+                  // And we the return the file container id, so
+                  // that we have somewhere to attach uploaded files
+                  return data.updateReview.confidentialComment.id
+                }}
+              />
+            </>
           )}
-        />
+        </Field>
       </div>
     </AdminSection>
   </>
@@ -181,11 +204,7 @@ const ReviewForm = ({
           <Title>Review</Title>
         </SectionHeader>
         <SectionRow key="note">
-          <ReviewComment
-            review={review}
-            updateReview={updateReview}
-            uploadFile={uploadFile}
-          />
+          <ReviewComment review={review} updateReview={updateReview} />
         </SectionRow>
         <SectionHeader>
           <Title>Recommendation</Title>
diff --git a/app/components/component-review/src/components/review/util.js b/app/components/component-review/src/components/review/util.js
index a760b1ba6d..b382a48263 100644
--- a/app/components/component-review/src/components/review/util.js
+++ b/app/components/component-review/src/components/review/util.js
@@ -4,35 +4,21 @@ export const stripHtml = htmlString => {
   return temp.textContent
 }
 
-export const getCommentFiles = (review = {}, type) => {
-  const comments =
-    (review.comments || []).find(comment => (comment || {}).type === type) || {}
-  return comments.files || []
-}
-
-export const getCommentContent = (review = {}, type) => {
-  const comments =
-    (review.comments || []).find(comment => (comment || {}).type === type) || {}
-  return comments.content || ''
-}
-
-export const createComments = (values, val, type) => {
-  let updateIndex = (values.comments || []).findIndex(
-    comment => (comment || {}).type === type,
-  )
-  updateIndex =
-    (values.comments || []).length > 0 && updateIndex < 0 ? 1 : updateIndex
-  updateIndex = updateIndex < 0 ? 0 : updateIndex
-
-  const comment = Object.assign(
-    {
-      type,
-      content: '',
-      files: [],
-    },
-    (values.comments || [])[updateIndex],
-    val,
-  )
+export const reviewWithComment = ({
+  id,
+  value,
+  values,
+  commentType,
+  name,
+  isDecision,
+}) => {
+  const data = { id: values.id }
 
-  return { updateIndex, comment }
+  data.isDecision = isDecision
+  data[name] = {
+    id,
+    commentType,
+    content: value ? stripHtml(value) : '',
+  }
+  return data
 }
diff --git a/app/components/component-review/src/components/reviewers/Reviewers.js b/app/components/component-review/src/components/reviewers/Reviewers.js
index 7935205aaf..5ff9ad3024 100644
--- a/app/components/component-review/src/components/reviewers/Reviewers.js
+++ b/app/components/component-review/src/components/reviewers/Reviewers.js
@@ -43,9 +43,7 @@ const Reviewers = ({
       <Heading>Reviewers</Heading>
       <Button
         onClick={() =>
-          history.push(
-            `/journal/versions/${manuscript.id}/decisions/${manuscript.id}`,
-          )
+          history.push(`/journal/versions/${manuscript.id}/decision`)
         }
         primary
       >
-- 
GitLab