From 0a6c04f7a9b7a8eb8af7344df37d41eed90ac548 Mon Sep 17 00:00:00 2001
From: Alexandru Munteanu <alexandru.munt@gmail.com>
Date: Tue, 28 Aug 2018 15:30:00 +0300
Subject: [PATCH] feat(assign-he): create remote opener

---
 .../component-faraday-ui/src/ContextualBox.js |  41 +++---
 .../component-faraday-ui/src/ContextualBox.md |  23 +++-
 .../component-faraday-ui/src/RemoteOpener.js  |  13 ++
 .../component-faraday-ui/src/RemoteOpener.md  |  12 ++
 packages/component-faraday-ui/src/index.js    |   3 +-
 .../src/pending/Accordion.js                  | 123 ++++++++++--------
 .../src/pending/Accordion.md                  |   7 +
 .../src/pending/ControlledAccordion.js        |  89 +++++++++++++
 .../src/pending/ControlledAccordion.md        |  35 +++++
 .../src/components/ManuscriptLayout.js        |  73 ++++++-----
 .../src/components/ManuscriptPage.js          |   4 +-
 .../component-manuscript/src/redux/editors.js |   2 +-
 packages/styleguide/styleguide.config.js      |   5 +
 packages/xpub-faraday/config/default.js       |   2 +-
 14 files changed, 326 insertions(+), 106 deletions(-)
 create mode 100644 packages/component-faraday-ui/src/RemoteOpener.js
 create mode 100644 packages/component-faraday-ui/src/RemoteOpener.md
 create mode 100644 packages/component-faraday-ui/src/pending/Accordion.md
 create mode 100644 packages/component-faraday-ui/src/pending/ControlledAccordion.js
 create mode 100644 packages/component-faraday-ui/src/pending/ControlledAccordion.md

diff --git a/packages/component-faraday-ui/src/ContextualBox.js b/packages/component-faraday-ui/src/ContextualBox.js
index 44d46148e..32ff2966e 100644
--- a/packages/component-faraday-ui/src/ContextualBox.js
+++ b/packages/component-faraday-ui/src/ContextualBox.js
@@ -1,9 +1,11 @@
 import React from 'react'
-import { Icon, H3 } from '@pubsweet/ui'
+import { has } from 'lodash'
 import styled from 'styled-components'
+import { Icon, H3 } from '@pubsweet/ui'
 import { override, th } from '@pubsweet/ui-toolkit'
 
 import Accordion from './pending/Accordion'
+import ControlledAccordion from './pending/ControlledAccordion'
 
 const CustomHeader = ({
   label,
@@ -31,23 +33,26 @@ const CustomHeader = ({
   </Header>
 )
 
-const ContextualBox = ({
-  label,
-  children,
-  transparent,
-  rightChildren,
-  ...props
-}) => (
-  <Accordion
-    header={CustomHeader}
-    label={label}
-    rightChildren={rightChildren}
-    transparent={transparent}
-    {...props}
-  >
-    {children}
-  </Accordion>
-)
+const ContextualBox = ({ label, children, rightChildren, ...props }) =>
+  has(props, 'expanded') ? (
+    <ControlledAccordion
+      header={CustomHeader}
+      label={label}
+      rightChildren={rightChildren}
+      {...props}
+    >
+      {children}
+    </ControlledAccordion>
+  ) : (
+    <Accordion
+      header={CustomHeader}
+      label={label}
+      rightChildren={rightChildren}
+      {...props}
+    >
+      {children}
+    </Accordion>
+  )
 
 export default ContextualBox
 
diff --git a/packages/component-faraday-ui/src/ContextualBox.md b/packages/component-faraday-ui/src/ContextualBox.md
index f29c14bfd..52a8c6e07 100644
--- a/packages/component-faraday-ui/src/ContextualBox.md
+++ b/packages/component-faraday-ui/src/ContextualBox.md
@@ -77,7 +77,6 @@ Render custom components on the right side of the header.
 React components can be passed as right children too. All the props provided to the contextual box are also passed to the right header children.
 
 ```js
-
 const MyRightComponent = ({headLabel}) => <div>{headLabel}: 1 accepted, 4 denied</div>;
 
 <ContextualBox
@@ -111,3 +110,25 @@ const MyRightComponent = ({headLabel}) => <div>{headLabel}: 1 accepted, 4 denied
   </div>
 </ContextualBox>
 ```
+
+A controlled ContextualBox.
+
+```js
+const MyRightComponent = () => <div>works like a charm!</div>;
+
+<RemoteOpener>
+  {(expanded, toggle) => (
+    <div>
+      <button onClick={toggle}>Toggle</button>
+      <ContextualBox
+        toggle={toggle}
+        expanded={expanded}
+        label="Controlled contextual box"
+        rightChildren={MyRightComponent}
+      >
+        <div>Peek a boo!</div>
+      </ContextualBox>
+    </div>
+  )}
+</RemoteOpener>
+```
diff --git a/packages/component-faraday-ui/src/RemoteOpener.js b/packages/component-faraday-ui/src/RemoteOpener.js
new file mode 100644
index 000000000..3cec7f5f2
--- /dev/null
+++ b/packages/component-faraday-ui/src/RemoteOpener.js
@@ -0,0 +1,13 @@
+import { withStateHandlers } from 'recompose'
+
+const RemoteOpener = ({ expanded, toggle, children }) =>
+  children(expanded, toggle)
+
+export default withStateHandlers(
+  ({ startExpanded }) => ({ expanded: startExpanded }),
+  {
+    toggle: ({ expanded }) => () => ({
+      expanded: !expanded,
+    }),
+  },
+)(RemoteOpener)
diff --git a/packages/component-faraday-ui/src/RemoteOpener.md b/packages/component-faraday-ui/src/RemoteOpener.md
new file mode 100644
index 000000000..283c505e2
--- /dev/null
+++ b/packages/component-faraday-ui/src/RemoteOpener.md
@@ -0,0 +1,12 @@
+Toggle a boolean flag and pass it around in your React components tree.
+
+```js
+<RemoteOpener>
+  {(expanded, toggle) => (
+    <div>
+      <button onClick={toggle}>Toggle</button>
+      <span>{expanded ? 'Collapse me!' : 'Expand me!'}</span>
+    </div>
+  )}
+</RemoteOpener>
+```
diff --git a/packages/component-faraday-ui/src/index.js b/packages/component-faraday-ui/src/index.js
index e8fb22e91..6368cb7fa 100644
--- a/packages/component-faraday-ui/src/index.js
+++ b/packages/component-faraday-ui/src/index.js
@@ -24,12 +24,13 @@ export { default as ManuscriptCard } from './ManuscriptCard'
 export { default as ReviewerBreakdown } from './ReviewerBreakdown'
 export { default as PersonInfo } from './PersonInfo'
 export { default as PersonInvitation } from './PersonInvitation'
+export { default as PreviewFile } from './PreviewFile'
+export { default as RemoteOpener } from './RemoteOpener'
 export { default as SortableList } from './SortableList'
 export { default as Tag } from './Tag'
 export { default as Text } from './Text'
 export { default as WizardAuthors } from './WizardAuthors'
 export { default as WizardFiles } from './WizardFiles'
-export { default as PreviewFile } from './PreviewFile'
 
 export * from './manuscriptDetails'
 export * from './contextualBoxes'
diff --git a/packages/component-faraday-ui/src/pending/Accordion.js b/packages/component-faraday-ui/src/pending/Accordion.js
index e8037b3fa..be71d2e6c 100644
--- a/packages/component-faraday-ui/src/pending/Accordion.js
+++ b/packages/component-faraday-ui/src/pending/Accordion.js
@@ -1,11 +1,80 @@
 /* eslint-disable react/require-default-props */
+/* eslint-disable react/prefer-stateless-function */
 
 import React from 'react'
 import PropTypes from 'prop-types'
 import { Icon } from '@pubsweet/ui'
 import styled from 'styled-components'
 import { th, override } from '@pubsweet/ui-toolkit'
-import { withState, withHandlers, compose } from 'recompose'
+
+import { marginHelper } from '../'
+
+const HeaderComponent = ({ icon, label, toggle, expanded, ...props }) => (
+  <Header expanded={expanded} onClick={toggle} {...props}>
+    <HeaderIcon expanded={expanded}>
+      <Icon primary size={3}>
+        {icon}
+      </Icon>
+    </HeaderIcon>
+    <HeaderLabel>{label}</HeaderLabel>
+  </Header>
+)
+
+class Accordion extends React.Component {
+  state = {
+    expanded: false,
+  }
+
+  componentDidUpdate(prevProps) {
+    const shouldScroll = !prevProps.expanded && this.props.expanded
+
+    if (this.props.scrollIntoView && shouldScroll) {
+      this._accordion.scrollIntoView && this._accordion.scrollIntoView()
+    }
+  }
+  _accordion = null
+
+  toggle = () => {
+    this.setState(p => ({ expanded: !p.expanded }))
+  }
+
+  render() {
+    const {
+      children,
+      icon = 'chevron_up',
+      header: Header = HeaderComponent,
+      ...rest
+    } = this.props
+    const { expanded } = this.state
+    return (
+      <Root expanded={expanded} innerRef={r => (this._accordion = r)} {...rest}>
+        <Header
+          expanded={expanded}
+          icon={icon}
+          toggle={this.toggle}
+          {...rest}
+        />
+        {expanded && children}
+      </Root>
+    )
+  }
+}
+
+Accordion.propTypes = {
+  /** Header icon, from the [Feather](https://feathericons.com/) icon set. */
+  icon: PropTypes.string,
+  /** Initial state of the accordion. */
+  startExpanded: PropTypes.bool,
+  /** Function called when toggling the accordion. The new state is passed as a paremeter. */
+  onToggle: PropTypes.func,
+}
+
+Accordion.defaultProps = {
+  onToggle: null,
+  startExpanded: false,
+}
+
+export default Accordion
 
 // #region styles
 const Root = styled.div`
@@ -14,6 +83,7 @@ const Root = styled.div`
   flex-direction: column;
   transition: all ${th('transitionDuration')};
 
+  ${marginHelper};
   ${override('ui.Accordion')};
 `
 
@@ -47,54 +117,3 @@ const HeaderIcon = styled.div`
   ${override('ui.Accordion.Header.Icon')};
 `
 // #endregion
-
-const HeaderComponent = ({ icon, label, toggle, expanded, ...props }) => (
-  <Header expanded={expanded} onClick={toggle} {...props}>
-    <HeaderIcon expanded={expanded}>
-      <Icon primary size={3}>
-        {icon}
-      </Icon>
-    </HeaderIcon>
-    <HeaderLabel>{label}</HeaderLabel>
-  </Header>
-)
-
-const Accordion = ({
-  toggle,
-  expanded,
-  children,
-  icon = 'chevron_up',
-  header: Header = HeaderComponent,
-  ...props
-}) => (
-  <Root expanded={expanded} {...props}>
-    <Header expanded={expanded} icon={icon} toggle={toggle} {...props} />
-    {expanded && children}
-  </Root>
-)
-
-Accordion.propTypes = {
-  /** Header icon, from the [Feather](https://feathericons.com/) icon set. */
-  icon: PropTypes.string,
-  /** Initial state of the accordion. */
-  startExpanded: PropTypes.bool,
-  /** Function called when toggling the accordion. The new state is passed as a paremeter. */
-  onToggle: PropTypes.func,
-}
-
-Accordion.defaultProps = {
-  onToggle: null,
-  startExpanded: false,
-}
-
-export default compose(
-  withState('expanded', 'setExpanded', ({ startExpanded }) => startExpanded),
-  withHandlers({
-    toggle: ({ expanded, setExpanded, onToggle }) => () => {
-      setExpanded(!expanded)
-      if (typeof onToggle === 'function') {
-        onToggle(!expanded)
-      }
-    },
-  }),
-)(Accordion)
diff --git a/packages/component-faraday-ui/src/pending/Accordion.md b/packages/component-faraday-ui/src/pending/Accordion.md
new file mode 100644
index 000000000..46809097f
--- /dev/null
+++ b/packages/component-faraday-ui/src/pending/Accordion.md
@@ -0,0 +1,7 @@
+An expandable section.
+
+```js
+<Accordion label="Uncontrolled accordion here">
+  <div>peek a boo</div>
+</Accordion>
+```
diff --git a/packages/component-faraday-ui/src/pending/ControlledAccordion.js b/packages/component-faraday-ui/src/pending/ControlledAccordion.js
new file mode 100644
index 000000000..1f21e0e10
--- /dev/null
+++ b/packages/component-faraday-ui/src/pending/ControlledAccordion.js
@@ -0,0 +1,89 @@
+import React from 'react'
+import { Icon } from '@pubsweet/ui'
+import styled from 'styled-components'
+import { th, override } from '@pubsweet/ui-toolkit'
+
+import { marginHelper } from '../'
+
+const HeaderComponent = ({ icon, label, toggle, expanded, ...props }) => (
+  <Header expanded={expanded} onClick={toggle} {...props}>
+    <HeaderIcon expanded={expanded}>
+      <Icon primary size={3}>
+        {icon}
+      </Icon>
+    </HeaderIcon>
+    <HeaderLabel>{label}</HeaderLabel>
+  </Header>
+)
+
+class ControlledAccordion extends React.Component {
+  componentDidUpdate(prevProps) {
+    const shouldScroll = !prevProps.expanded && this.props.expanded
+
+    if (this.props.scrollIntoView && shouldScroll) {
+      this._accordion.scrollIntoView && this._accordion.scrollIntoView()
+    }
+  }
+
+  _accordion = null
+
+  render() {
+    const {
+      expanded,
+      children,
+      icon = 'chevron_up',
+      header: Header = HeaderComponent,
+      ...rest
+    } = this.props
+    return (
+      <Root expanded={expanded} innerRef={r => (this._accordion = r)} {...rest}>
+        <Header expanded={expanded} icon={icon} {...rest} />
+        {expanded && children}
+      </Root>
+    )
+  }
+}
+
+export default ControlledAccordion
+
+// #region styles
+const Root = styled.div`
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  transition: all ${th('transitionDuration')};
+
+  ${marginHelper};
+  ${override('ui.Accordion')};
+`
+
+const Header = styled.div.attrs({
+  'data-test-id': props => props['data-test-id'] || 'accordion-header',
+})`
+  align-items: center;
+  cursor: pointer;
+  display: flex;
+  justify-content: flex-start;
+
+  ${override('ui.Accordion.Header')};
+`
+
+const HeaderLabel = styled.span`
+  color: ${th('colorPrimary')};
+  font-family: ${th('fontHeading')};
+  font-size: ${th('fontSizeBase')};
+
+  ${override('ui.Accordion.Header.Label')};
+`
+
+const HeaderIcon = styled.div`
+  align-items: center;
+  display: flex;
+  justify-content: center;
+
+  transform: ${({ expanded }) => `rotateZ(${expanded ? 0 : 180}deg)`};
+  transition: transform ${th('transitionDuration')};
+
+  ${override('ui.Accordion.Header.Icon')};
+`
+// #endregion
diff --git a/packages/component-faraday-ui/src/pending/ControlledAccordion.md b/packages/component-faraday-ui/src/pending/ControlledAccordion.md
new file mode 100644
index 000000000..c4f68b770
--- /dev/null
+++ b/packages/component-faraday-ui/src/pending/ControlledAccordion.md
@@ -0,0 +1,35 @@
+A controlled accordion component.
+
+```js
+class Master extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      expanded: false,
+    }
+
+    this.toggle = this.toggle.bind(this)
+  }
+
+  toggle() {
+    this.setState(prev => ({
+      expanded: !prev.expanded,
+    }))
+  }
+
+  render() {
+    const { expanded } = this.state
+    return (
+      <ControlledAccordion
+        label="Controller accordion"
+        expanded={expanded}
+        toggle={this.toggle}
+      >
+        <div>peek a boo</div>
+      </ControlledAccordion>
+    )
+  }
+}
+
+;<Master />
+```
diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js
index ff0e5b130..bb4633cce 100644
--- a/packages/component-manuscript/src/components/ManuscriptLayout.js
+++ b/packages/component-manuscript/src/components/ManuscriptLayout.js
@@ -3,11 +3,12 @@ import { isEmpty } from 'lodash'
 import styled from 'styled-components'
 import {
   Text,
+  AssignHE,
+  RemoteOpener,
+  ContextualBox,
   ManuscriptHeader,
   ManuscriptMetadata,
   ManuscriptDetailsTop,
-  ContextualBox,
-  AssignHE,
 } from 'pubsweet-component-faraday-ui'
 
 const ManuscriptLayout = ({
@@ -28,38 +29,48 @@ const ManuscriptLayout = ({
 }) => (
   <Root>
     {!isEmpty(collection) && !isEmpty(fragment) ? (
-      <Fragment>
-        <ManuscriptDetailsTop
-          collection={collection}
-          currentUser={currentUser}
-          fragment={fragment}
-          getSignedUrl={getSignedUrl}
-          history={history}
-          {...permissions}
-        />
-        <ManuscriptHeader
-          collection={collection}
-          editorInChief={editorInChief}
-          fragment={fragment}
-          handlingEditors={handlingEditors}
-          journal={journal}
-          resendInvitation={assignHE}
-          revokeInvitation={revokeHE}
-        />
-        <ManuscriptMetadata
-          currentUser={currentUser}
-          fragment={fragment}
-          getSignedUrl={getSignedUrl}
-        />
-        {currentUser.canAssignHE && (
-          <ContextualBox label="Assign Handling Editor">
-            <AssignHE
+      <RemoteOpener>
+        {(expanded, toggle) => (
+          <Fragment>
+            <ManuscriptDetailsTop
+              collection={collection}
+              currentUser={currentUser}
+              fragment={fragment}
+              getSignedUrl={getSignedUrl}
+              history={history}
+              {...permissions}
+            />
+            <ManuscriptHeader
+              collection={collection}
+              editorInChief={editorInChief}
+              fragment={fragment}
               handlingEditors={handlingEditors}
-              inviteHandlingEditor={assignHE}
+              inviteHE={toggle}
+              journal={journal}
+              resendInvitation={assignHE}
+              revokeInvitation={revokeHE}
+            />
+            <ManuscriptMetadata
+              currentUser={currentUser}
+              fragment={fragment}
+              getSignedUrl={getSignedUrl}
             />
-          </ContextualBox>
+            {currentUser.canAssignHE && (
+              <ContextualBox
+                expanded={expanded}
+                label="Assign Handling Editor"
+                scrollIntoView
+                toggle={toggle}
+              >
+                <AssignHE
+                  handlingEditors={handlingEditors}
+                  inviteHandlingEditor={assignHE}
+                />
+              </ContextualBox>
+            )}
+          </Fragment>
         )}
-      </Fragment>
+      </RemoteOpener>
     ) : (
       <Text>Loading...</Text>
     )}
diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js
index cdda8c169..53a86f701 100644
--- a/packages/component-manuscript/src/components/ManuscriptPage.js
+++ b/packages/component-manuscript/src/components/ManuscriptPage.js
@@ -18,6 +18,7 @@ import {
   withProps,
   withHandlers,
   setDisplayName,
+  toClass,
 } from 'recompose'
 import { getSignedUrl } from 'pubsweet-components-faraday/src/redux/files'
 import { reviewerDecision } from 'pubsweet-components-faraday/src/redux/reviewers'
@@ -32,8 +33,8 @@ import {
   canMakeRevision,
   canMakeDecision,
   canEditManuscript,
-  canMakeRecommendation,
   currentUserIsReviewer,
+  canMakeRecommendation,
   canOverrideTechnicalChecks,
 } from 'pubsweet-component-faraday-selectors'
 
@@ -48,6 +49,7 @@ import {
 } from '../redux/editors'
 
 export default compose(
+  toClass,
   setDisplayName('ManuscriptPage'),
   withJournal,
   withRouter,
diff --git a/packages/component-manuscript/src/redux/editors.js b/packages/component-manuscript/src/redux/editors.js
index 933b4b23f..b9b322422 100644
--- a/packages/component-manuscript/src/redux/editors.js
+++ b/packages/component-manuscript/src/redux/editors.js
@@ -19,7 +19,7 @@ const setHandlingEditors = editors => ({
 export const selectFetching = state => get(state, 'editors.isFetching', false)
 export const selectHandlingEditors = state => get(state, 'editors.editors', [])
 
-export const canAssignHE = (state, collectionId) => {
+export const canAssignHE = (state, collectionId = '') => {
   const isEIC = currentUserIs(state, 'adminEiC')
   const hasHE = chain(state)
     .get('collections', [])
diff --git a/packages/styleguide/styleguide.config.js b/packages/styleguide/styleguide.config.js
index 7c48e7d9a..e04c281dd 100644
--- a/packages/styleguide/styleguide.config.js
+++ b/packages/styleguide/styleguide.config.js
@@ -22,6 +22,11 @@ module.exports = {
       sectionDepth: 1,
       components: ['../component-faraday-ui/src/contextualBoxes/[A-Z]*.js'],
     },
+    {
+      name: 'Pending Items',
+      sectionDepth: 1,
+      components: ['../component-faraday-ui/src/pending/[A-Z]*.js'],
+    },
     {
       name: 'Grid Items',
       sectionDepth: 1,
diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js
index aebc90186..daf2f46d9 100644
--- a/packages/xpub-faraday/config/default.js
+++ b/packages/xpub-faraday/config/default.js
@@ -46,7 +46,7 @@ module.exports = {
     API_ENDPOINT: '/api',
     baseUrl: process.env.CLIENT_BASE_URL || 'http://localhost:3000',
     'login-redirect': '/',
-    'redux-log': true, // process.env.NODE_ENV !== 'production',
+    'redux-log': false, // process.env.NODE_ENV !== 'production',
     theme: process.env.PUBSWEET_THEME,
   },
   orcid: {
-- 
GitLab