diff --git a/app/components/shared/Badge.js b/app/components/shared/Badge.js index a6bd8314771ff62f5a7ce791bc311170c422cbbf..dfbaca6b82c3bfd655f04c9134dd5ec7ec79c19c 100644 --- a/app/components/shared/Badge.js +++ b/app/components/shared/Badge.js @@ -59,7 +59,8 @@ const label = (status, published) => { new: 'Unsubmitted', rejected: 'Rejected', submitted: 'Submitted', - revise: 'Revising', + revise: 'Revise', + revising: 'Revising', invited: 'Invited', // reviewer status completed: 'Completed', // reviewer status } diff --git a/app/components/shared/ErrorBoundary.js b/app/components/shared/ErrorBoundary.js new file mode 100644 index 0000000000000000000000000000000000000000..5a1c54f6f9a944a14206b1fad6998426c63d6c86 --- /dev/null +++ b/app/components/shared/ErrorBoundary.js @@ -0,0 +1,32 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable handle-callback-err */ +/* eslint-disable react/sort-comp */ +import React from 'react' + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI. + return { hasError: true } + } + + componentDidCatch(error, errorInfo) { + // You can also log the error to an error reporting service + console.error(error, errorInfo) + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return <h1>Something went wrong.</h1> + } + + return this.props.children + } +} + +export { ErrorBoundary } diff --git a/app/components/shared/General.js b/app/components/shared/General.js index f06d8434c3fd44c222c2114afe9981b15317d688..8bb8f942bd69e569f3f994f533409e3bd7170c42 100644 --- a/app/components/shared/General.js +++ b/app/components/shared/General.js @@ -49,6 +49,20 @@ export const SectionRow = styled.div` padding: ${grid(2)} ${grid(3)}; ` +export const ClickableSectionRow = styled(SectionRow)` + color: ${th('colorText')}; + :last-of-type { + border-radius: 0 0 ${th('borderRadius')} ${th('borderRadius')}; + } + &:hover { + cursor: pointer; + background-color: ${th('colorBackgroundHue')}; + + svg { + stroke: ${th('colorPrimary')}; + } + } +` export const SectionRowGrid = styled(SectionRow)` display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -81,5 +95,6 @@ export { Page, Heading } export const HeadingWithAction = styled.div` display: grid; grid-template-columns: 1fr auto; + grid-gap: ${grid(2)}; align-items: center; ` diff --git a/app/components/shared/Icon.jsx b/app/components/shared/Icon.jsx index 311230387ec34741d874b7c81f37dec7963776be..171773ae63a5facde2156b28cf73348047acc62f 100644 --- a/app/components/shared/Icon.jsx +++ b/app/components/shared/Icon.jsx @@ -13,7 +13,7 @@ const IconWrapper = styled.div` position: relative; border-radius: 6px; padding: ${props => (props.noPadding || props.inline ? '0' : '8px 12px')}; - + top: ${props => props.top || 0}; svg { stroke: ${props => props.color || props.theme.colorText}; width: calc(${props => props.size} * ${th('gridUnit')}); @@ -28,6 +28,7 @@ export const Icon = ({ size = 3, noPadding, inline, + top, ...props }) => { const name = _.upperFirst(_.camelCase(children)) @@ -40,6 +41,7 @@ export const Icon = ({ noPadding={noPadding} role="img" size={size} + top={top} > {icons[name]({})} </IconWrapper> diff --git a/app/components/shared/Select.js b/app/components/shared/Select.js index 3f7f7a0b81fccf6e03c2f794ac0257157b94e840..0850caa8a9cbfa05724961696b3d5a250f3f3ffd 100644 --- a/app/components/shared/Select.js +++ b/app/components/shared/Select.js @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ import React, { useContext } from 'react' import ReactSelect from 'react-select' import { ThemeContext } from 'styled-components' @@ -10,10 +11,19 @@ const styles = th => ({ control: (provided, state) => ({ ...provided, - border: state.isFocused - ? `1px solid ${th.colorPrimary}` - : `1px solid ${th.colorBorder}`, - boxShadow: state.isFocused ? `0 0 0 1px ${th.colorPrimary}` : 'none', + border: !state.selectProps.standalone + ? state.isFocused + ? `1px solid ${th.colorPrimary}` + : `1px solid ${th.colorBorder}` + : 'none', + boxShadow: !state.selectProps.standalone + ? state.isFocused + ? `0 0 0 1px ${th.colorPrimary}` + : 'none' + : state.isFocused + ? `0 0 0 1px ${th.colorPrimary}` + : th.boxShadow, + borderRadius: th.borderRadius, '&:hover': { boxShadow: `0 0 0 1px ${th.colorPrimary}`, diff --git a/app/components/shared/Tabs.js b/app/components/shared/Tabs.js new file mode 100644 index 0000000000000000000000000000000000000000..b278d30c19added0da8513c39f3f3a06df81fa22 --- /dev/null +++ b/app/components/shared/Tabs.js @@ -0,0 +1,64 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import { th, override } from '@pubsweet/ui-toolkit' + +const Tab = styled.div` + padding: ${th('gridUnit')} 1em; + font-size: ${th('fontSizeBaseSmall')}; + font-weight: 500; + background-color: ${({ active }) => + active ? th('colorBackground') : th('colorFurniture')}; + border-radius: ${th('borderRadius')} ${th('borderRadius')} 0 0; + border-bottom: 2px solid + ${({ active }) => (active ? th('colorPrimary') : th('colorFurniture'))}; + color: ${({ active }) => (active ? th('colorPrimary') : th('colorText'))}; + cursor: pointer; + ${override('ui.Tab')}; +` + +const TabsContainer = styled.div` + display: flex; +` + +const TabContainer = styled.div.attrs(props => ({ + 'data-test-id': props['data-test-id'] || 'tab-container', +}))`` + +const Content = styled.div`` + +const Tabs = ({ sections, onChange, defaultActiveKey = null }) => { + const [activeKey, setActiveKey] = useState(defaultActiveKey) + + useEffect(() => { + setActiveKey(defaultActiveKey) + }, [defaultActiveKey]) + + const setActiveKeyAndCallOnChange = activeKey => { + setActiveKey(activeKey) + if (typeof onChange === 'function') { + onChange(activeKey) + } + } + + const currentContent = ( + sections.find(section => section.key === activeKey) || {} + ).content + return ( + <> + <TabsContainer> + {sections.map(({ key, label }) => ( + <TabContainer + key={key} + onClick={() => setActiveKeyAndCallOnChange(key)} + > + <Tab active={activeKey === key}>{label || key}</Tab> + </TabContainer> + ))} + </TabsContainer> + + {activeKey && <Content>{currentContent}</Content>} + </> + ) +} + +export { Tabs } diff --git a/app/components/shared/VersionSwitcher.jsx b/app/components/shared/VersionSwitcher.jsx new file mode 100644 index 0000000000000000000000000000000000000000..453b3b618aa184d6ecdc6c825662a6531d3285f1 --- /dev/null +++ b/app/components/shared/VersionSwitcher.jsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import { grid } from '@pubsweet/ui-toolkit' +import { Select } from './Select' + +const Container = styled.div` + margin-top: ${props => grid(props.top)}; +` + +export const VersionSwitcher = ({ versions = [], children, top = 2 }) => { + // One can pass in versions as prop or as children + let normalizedVersions + let mode + + if (versions.length) { + normalizedVersions = versions + mode = 'props' + } else if (children) { + normalizedVersions = children + mode = 'children' + } + + const defaultVersion = normalizedVersions[0] && normalizedVersions[0].key + const [selectedVersionKey, selectVersionKey] = useState(defaultVersion) + + useEffect(() => { + normalizedVersions = versions.length ? versions : children + selectVersionKey(normalizedVersions[0] && normalizedVersions[0].key) + }, []) + + if (!normalizedVersions) { + return null + } + + const selectedVersion = normalizedVersions.find( + v => v.key === selectedVersionKey, + ) + + return ( + <> + <Select + onChange={option => { + selectVersionKey(option.value) + }} + options={normalizedVersions.map(d => ({ + value: d.key, + label: mode === 'props' ? d.label : d.props.label, + }))} + placeholder="Select version..." + standalone + value={selectedVersionKey} + /> + <Container top={top}> + {mode === 'props' ? selectedVersion.content : selectedVersion} + </Container> + </> + ) +} diff --git a/app/components/shared/index.js b/app/components/shared/index.js index beaf45b435875c15e1dd26d777a72b14deb3cd1c..94f0b4d530242ec5a2b350930c35b26bb62428ed 100644 --- a/app/components/shared/index.js +++ b/app/components/shared/index.js @@ -10,3 +10,6 @@ export * from './Badge' export * from './Select' export * from './Dropzone' export * from './FilesUpload' +export * from './VersionSwitcher' +export * from './Tabs' +export * from './ErrorBoundary' diff --git a/app/components/wax-collab/src/EditoriaLayout.js b/app/components/wax-collab/src/EditoriaLayout.js index 1f703cc1080716ba4e41c86a2440b4feb4fd80c7..d813a25aeb4a0b93ce97dbf06fc5fb51edc13045 100644 --- a/app/components/wax-collab/src/EditoriaLayout.js +++ b/app/components/wax-collab/src/EditoriaLayout.js @@ -9,8 +9,9 @@ import EditorElements from './EditorElements' const Layout = styled.div` background-color: ${th('colorBackground')}; - border-radius: ${th('borderRadius')}; - max-width: 90rem; + border-radius: 0 ${th('borderRadius')} ${th('borderRadius')} + ${th('borderRadius')}; + // max-width: 90rem; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); display: grid; diff --git a/app/shared/manuscript_versions.js b/app/shared/manuscript_versions.js new file mode 100644 index 0000000000000000000000000000000000000000..f29e1741757db0d02aad6dc273085a629b1b5153 --- /dev/null +++ b/app/shared/manuscript_versions.js @@ -0,0 +1,26 @@ +import moment from 'moment' + +// TODO: memoize +const manuscriptVersions = manuscript => { + const versions = [] + if (manuscript.manuscriptVersions?.[0]) { + // TODO: The manuscript versions generally come ordered by + // created descending, but we could sort them again if need be + versions.push(...manuscript.manuscriptVersions) + versions.push(manuscript) + } else { + versions.push(manuscript) + } + + return versions.map((manuscript, index) => ({ + label: + index === 0 + ? `Current version (${versions.length})` + : `${moment(manuscript.created).format( + 'YYYY-MM-DD', + )} (${versions.length - index})`, + manuscript, + })) +} + +export default manuscriptVersions