From 963252c52d0781f854c56497fce3928b6a78ea86 Mon Sep 17 00:00:00 2001 From: Jure Triglav <juretriglav@gmail.com> Date: Tue, 29 Sep 2020 01:01:07 +0200 Subject: [PATCH] feat: improve shared components --- app/components/shared/Badge.js | 3 +- app/components/shared/ErrorBoundary.js | 32 ++++++++++ app/components/shared/General.js | 15 +++++ app/components/shared/Icon.jsx | 4 +- app/components/shared/Select.js | 18 ++++-- app/components/shared/Tabs.js | 64 +++++++++++++++++++ app/components/shared/VersionSwitcher.jsx | 58 +++++++++++++++++ app/components/shared/index.js | 3 + .../wax-collab/src/EditoriaLayout.js | 5 +- app/shared/manuscript_versions.js | 26 ++++++++ 10 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 app/components/shared/ErrorBoundary.js create mode 100644 app/components/shared/Tabs.js create mode 100644 app/components/shared/VersionSwitcher.jsx create mode 100644 app/shared/manuscript_versions.js diff --git a/app/components/shared/Badge.js b/app/components/shared/Badge.js index a6bd831477..dfbaca6b82 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 0000000000..5a1c54f6f9 --- /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 f06d8434c3..8bb8f942bd 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 311230387e..171773ae63 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 3f7f7a0b81..0850caa8a9 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 0000000000..b278d30c19 --- /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 0000000000..453b3b618a --- /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 beaf45b435..94f0b4d530 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 1f703cc108..d813a25aeb 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 0000000000..f29e174175 --- /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 -- GitLab