From fe0b497334bae8f777edc9aaea963e72bd61f3db Mon Sep 17 00:00:00 2001
From: Alexandru Munteanu <alexandru.munt@gmail.com>
Date: Thu, 27 Sep 2018 14:43:05 +0300
Subject: [PATCH] feat(reviewer-report): reviewer report form

---
 .../component-faraday-selectors/src/index.js  |  40 +-
 packages/component-faraday-ui/src/File.js     |   3 +
 packages/component-faraday-ui/src/Textarea.js |   4 -
 .../src/contextualBoxes/ReviewerReportForm.js | 174 ++++++++
 .../src/contextualBoxes/ReviewerReportForm.md |  10 +
 .../src/contextualBoxes/index.js              |   1 +
 .../ManuscriptFileSection.js                  |   8 +-
 .../src/components/ManuscriptLayout.js        |   8 +
 .../src/components/ManuscriptPage.js          |   5 +
 .../src/components/ReviewerReportForm.js      | 391 ++++++++----------
 .../src/components/ReviewerReportForm.old.js  | 298 +++++++++++++
 .../src/components/utils.js                   | 112 +++--
 packages/component-manuscript/src/index.js    |   1 -
 .../component-manuscript/src/redux/index.js   |   1 -
 .../src/redux/recommendations.js              |  51 +--
 .../src/components/withModal.js               |   1 +
 .../components-faraday/src/redux/files.js     |  58 +--
 .../src/redux/recommendations.js              |  20 +-
 18 files changed, 789 insertions(+), 397 deletions(-)
 create mode 100644 packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js
 create mode 100644 packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md
 create mode 100644 packages/component-manuscript/src/components/ReviewerReportForm.old.js
 delete mode 100644 packages/component-manuscript/src/redux/index.js

diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js
index 6c9ebc928..46e0a68e5 100644
--- a/packages/component-faraday-selectors/src/index.js
+++ b/packages/component-faraday-selectors/src/index.js
@@ -8,18 +8,6 @@ export const isHEToManuscript = (state, collectionId) => {
   return get(collection, 'handlingEditor.id') === currentUserId
 }
 
-const canMakeRecommendationStatuses = [
-  'heAssigned',
-  'underReview',
-  'reviewCompleted',
-]
-export const canMakeRecommendation = (state, collection, fragment = {}) => {
-  if (fragment.id !== last(get(collection, 'fragments', []))) return false
-  const isHE = isHEToManuscript(state, get(collection, 'id', ''))
-  const status = get(collection, 'status', 'draft')
-  return isHE && canMakeRecommendationStatuses.includes(status)
-}
-
 export const currentUserIs = ({ currentUser: { user } }, role) => {
   const isAdmin = get(user, 'admin')
   const isEic = get(user, 'editorInChief')
@@ -228,3 +216,31 @@ export const getInvitationsWithReviewersForFragment = (state, fragmentId) =>
       ),
     }))
     .value()
+
+// #region Editorial and reviewer recommendations
+const canMakeRecommendationStatuses = [
+  'heAssigned',
+  'underReview',
+  'reviewCompleted',
+]
+export const canMakeRecommendation = (state, collection, fragment = {}) => {
+  if (fragment.id !== last(get(collection, 'fragments', []))) return false
+  const isHE = isHEToManuscript(state, get(collection, 'id', ''))
+  const status = get(collection, 'status', 'draft')
+  return isHE && canMakeRecommendationStatuses.includes(status)
+}
+
+export const getFragmentRecommendations = (state, fragmentId) =>
+  get(state, `fragments.${fragmentId}.recommendations`, [])
+
+export const getFragmentReviewerRecommendations = (state, fragmentId) =>
+  getFragmentRecommendations(state, fragmentId).filter(
+    r => r.recommendationType === 'review',
+  )
+
+export const getOwnRecommendation = (state, fragmentId) =>
+  chain(state)
+    .get(`fragments.${fragmentId}.recommendations`, [])
+    .find(r => r.userId === get(state, 'currentUser.user.id', ''))
+    .value()
+// #endregion
diff --git a/packages/component-faraday-ui/src/File.js b/packages/component-faraday-ui/src/File.js
index 99a4afea1..4c6014d3e 100644
--- a/packages/component-faraday-ui/src/File.js
+++ b/packages/component-faraday-ui/src/File.js
@@ -56,6 +56,7 @@ const FileItem = ({
         ml={1}
         mr={1}
         onClick={onPreview}
+        pt={1 / 2}
         secondary
       />
     )}
@@ -65,6 +66,7 @@ const FileItem = ({
       ml={hasPreview ? 0 : 1}
       mr={1}
       onClick={onDownload}
+      pt={1 / 2}
       secondary
     />
     {hasDelete && (
@@ -73,6 +75,7 @@ const FileItem = ({
         iconSize={2}
         mr={1}
         onClick={onDelete}
+        pt={1 / 2}
         secondary
       />
     )}
diff --git a/packages/component-faraday-ui/src/Textarea.js b/packages/component-faraday-ui/src/Textarea.js
index bd385619a..122cf5a68 100644
--- a/packages/component-faraday-ui/src/Textarea.js
+++ b/packages/component-faraday-ui/src/Textarea.js
@@ -32,9 +32,5 @@ const Textarea = styled.textarea`
   }
 `
 
-Textarea.defaultProps = {
-  mb: 1,
-}
-
 /** @component */
 export default Textarea
diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js
new file mode 100644
index 000000000..3090f3363
--- /dev/null
+++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js
@@ -0,0 +1,174 @@
+import React, { Fragment } from 'react'
+import { reduxForm } from 'redux-form'
+import styled from 'styled-components'
+import { th } from '@pubsweet/ui-toolkit'
+import { required } from 'xpub-validators'
+import { compose, withHandlers, withStateHandlers } from 'recompose'
+import { withModal } from 'pubsweet-component-modal/src/components'
+import { Button, FilePicker, Menu, ValidatedField } from '@pubsweet/ui'
+
+import {
+  Row,
+  Item,
+  Label,
+  FileItem,
+  Textarea,
+  ActionLink,
+  MultiAction,
+  ContextualBox,
+  ItemOverrideAlert,
+  withFetching,
+} from 'pubsweet-component-faraday-ui/src'
+
+const options = [
+  { value: 'a', label: 'a' },
+  { value: 'b', label: 'b' },
+  { value: 'c', label: 'c' },
+  { value: 'd', label: 'd' },
+]
+
+const testFiles = [
+  {
+    id:
+      '8dca903a-05b9-45ab-89b9-9cb99a9a29c6/02db6c5e-2938-45ac-a5ee-67ae63919bb2',
+    name: 'Supplementary File 1.jpg',
+    size: 59621,
+    originalName: 'Supplementary File 1.jpg',
+  },
+  {
+    id:
+      '8dca903a-05b9-45ab-89b9-9cb99a9a29c6/5e69e3d9-7f9d-4e8d-b649-6e6a45658d75',
+    name: 'Supplementary File 2.docx',
+    size: 476862,
+    originalName: 'Supplementary File 2.docx',
+  },
+]
+
+const ReviewerReportForm = ({
+  file,
+  addFile,
+  expanded,
+  toggleMenu,
+  handleSubmit,
+}) => (
+  <ContextualBox label="Your report" startExpanded>
+    <Root>
+      <Row>
+        <ItemOverrideAlert vertical>
+          <Label required>Recommendation</Label>
+          <ValidatedField
+            component={input => <Menu {...input} options={options} />}
+            name="recommendation"
+            validate={[required]}
+          />
+        </ItemOverrideAlert>
+      </Row>
+
+      <Row alignItems="center" justify="space-between" mt={1}>
+        <Item>
+          <Label required>Your report</Label>
+          <FilePicker onUpload={addFile}>
+            <ActionLink icon="plus">UPLOAD FILE</ActionLink>
+          </FilePicker>
+        </Item>
+
+        <Item justify="flex-end">
+          <ActionLink to="https://about.hindawi.com/authors/peer-review-at-hindawi/">
+            Hindawi Reviewer Guidelines
+          </ActionLink>
+        </Item>
+      </Row>
+
+      <Row mb={1}>
+        <ItemOverrideAlert vertical>
+          <ValidatedField
+            component={Textarea}
+            name="message"
+            validate={[required]}
+          />
+        </ItemOverrideAlert>
+      </Row>
+
+      <Row justify="flex-start">
+        <Item flex={0}>
+          <FileItem item={file} />
+        </Item>
+      </Row>
+
+      <Row alignItems="center">
+        {expanded ? (
+          <Fragment>
+            <Item>
+              <Label>Confidential note for the Editorial Team</Label>
+            </Item>
+            <Item justify="flex-end">
+              <ActionLink icon="x" onClick={toggleMenu}>
+                Remove
+              </ActionLink>
+            </Item>
+          </Fragment>
+        ) : (
+          <Item>
+            <ActionLink onClick={toggleMenu}>
+              Add Confidential note for the Editorial Team
+            </ActionLink>
+          </Item>
+        )}
+      </Row>
+
+      {expanded && (
+        <Row>
+          <ItemOverrideAlert pr={2} vertical>
+            <ValidatedField component={Textarea} name="note" />
+          </ItemOverrideAlert>
+        </Row>
+      )}
+
+      <Row justify="flex-end" mt={2}>
+        <Button onClick={handleSubmit} primary size="medium">
+          Submit report
+        </Button>
+      </Row>
+    </Root>
+  </ContextualBox>
+)
+
+export default compose(
+  withFetching,
+  withModal(() => ({
+    modalComponent: MultiAction,
+  })),
+  withStateHandlers(
+    { expanded: false, file: testFiles[0] },
+    {
+      toggleMenu: ({ expanded }) => () => ({ expanded: !expanded }),
+    },
+  ),
+  withHandlers({
+    addFile: () => file => {},
+  }),
+  reduxForm({
+    form: 'reviewer-report',
+    onSubmit: (
+      values,
+      dispatch,
+      { showModal, setFetching, onSubmitReport },
+    ) => {
+      showModal({
+        title: 'Ready to Submit your Report?',
+        subtitle: `Once submitted, the report can't be modified.`,
+        onConfirm: modalProps =>
+          onSubmitReport(values, { ...modalProps, setFetching }),
+        confirmText: 'Submit report',
+        cancelText: 'Not yet',
+      })
+    },
+  }),
+)(ReviewerReportForm)
+
+// #region styles
+const Root = styled.div`
+  background-color: ${th('colorBackgroundHue2')};
+  padding: calc(${th('gridUnit')} * 2);
+`
+// #endregion
diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md
new file mode 100644
index 000000000..bf3998da8
--- /dev/null
+++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md
@@ -0,0 +1,10 @@
+Reviewer report contextual box.
+
+```js
+<ReviewerReportForm
+  onSubmitReport={(values, { setFetching }) => {
+    console.log('submitting report', values)
+    setFetching(true)
+  }}
+/>
+```
diff --git a/packages/component-faraday-ui/src/contextualBoxes/index.js b/packages/component-faraday-ui/src/contextualBoxes/index.js
index d8e24b485..54a8bdd97 100644
--- a/packages/component-faraday-ui/src/contextualBoxes/index.js
+++ b/packages/component-faraday-ui/src/contextualBoxes/index.js
@@ -1,2 +1,3 @@
 export { default as AssignHE } from './AssignHE'
 export { default as ReviewerDetails } from './ReviewerDetails'
+export { default as ReviewerReportForm } from './ReviewerReportForm'
diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js
index 8b0976d54..de2087fa2 100644
--- a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js
+++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js
@@ -5,9 +5,11 @@ const ManuscriptFileSection = ({ list = [], label = '', ...rest }) => (
   <Fragment>
     {!!list.length && (
       <Fragment>
-        <Text labelLine mb={1} mt={1}>
-          {label}
-        </Text>
+        {label && (
+          <Text labelLine mb={1} mt={1}>
+            {label}
+          </Text>
+        )}
         <Row justify="flex-start" mb={1}>
           {list.map(file => (
             <Item
diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js
index e33c34c50..0099f2215 100644
--- a/packages/component-manuscript/src/components/ManuscriptLayout.js
+++ b/packages/component-manuscript/src/components/ManuscriptLayout.js
@@ -13,6 +13,8 @@ import {
   paddingHelper,
 } from 'pubsweet-component-faraday-ui'
 
+import ReviewerReportForm from './ReviewerReportForm'
+
 const eicDecisions = [
   { value: 'return-to-handling-editor', label: 'Return to Handling Editor' },
   { value: 'publish', label: 'Publish' },
@@ -78,6 +80,12 @@ const ManuscriptLayout = ({
           revokeInvitation={revokeHE}
         />
 
+        <ReviewerReportForm
+          modalKey="reviewer-report"
+          project={collection}
+          version={fragment}
+        />
+
         <ManuscriptMetadata
           currentUser={currentUser}
           fragment={fragment}
diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js
index ab2934aa0..02c7b2b50 100644
--- a/packages/component-manuscript/src/components/ManuscriptPage.js
+++ b/packages/component-manuscript/src/components/ManuscriptPage.js
@@ -44,6 +44,7 @@ import {
   parseCollectionDetails,
   pendingReviewerInvitation,
   canOverrideTechnicalChecks,
+  getFragmentReviewerRecommendations,
   getInvitationsWithReviewersForFragment,
 } from 'pubsweet-component-faraday-selectors'
 import { RemoteOpener, handleError } from 'pubsweet-component-faraday-ui'
@@ -93,6 +94,10 @@ export default compose(
         state,
         match.params.version,
       ),
+      reviewerRecommendations: getFragmentReviewerRecommendations(
+        state,
+        match.params.version,
+      ),
     }),
     {
       getSignedUrl,
diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js
index d0285b89b..64db430c3 100644
--- a/packages/component-manuscript/src/components/ReviewerReportForm.js
+++ b/packages/component-manuscript/src/components/ReviewerReportForm.js
@@ -1,197 +1,223 @@
 import React, { Fragment } from 'react'
+import { get } from 'lodash'
 import { connect } from 'react-redux'
-import { isEmpty, merge } from 'lodash'
+import styled from 'styled-components'
 import { th } from '@pubsweet/ui-toolkit'
 import { withJournal } from 'xpub-journal'
 import { required } from 'xpub-validators'
-import styled, { css } from 'styled-components'
-import { compose, withHandlers, withProps } from 'recompose'
-import { Menu, Icon, Button, ErrorText, ValidatedField } from '@pubsweet/ui'
+import { withModal } from 'pubsweet-component-modal/src/components'
+import { compose, withHandlers, withProps, withState } from 'recompose'
+import { reduxForm, getFormValues, change as changeForm } from 'redux-form'
+import { Menu, Button, FilePicker, Spinner, ValidatedField } from '@pubsweet/ui'
+
 import {
-  reduxForm,
-  isSubmitting,
-  getFormValues,
-  change as changeForm,
-} from 'redux-form'
+  Row,
+  Item,
+  Text,
+  Label,
+  FileItem,
+  Textarea,
+  ActionLink,
+  MultiAction,
+  ContextualBox,
+  ItemOverrideAlert,
+  handleError,
+  withFetching,
+  withFilePreview,
+  withFileDownload,
+} from 'pubsweet-component-faraday-ui'
+
+import { getOwnRecommendation } from 'pubsweet-component-faraday-selectors'
 
 import {
   uploadFile,
   deleteFile,
   getSignedUrl,
-  getRequestStatus,
 } from 'pubsweet-components-faraday/src/redux/files'
 
-import {
-  withModal,
-  ConfirmationModal,
-} from 'pubsweet-component-modal/src/components'
-
-import {
-  createRecommendation,
-  updateRecommendation,
-} from 'pubsweet-components-faraday/src/redux/recommendations'
-
 import {
   onReviewSubmit,
   onReviewChange,
+  reviewerReportValidate,
   parseReviewResponseToForm,
 } from './utils'
 
-const guidelinesLink =
-  'https://about.hindawi.com/authors/peer-review-at-hindawi/'
-
-const TextAreaField = input => <Textarea {...input} height={70} rows={6} />
-
 const ReviewerReportForm = ({
+  addNote,
   addFile,
+  expanded,
   fileError,
+  isFetching,
+  removeNote,
   removeFile,
-  changeField,
-  isSubmitting,
+  previewFile,
+  downloadFile,
   handleSubmit,
-  fileFetching,
+  fetchingError,
   review = {},
   formValues = {},
   journal: { recommendations },
 }) => (
-  <Root>
-    <Row>
-      <Label>Recommendation*</Label>
-      <ActionLink href={guidelinesLink} target="_blank">
-        Hindawi Reviewer Guidelines
-      </ActionLink>
-    </Row>
-    <Row>
-      <ValidatedField
-        component={input => (
-          <Menu
-            {...input}
-            inline
-            onChange={v => changeField('recommendation', v)}
-            options={recommendations}
-            placeholder="Please select"
+  <ContextualBox label="Your report" startExpanded>
+    <Root>
+      <Row justify="flex-start">
+        <ItemOverrideAlert flex={0} vertical>
+          <Label required>Recommendation</Label>
+          <ValidatedField
+            component={input => <Menu {...input} options={recommendations} />}
+            name="recommendation"
+            validate={[required]}
           />
-        )}
-        name="recommendation"
-        validate={[required]}
-      />
-    </Row>
+        </ItemOverrideAlert>
+      </Row>
 
-    <Spacing />
+      <Row alignItems="center" justify="space-between" mt={1}>
+        <Item>
+          <Label required>Your report</Label>
+          {!formValues.file && (
+            <FilePicker onUpload={addFile}>
+              <ActionLink icon="plus">UPLOAD FILE</ActionLink>
+            </FilePicker>
+          )}
+        </Item>
+
+        <Item justify="flex-end">
+          <ActionLink to="https://about.hindawi.com/authors/peer-review-at-hindawi/">
+            Hindawi Reviewer Guidelines
+          </ActionLink>
+        </Item>
+      </Row>
 
-    <Row>
-      <FullWidth className="full-width">
-        <ValidatedField
-          component={TextAreaField}
-          name="public"
-          validate={isEmpty(formValues.files) ? [required] : []}
-        />
-      </FullWidth>
-    </Row>
+      <Row mb={1 / 2}>
+        <ItemOverrideAlert vertical>
+          <ValidatedField component={Textarea} name="public" />
+        </ItemOverrideAlert>
+      </Row>
 
-    {formValues.hasConfidential ? (
-      <Fragment>
-        <Spacing />
-        <Row>
-          <Label>
-            Note for the editorial team <i>Not shared with the author</i>
-          </Label>
-          <ActionTextIcon onClick={() => changeField('hasConfidential', false)}>
-            <Icon primary size={3}>
-              x
-            </Icon>
-            Remove
-          </ActionTextIcon>
-        </Row>
-        <Row>
-          <FullWidth>
-            <ValidatedField
-              component={TextAreaField}
-              name="confidential"
-              validate={[required]}
+      {formValues.file && (
+        <Row justify="flex-start" mb={1}>
+          <Item flex={0}>
+            <FileItem
+              item={formValues.file}
+              onDelete={removeFile}
+              onDownload={downloadFile}
+              onPreview={previewFile}
             />
-          </FullWidth>
+          </Item>
         </Row>
-      </Fragment>
-    ) : (
-      <Row>
-        <ActionText onClick={() => changeField('hasConfidential', true)}>
-          Add confidential note for the Editorial Team
-        </ActionText>
+      )}
+
+      <Row alignItems="center">
+        {expanded ? (
+          <Fragment>
+            <Item>
+              <Label>Confidential note for the Editorial Team</Label>
+            </Item>
+            <Item justify="flex-end">
+              <ActionLink icon="x" onClick={removeNote}>
+                Remove
+              </ActionLink>
+            </Item>
+          </Fragment>
+        ) : (
+          <Item>
+            <ActionLink onClick={addNote}>
+              Add Confidential note for the Editorial Team
+            </ActionLink>
+          </Item>
+        )}
       </Row>
-    )}
 
-    <Spacing />
-    {fileError && (
-      <Row>
-        <ErrorText>{fileError}</ErrorText>
+      {expanded && (
+        <Row>
+          <ItemOverrideAlert vertical>
+            <ValidatedField component={Textarea} name="confidential" />
+          </ItemOverrideAlert>
+        </Row>
+      )}
+
+      {fetchingError && (
+        <Row justify="flex-start">
+          <Text error>{fetchingError}</Text>
+        </Row>
+      )}
+
+      <Row justify="flex-end" mt={1}>
+        {isFetching ? (
+          <Spinner />
+        ) : (
+          <Button onClick={handleSubmit} primary size="medium">
+            Submit report
+          </Button>
+        )}
       </Row>
-    )}
-    <Row>
-      <ActionButton onClick={handleSubmit}>Submit report</ActionButton>
-    </Row>
-  </Root>
+    </Root>
+  </ContextualBox>
 )
 
-const ModalWrapper = compose(
-  connect(state => ({
-    fetching: false,
-  })),
-)(({ fetching, ...rest }) => (
-  <ConfirmationModal {...rest} isFetching={fetching} />
-))
-
+// #region export
 export default compose(
   withJournal,
+  withFetching,
   connect(
-    state => ({
-      fileFetching: getRequestStatus(state),
+    (state, { version, ...rest }) => ({
+      review: getOwnRecommendation(state, version.id),
+      token: get(state, 'currentUser.user.token', ''),
       formValues: getFormValues('reviewerReport')(state),
-      isSubmitting: isSubmitting('reviewerReport')(state),
     }),
     {
-      uploadFile,
-      deleteFile,
       changeForm,
-      getSignedUrl,
-      getFormValues,
-      createRecommendation,
-      updateRecommendation,
     },
   ),
   withProps(({ review = {}, formValues = {} }) => ({
-    initialValues: merge(parseReviewResponseToForm(review), formValues),
+    getSignedUrl,
+    initialValues: parseReviewResponseToForm(review),
   })),
-  withModal(props => ({
-    modalComponent: ModalWrapper,
+  withFilePreview,
+  withFileDownload,
+  withModal(({ isFetching, modalKey }) => ({
+    modalKey,
+    isFetching,
+    modalComponent: MultiAction,
   })),
+  withState(
+    'expanded',
+    'setExpanded',
+    ({ review }) => get(review, 'comments', []).length === 2,
+  ),
   withHandlers({
-    changeField: ({ changeForm }) => (field, value) => {
-      changeForm('reviewerReport', field, value)
+    addNote: ({ setExpanded }) => () => {
+      setExpanded(true)
+    },
+    removeNote: ({ setExpanded, changeForm }) => () => {
+      changeForm('reviewerReport', 'confidential', '')
+      setExpanded(false)
     },
-    addFile: ({ formValues = {}, uploadFile, changeForm, version }) => file => {
-      uploadFile(file, 'review', version)
+    addFile: ({ version, changeForm, setFetching, setError }) => file => {
+      setFetching(true)
+      setError('')
+      uploadFile({ file, type: 'review', fragment: version })
         .then(file => {
-          const files = formValues.files || []
-          const newFiles = [...files, file]
-          changeForm('reviewerReport', 'files', newFiles)
+          setFetching(false)
+          changeForm('reviewerReport', 'file', file)
+        })
+        .catch(err => {
+          setFetching(false)
+          handleError(setError)(err)
         })
-        .catch(e => console.error(`Couldn't upload file.`, e))
     },
-    removeFile: ({
-      formValues: { files = [] },
-      changeForm,
-      deleteFile,
-    }) => id => e => {
-      deleteFile(id)
+    removeFile: ({ changeForm, setError, setFetching }) => file => {
+      setFetching(true)
+      setError('')
+      deleteFile(file.id)
         .then(r => {
-          const newFiles = files.filter(f => f.id !== id)
-          changeForm('reviewerReport', 'files', newFiles)
+          setFetching(false)
+          changeForm('reviewerReport', 'file', null)
+        })
+        .catch(err => {
+          setFetching(false)
+          handleError(setError)(err)
         })
-        .catch(e => console.error(`Couldn't delete the file.`, e))
-
-      const newFiles = files.filter(f => f.id !== id)
-      changeForm('reviewerReport', 'files', newFiles)
     },
   }),
   reduxForm({
@@ -200,99 +226,14 @@ export default compose(
     onSubmit: onReviewSubmit,
     enableReinitialize: false,
     keepDirtyOnReinitialize: true,
+    validate: reviewerReportValidate,
   }),
 )(ReviewerReportForm)
+// #endregion
 
 // #region styled-components
-
-const defaultText = css`
-  color: ${th('colorPrimary')};
-  font-family: ${th('fontReading')};
-  font-size: ${th('fontSizeBaseSmall')};
-`
 const Root = styled.div`
-  display: flex;
-  flex-direction: column;
-  margin: auto;
-  [role='listbox'] {
-    min-width: 280px;
-  }
-`
-
-const Label = styled.div`
-  ${defaultText};
-  text-transform: uppercase;
-  i {
-    text-transform: none;
-    margin-left: ${th('gridUnit')};
-  }
-`
-
-const ActionText = styled.span`
-  ${defaultText};
-  cursor: pointer;
-  margin-left: ${({ left }) => left || 0}px;
-  text-decoration: underline;
-`
-
-const ActionTextIcon = styled(ActionText)`
-  align-items: center;
-  display: flex;
-`
-const ActionLink = styled.a`
-  ${defaultText};
-`
-
-const Textarea = styled.textarea`
-  border-color: ${({ hasError }) =>
-    hasError ? th('colorError') : th('colorPrimary')};
-  font-size: ${th('fontSizeBaseSmall')};
-  font-family: ${th('fontWriting')};
-  padding: calc(${th('subGridUnit')} * 2);
-  transition: all 300ms linear;
-  width: 100%;
-
-  &:read-only {
-    background-color: ${th('colorBackgroundHue')};
-  }
-`
-
-const Spacing = styled.div`
-  flex: 1;
-  margin-top: calc(${th('gridUnit')} / 2);
-`
-
-const FullWidth = styled.div`
-  flex: 1;
-  > div {
-    flex: 1;
-  }
-`
-
-const Row = styled.div`
-  ${defaultText};
-  align-items: center;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: row;
-  flex: 1;
-  flex-wrap: wrap;
-  justify-content: ${({ left }) => (left ? 'left' : 'space-between')};
-
-  div[role='alert'] {
-    margin-top: 0;
-  }
-`
-
-const ActionButton = styled(Button)`
-  ${defaultText};
-  align-items: center;
-  background-color: ${th('colorPrimary')};
-  color: ${th('colorTextReverse')};
-  height: calc(${th('subGridUnit')} * 5);
-  display: flex;
-  padding: calc(${th('subGridUnit')} / 2) calc(${th('subGridUnit')});
-  text-align: center;
-  text-transform: uppercase;
+  background-color: ${th('colorBackgroundHue2')};
+  padding: calc(${th('gridUnit')} * 2);
 `
 // #endregion
diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.old.js b/packages/component-manuscript/src/components/ReviewerReportForm.old.js
new file mode 100644
index 000000000..d0285b89b
--- /dev/null
+++ b/packages/component-manuscript/src/components/ReviewerReportForm.old.js
@@ -0,0 +1,298 @@
+import React, { Fragment } from 'react'
+import { connect } from 'react-redux'
+import { isEmpty, merge } from 'lodash'
+import { th } from '@pubsweet/ui-toolkit'
+import { withJournal } from 'xpub-journal'
+import { required } from 'xpub-validators'
+import styled, { css } from 'styled-components'
+import { compose, withHandlers, withProps } from 'recompose'
+import { Menu, Icon, Button, ErrorText, ValidatedField } from '@pubsweet/ui'
+import {
+  reduxForm,
+  isSubmitting,
+  getFormValues,
+  change as changeForm,
+} from 'redux-form'
+
+import {
+  uploadFile,
+  deleteFile,
+  getSignedUrl,
+  getRequestStatus,
+} from 'pubsweet-components-faraday/src/redux/files'
+
+import {
+  withModal,
+  ConfirmationModal,
+} from 'pubsweet-component-modal/src/components'
+
+import {
+  createRecommendation,
+  updateRecommendation,
+} from 'pubsweet-components-faraday/src/redux/recommendations'
+
+import {
+  onReviewSubmit,
+  onReviewChange,
+  parseReviewResponseToForm,
+} from './utils'
+
+const guidelinesLink =
+  'https://about.hindawi.com/authors/peer-review-at-hindawi/'
+
+const TextAreaField = input => <Textarea {...input} height={70} rows={6} />
+
+const ReviewerReportForm = ({
+  addFile,
+  fileError,
+  removeFile,
+  changeField,
+  isSubmitting,
+  handleSubmit,
+  fileFetching,
+  review = {},
+  formValues = {},
+  journal: { recommendations },
+}) => (
+  <Root>
+    <Row>
+      <Label>Recommendation*</Label>
+      <ActionLink href={guidelinesLink} target="_blank">
+        Hindawi Reviewer Guidelines
+      </ActionLink>
+    </Row>
+    <Row>
+      <ValidatedField
+        component={input => (
+          <Menu
+            {...input}
+            inline
+            onChange={v => changeField('recommendation', v)}
+            options={recommendations}
+            placeholder="Please select"
+          />
+        )}
+        name="recommendation"
+        validate={[required]}
+      />
+    </Row>
+
+    <Spacing />
+
+    <Row>
+      <FullWidth className="full-width">
+        <ValidatedField
+          component={TextAreaField}
+          name="public"
+          validate={isEmpty(formValues.files) ? [required] : []}
+        />
+      </FullWidth>
+    </Row>
+
+    {formValues.hasConfidential ? (
+      <Fragment>
+        <Spacing />
+        <Row>
+          <Label>
+            Note for the editorial team <i>Not shared with the author</i>
+          </Label>
+          <ActionTextIcon onClick={() => changeField('hasConfidential', false)}>
+            <Icon primary size={3}>
+              x
+            </Icon>
+            Remove
+          </ActionTextIcon>
+        </Row>
+        <Row>
+          <FullWidth>
+            <ValidatedField
+              component={TextAreaField}
+              name="confidential"
+              validate={[required]}
+            />
+          </FullWidth>
+        </Row>
+      </Fragment>
+    ) : (
+      <Row>
+        <ActionText onClick={() => changeField('hasConfidential', true)}>
+          Add confidential note for the Editorial Team
+        </ActionText>
+      </Row>
+    )}
+
+    <Spacing />
+    {fileError && (
+      <Row>
+        <ErrorText>{fileError}</ErrorText>
+      </Row>
+    )}
+    <Row>
+      <ActionButton onClick={handleSubmit}>Submit report</ActionButton>
+    </Row>
+  </Root>
+)
+
+const ModalWrapper = compose(
+  connect(state => ({
+    fetching: false,
+  })),
+)(({ fetching, ...rest }) => (
+  <ConfirmationModal {...rest} isFetching={fetching} />
+))
+
+export default compose(
+  withJournal,
+  connect(
+    state => ({
+      fileFetching: getRequestStatus(state),
+      formValues: getFormValues('reviewerReport')(state),
+      isSubmitting: isSubmitting('reviewerReport')(state),
+    }),
+    {
+      uploadFile,
+      deleteFile,
+      changeForm,
+      getSignedUrl,
+      getFormValues,
+      createRecommendation,
+      updateRecommendation,
+    },
+  ),
+  withProps(({ review = {}, formValues = {} }) => ({
+    initialValues: merge(parseReviewResponseToForm(review), formValues),
+  })),
+  withModal(props => ({
+    modalComponent: ModalWrapper,
+  })),
+  withHandlers({
+    changeField: ({ changeForm }) => (field, value) => {
+      changeForm('reviewerReport', field, value)
+    },
+    addFile: ({ formValues = {}, uploadFile, changeForm, version }) => file => {
+      uploadFile(file, 'review', version)
+        .then(file => {
+          const files = formValues.files || []
+          const newFiles = [...files, file]
+          changeForm('reviewerReport', 'files', newFiles)
+        })
+        .catch(e => console.error(`Couldn't upload file.`, e))
+    },
+    removeFile: ({
+      formValues: { files = [] },
+      changeForm,
+      deleteFile,
+    }) => id => e => {
+      deleteFile(id)
+        .then(r => {
+          const newFiles = files.filter(f => f.id !== id)
+          changeForm('reviewerReport', 'files', newFiles)
+        })
+        .catch(e => console.error(`Couldn't delete the file.`, e))
+
+      const newFiles = files.filter(f => f.id !== id)
+      changeForm('reviewerReport', 'files', newFiles)
+    },
+  }),
+  reduxForm({
+    form: 'reviewerReport',
+    onChange: onReviewChange,
+    onSubmit: onReviewSubmit,
+    enableReinitialize: false,
+    keepDirtyOnReinitialize: true,
+  }),
+)(ReviewerReportForm)
+
+// #region styled-components
+
+const defaultText = css`
+  color: ${th('colorPrimary')};
+  font-family: ${th('fontReading')};
+  font-size: ${th('fontSizeBaseSmall')};
+`
+const Root = styled.div`
+  display: flex;
+  flex-direction: column;
+  margin: auto;
+  [role='listbox'] {
+    min-width: 280px;
+  }
+`
+
+const Label = styled.div`
+  ${defaultText};
+  text-transform: uppercase;
+  i {
+    text-transform: none;
+    margin-left: ${th('gridUnit')};
+  }
+`
+
+const ActionText = styled.span`
+  ${defaultText};
+  cursor: pointer;
+  margin-left: ${({ left }) => left || 0}px;
+  text-decoration: underline;
+`
+
+const ActionTextIcon = styled(ActionText)`
+  align-items: center;
+  display: flex;
+`
+const ActionLink = styled.a`
+  ${defaultText};
+`
+
+const Textarea = styled.textarea`
+  border-color: ${({ hasError }) =>
+    hasError ? th('colorError') : th('colorPrimary')};
+  font-size: ${th('fontSizeBaseSmall')};
+  font-family: ${th('fontWriting')};
+  padding: calc(${th('subGridUnit')} * 2);
+  transition: all 300ms linear;
+  width: 100%;
+
+  &:read-only {
+    background-color: ${th('colorBackgroundHue')};
+  }
+`
+
+const Spacing = styled.div`
+  flex: 1;
+  margin-top: calc(${th('gridUnit')} / 2);
+`
+
+const FullWidth = styled.div`
+  flex: 1;
+  > div {
+    flex: 1;
+  }
+`
+
+const Row = styled.div`
+  ${defaultText};
+  align-items: center;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: row;
+  flex: 1;
+  flex-wrap: wrap;
+  justify-content: ${({ left }) => (left ? 'left' : 'space-between')};
+
+  div[role='alert'] {
+    margin-top: 0;
+  }
+`
+
+const ActionButton = styled(Button)`
+  ${defaultText};
+  align-items: center;
+  background-color: ${th('colorPrimary')};
+  color: ${th('colorTextReverse')};
+  height: calc(${th('subGridUnit')} * 5);
+  display: flex;
+  padding: calc(${th('subGridUnit')} / 2) calc(${th('subGridUnit')});
+  text-align: center;
+  text-transform: uppercase;
+`
+// #endregion
diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js
index 290712b16..28d7fef24 100644
--- a/packages/component-manuscript/src/components/utils.js
+++ b/packages/component-manuscript/src/components/utils.js
@@ -10,13 +10,19 @@ import {
 } from 'lodash'
 
 import { actions } from 'pubsweet-client/src'
-import { change as changeForm } from 'redux-form'
+import { handleError } from 'pubsweet-component-faraday-ui'
+
 import {
   autosaveRequest,
-  autosaveSuccess,
   autosaveFailure,
+  autosaveSuccess,
 } from 'pubsweet-component-wizard/src/redux/autosave'
 
+import {
+  createRecommendation,
+  updateRecommendation,
+} from '../redux/recommendations'
+
 export const parseTitle = version => {
   const title = get(version, 'metadata.title')
   if (title) {
@@ -99,15 +105,14 @@ export const redirectToError = redirectFn => err => {
 
 export const parseReviewResponseToForm = (review = {}) => {
   if (isEmpty(review)) return {}
-  const comments = review.comments || []
+  const comments = get(review, 'comments', [])
   const publicComment = comments.find(c => c.public)
   const privateComment = comments.find(c => !c.public)
   return {
     ...review,
     public: get(publicComment, 'content'),
-    files: get(publicComment, 'files'),
+    file: get(publicComment, 'files.0', null),
     confidential: get(privateComment, 'content'),
-    hasConfidential: !!get(privateComment, 'content'),
   }
 }
 
@@ -116,12 +121,12 @@ export const parseReviewRequest = (review = {}) => {
   const comments = [
     {
       public: true,
-      content: review.public || undefined,
-      files: review.files || [],
+      files: [get(review, 'file', {})],
+      content: get(review, 'public', ''),
     },
   ]
 
-  if (review.hasConfidential) {
+  if (get(review, 'confidential', '')) {
     comments.push({
       public: false,
       content: review.confidential || undefined,
@@ -129,39 +134,57 @@ export const parseReviewRequest = (review = {}) => {
     })
   }
   return {
-    ...omit(review, [
-      'public',
-      'confidential',
-      'hasConfidential',
-      'files',
-      'userId',
-    ]),
-    recommendationType: 'review',
+    id: get(review, 'id', null),
     comments,
+    recommendationType: 'review',
+    recommendation: get(review, 'recommendation', 'publish'),
   }
 }
 
-const onChange = (
-  values,
-  dispatch,
-  { project, version, createRecommendation, updateRecommendation },
-) => {
+export const reviewerReportValidate = values => {
+  const errors = {}
+
+  if (!values.public && !values.file) {
+    errors.public = 'A file or a public report is required.'
+  }
+
+  return errors
+}
+
+const onChange = (values, dispatch, { project, version }) => {
   const newValues = parseReviewRequest(values)
-  // if (!isEqual(newValues, prevValues)) {
   dispatch(autosaveRequest())
   if (newValues.id) {
-    updateRecommendation(project.id, version.id, newValues)
-      .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
-      .catch(e => dispatch(autosaveFailure(e)))
+    updateRecommendation({
+      fragmentId: version.id,
+      collectionId: project.id,
+      recommendation: newValues,
+    }).then(
+      r => {
+        dispatch(autosaveSuccess(Date.now()))
+        return r
+      },
+      err => {
+        dispatch(autosaveFailure())
+        throw err
+      },
+    )
   } else {
-    createRecommendation(project.id, version.id, newValues)
-      .then(r => {
-        dispatch(changeForm('reviewerReport', 'id', r.id))
-        return dispatch(autosaveSuccess(get(r, 'updatedOn')))
-      })
-      .catch(e => dispatch(autosaveFailure(e)))
+    createRecommendation({
+      fragmentId: version.id,
+      collectionId: project.id,
+      recommendation: omit(newValues, 'id'),
+    }).then(
+      r => {
+        dispatch(autosaveSuccess(Date.now()))
+        return r
+      },
+      err => {
+        dispatch(autosaveFailure())
+        throw err
+      },
+    )
   }
-  // }
 }
 
 export const onReviewChange = debounce(onChange, 1000, { maxWait: 5000 })
@@ -169,31 +192,30 @@ export const onReviewChange = debounce(onChange, 1000, { maxWait: 5000 })
 export const onReviewSubmit = (
   values,
   dispatch,
-  {
-    project,
-    version,
-    showModal,
-    hideModal,
-    isSubmitting,
-    updateRecommendation,
-  },
+  { project, version, showModal, setFetching, isSubmitting },
 ) => {
   showModal({
     title: 'Ready to Submit your Report?',
     subtitle: 'Once submitted, the report can`t be modified',
     confirmText: 'Submit report',
-    onConfirm: () => {
+    onConfirm: ({ hideModal, setModalError }) => {
+      setFetching(true)
       const newValues = parseReviewRequest(values)
       newValues.submittedOn = Date.now()
-      dispatch(autosaveRequest())
-      updateRecommendation(project.id, version.id, newValues)
-        .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
+      updateRecommendation({
+        fragmentId: version.id,
+        collectionId: project.id,
+        recommendation: newValues,
+      })
         .then(() => {
           dispatch(actions.getFragments({ id: project.id }))
           hideModal()
         })
+        .catch(err => {
+          setFetching(false)
+          handleError(setModalError)(err)
+        })
     },
-    onCancel: hideModal,
   })
 }
 
diff --git a/packages/component-manuscript/src/index.js b/packages/component-manuscript/src/index.js
index 9e0071c95..01aa93785 100644
--- a/packages/component-manuscript/src/index.js
+++ b/packages/component-manuscript/src/index.js
@@ -3,7 +3,6 @@ module.exports = {
     components: [() => require('./components')],
     reducers: {
       editors: () => require('./redux/editors').default,
-      recommendations: () => require('./redux/recommendations').default,
     },
   },
 }
diff --git a/packages/component-manuscript/src/redux/index.js b/packages/component-manuscript/src/redux/index.js
deleted file mode 100644
index 05f36f6d4..000000000
--- a/packages/component-manuscript/src/redux/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as editors } from './editors'
diff --git a/packages/component-manuscript/src/redux/recommendations.js b/packages/component-manuscript/src/redux/recommendations.js
index 9b530a316..154e043de 100644
--- a/packages/component-manuscript/src/redux/recommendations.js
+++ b/packages/component-manuscript/src/redux/recommendations.js
@@ -1,17 +1,6 @@
 import { get } from 'lodash'
 import { create, update } from 'pubsweet-client/src/helpers/api'
 
-const RECOMMENDATION_REQUEST = 'recommendation/REQUEST'
-const RECOMMENDATION_DONE = 'recommendation/DONE'
-
-const recommendationRequest = () => ({
-  type: RECOMMENDATION_REQUEST,
-})
-
-const recommendationDone = () => ({
-  type: RECOMMENDATION_DONE,
-})
-
 // #region Selectors
 export const selectRecommendations = (state, fragmentId) =>
   get(state, `fragments.${fragmentId}.recommendations`, [])
@@ -34,45 +23,21 @@ export const createRecommendation = ({
   fragmentId,
   collectionId,
   recommendation,
-}) => dispatch => {
-  dispatch(recommendationRequest())
-  return create(
+}) =>
+  create(
     `/collections/${collectionId}/fragments/${fragmentId}/recommendations`,
     recommendation,
-  ).then(
-    res => {
-      dispatch(recommendationDone())
-      return res
-    },
-    err => {
-      dispatch(recommendationDone())
-      throw err
-    },
   )
-}
 
-export const updateRecommendation = (
-  collId,
-  fragId,
+export const updateRecommendation = ({
+  fragmentId,
+  collectionId,
   recommendation,
-) => dispatch => {
-  dispatch(recommendationRequest())
-  return update(
-    `/collections/${collId}/fragments/${fragId}/recommendations/${
+}) =>
+  update(
+    `/collections/${collectionId}/fragments/${fragmentId}/recommendations/${
       recommendation.id
     }`,
     recommendation,
-  ).then(
-    res => {
-      dispatch(recommendationDone())
-      return res
-    },
-    err => {
-      dispatch(recommendationDone())
-      throw err
-    },
   )
-}
 // #endregion
-
-export default (state = false, action) => action.type === RECOMMENDATION_REQUEST
diff --git a/packages/component-modal/src/components/withModal.js b/packages/component-modal/src/components/withModal.js
index 2be2e0d14..3d7f9913d 100644
--- a/packages/component-modal/src/components/withModal.js
+++ b/packages/component-modal/src/components/withModal.js
@@ -49,6 +49,7 @@ const withModal = mapperFn => BaseComponent =>
         )}
         <BaseComponent
           hideModal={hideModal}
+          isFetching={isFetching}
           setModalError={setModalError}
           showModal={showModal}
           {...rest}
diff --git a/packages/components-faraday/src/redux/files.js b/packages/components-faraday/src/redux/files.js
index 623177c8a..02a36e851 100644
--- a/packages/components-faraday/src/redux/files.js
+++ b/packages/components-faraday/src/redux/files.js
@@ -19,20 +19,6 @@ const REMOVE_FAILURE = 'files/REMOVE_FAILURE'
 const REMOVE_SUCCESS = 'files/REMOVE_SUCCESS'
 
 // action creators
-const uploadRequest = type => ({
-  type: UPLOAD_REQUEST,
-  fileType: type,
-})
-
-const uploadFailure = error => ({
-  type: UPLOAD_FAILURE,
-  error,
-})
-
-const uploadSuccess = () => ({
-  type: UPLOAD_SUCCESS,
-})
-
 const createFileData = ({ file, type, fragmentId, newName }) => {
   const data = new FormData()
   data.append('fileType', type)
@@ -49,7 +35,7 @@ const createFileData = ({ file, type, fragmentId, newName }) => {
   }
 }
 
-const setFileName = (file, { files }) => {
+const setFileName = (file, { files = [] }) => {
   let newFilename = file.name
   const fileCount = Object.values(files)
     .reduce((acc, f) => [...acc, ...f], [])
@@ -69,20 +55,6 @@ const setFileName = (file, { files }) => {
   return newFilename
 }
 
-const removeRequest = () => ({
-  type: REMOVE_REQUEST,
-})
-
-const removeFailure = error => ({
-  type: REMOVE_FAILURE,
-  error,
-})
-
-const removeSuccess = file => ({
-  type: REMOVE_SUCCESS,
-  file,
-})
-
 // selectors
 export const getRequestStatus = state => get(state, 'files.isFetching', false)
 export const getFileFetching = state =>
@@ -92,38 +64,18 @@ export const getFileFetching = state =>
 export const getFileError = state => get(state, 'files.error', null)
 
 // thunked actions
-export const uploadFile = (file, type, fragment) => dispatch => {
-  dispatch(uploadRequest(type))
+export const uploadFile = ({ file, type, fragment }) => {
   const newName = setFileName(file, fragment)
   return request(
     '/files',
     createFileData({ file, type, fragmentId: fragment.id, newName }),
-  ).then(
-    r => {
-      dispatch(uploadSuccess())
-      return r
-    },
-    error => {
-      dispatch(uploadFailure(error))
-      throw error
-    },
   )
 }
 
-export const deleteFile = (fileId, type = 'manuscripts') => dispatch => {
-  dispatch(removeRequest(type))
-  return remove(`/files/${fileId}`)
-    .then(r => {
-      dispatch(removeSuccess())
-      return r
-    })
-    .catch(err => {
-      dispatch(removeFailure(err.message))
-      throw err
-    })
-}
+export const deleteFile = (fileId, type = 'manuscripts') =>
+  remove(`/files/${fileId}`)
 
-export const getSignedUrl = fileId => dispatch => apiGet(`/files/${fileId}`)
+export const getSignedUrl = fileId => apiGet(`/files/${fileId}`)
 
 // reducer
 export default (state = initialState, action) => {
diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js
index 49f8ecc81..fb3d21fef 100644
--- a/packages/components-faraday/src/redux/recommendations.js
+++ b/packages/components-faraday/src/redux/recommendations.js
@@ -16,23 +16,23 @@ export const selectReviewRecommendations = (state, fragmentId) =>
 
 // #region Actions
 // error handling and fetching is handled by the autosave reducer
-export const createRecommendation = (
-  collId,
-  fragId,
+export const createRecommendation = ({
+  fragmentId,
+  collectionId,
   recommendation,
-) => dispatch =>
+}) =>
   create(
-    `/collections/${collId}/fragments/${fragId}/recommendations`,
+    `/collections/${collectionId}/fragments/${fragmentId}/recommendations`,
     recommendation,
   )
 
-export const updateRecommendation = (
-  collId,
-  fragId,
+export const updateRecommendation = ({
+  fragmentId,
+  collectionId,
   recommendation,
-) => dispatch =>
+}) =>
   update(
-    `/collections/${collId}/fragments/${fragId}/recommendations/${
+    `/collections/${collectionId}/fragments/${fragmentId}/recommendations/${
       recommendation.id
     }`,
     recommendation,
-- 
GitLab