diff --git a/packages/component-faraday-ui/src/ContextualBox.js b/packages/component-faraday-ui/src/ContextualBox.js index 44d46148e3705d5c97163cffed271d81a32e1615..32ff2966e40f3884d0069955ea65d619a1e8c689 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 f29c14bfd8e46b19968fe2951fa19b953a5ba24a..52a8c6e07b20781e01fc7f1208762ef4175c3504 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 0000000000000000000000000000000000000000..3cec7f5f23bf907061e008e6e719bee94d85b900 --- /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 0000000000000000000000000000000000000000..283c505e2379663a157b285e46908f68f86553aa --- /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 e8fb22e91094bd9a806c7aebc47a7745a753dca7..6368cb7fab1d4d7b3832480a52682dbb25f10bd9 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 e8037b3facc07db0b8ed6a195ba6f94c5a289d4e..be71d2e6cd0f1d8146a684c2fe764d486bd7e969 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 0000000000000000000000000000000000000000..46809097f2cfdef089ab4c02fd40c96b8e495f8d --- /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 0000000000000000000000000000000000000000..1f21e0e10b8672a9baf04b3e57349001ba32ba52 --- /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 0000000000000000000000000000000000000000..c4f68b770b51a2e16abe4589f35c7e45f1e54f4c --- /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 ff0e5b1301733c08d1735aa7b198502739623781..bb4633cceb9f104bb0a00c6486672baf86f96897 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 cdda8c169507720aa7d44b0f309ccefa0d3a4629..53a86f701ff1e0608b81de783b335590255b3606 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 933b4b23f6f0b54f0f1ea95ebad6ab763870c584..b9b322422845e209260a194a3a0dc9155dd80598 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 7c48e7d9adad544331b6ca1cc101fed64e1aea62..e04c281dd145d545c288eecc75187d2ceb1cfec1 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 aebc901864fd5ce67fd587a4b69e4ee908129b3a..daf2f46d9f92f960ea7d08dbb00a95331ab3caad 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: {