Commit 4f0c2ac7 authored by Yannis Barlas's avatar Yannis Barlas

feat(*): add section editor role

parent edcbb32e
!.cz-config.js
!.storybook
!.eslintrc.js
......@@ -10,7 +10,6 @@ module.exports = {
'import/no-extraneous-dependencies': [
'error',
{
// devDependencies: ['client/.storybook/**', 'client/stories/**'],
devDependencies: [
'.storybook/**',
'ui/stories/**',
......@@ -18,5 +17,9 @@ module.exports = {
],
},
],
'react/prop-types': [
2,
{ ignore: ['children', 'className', 'onClick', 'theme'] },
],
},
}
......@@ -10,26 +10,22 @@ module.exports = {
],
coverageDirectory: '<rootDir>/coverage',
projects: [
// {
// displayName: 'app',
// // not needed?
// moduleNameMapper: {
// '\\.s?css$': 'identity-obj-proxy',
// },
// rootDir: '<rootDir>/app',
// // setupTestFrameworkScriptFile: '<rootDir>/test/setup.js',
// setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
// snapshotSerializers: ['enzyme-to-json/serializer'],
// transformIgnorePatterns: ['node_modules/(?!(@?pubsweet|xpub-edit))'],
// },
{
displayName: 'app',
// not needed?
moduleNameMapper: {
'\\.s?css$': 'identity-obj-proxy',
},
rootDir: '<rootDir>/app',
setupTestFrameworkScriptFile: '<rootDir>/test/setup.js',
snapshotSerializers: ['enzyme-to-json/serializer'],
transformIgnorePatterns: ['node_modules/(?!(@?pubsweet|xpub-edit))'],
},
{
displayName: 'server',
rootDir: '<rootDir>/server',
displayName: 'models',
testEnvironment: 'node',
testRegex: 'server/*/.+test.jsx?$',
},
{
displayName: 'auth',
testRegex: '(?<!(app|server))/test/.+test.jsx?$',
testRegex: 'server/models/__tests__/.+test.js$',
},
],
}
......@@ -6,8 +6,7 @@
"// import PropTypes from 'prop-types'",
"// import styled from 'styled-components'",
"",
"// import {} from '@pubsweet/ui'",
"// import { th } from '@pubsweet/ui-toolkit'",
"// import { th } from '../_helpers'",
"",
"const Comp = props => {}",
"",
......@@ -35,5 +34,31 @@
"}",
""
]
},
"page": {
"prefix": "apollo",
"body": [
"import React from 'react'",
"import { useQuery } from '@apollo/react-hooks'",
"",
"import { } from '../graphql'",
"import { } from '../../ui'",
"",
"const Comp = props => {}",
"",
"export default Comp",
""
]
},
"graphql-client": {
"prefix": "graphql-client",
"body": [
"import gql from 'graphql-tag'",
"",
"const SOMETHING = gql``",
"",
"export default SOMETHING",
""
]
}
}
......@@ -17,14 +17,18 @@ import Loading from './Loading'
const Centered = styled.div`
margin: 0 auto;
/* max-width: 770px; */
width: 60%;
max-width: 1024px;
/* width: 60%; */
> div {
margin-bottom: calc(${th('gridUnit')} * 2);
}
`
const StyledPageHeader = styled(PageHeader)`
margin-top: 0;
`
const Invite = styled(Action)`
line-height: unset;
`
......@@ -301,7 +305,7 @@ const AssignReviewers = props => {
return (
<Centered>
<PageHeader>Assign Reviewers</PageHeader>
<StyledPageHeader>Assign Reviewers</StyledPageHeader>
<LinkWrapper>
<Action to={`/article/${articleId}`}>Back to Article</Action>
......
......@@ -77,6 +77,7 @@ const Dashboard = props => {
loading,
reviewerArticles,
scienceOfficerArticles,
sectionEditorArticles,
updateAssignedEditor,
updateAssignedScienceOfficer,
} = props
......@@ -86,6 +87,7 @@ const Dashboard = props => {
const isAdmin = currentUser.admin
const isEditor = currentUser.auth.isGlobalEditor
const isScienceOfficer = currentUser.auth.isGlobalScienceOfficer
const isSectionEditor = currentUser.auth.isGlobalSectionEditor
const options = [
{
......@@ -207,7 +209,7 @@ const Dashboard = props => {
]
return (
<React.Fragment>
<>
<DashboardWrapper>
<Section
actions={headerActions}
......@@ -239,6 +241,20 @@ const Dashboard = props => {
/>
)}
{isSectionEditor && (
<Section
actions={editorActions}
allEditors={allEditors}
allScienceOfficers={allScienceOfficers}
itemComponent={EditorSectionItem}
items={sectionEditorArticles}
label="Editor Section"
updateAssignedEditor={updateAssignedEditor}
updateAssignedScienceOfficer={updateAssignedScienceOfficer}
variant="editor"
/>
)}
{isScienceOfficer && (
<Section
itemComponent={EditorSectionItem}
......@@ -253,7 +269,7 @@ const Dashboard = props => {
isOpen={showModal}
onRequestClose={closeModal}
/>
</React.Fragment>
</>
)
}}
</State>
......
/* eslint-disable react/prop-types */
import React, { Fragment } from 'react'
import React from 'react'
import styled from 'styled-components'
import { first, last } from 'lodash'
// import { first, last } from 'lodash'
import { DateParser } from '@pubsweet/ui'
import { th } from '@pubsweet/ui-toolkit'
import ComposedEditorPanel from './compose/EditorPanel'
import Loading from './Loading'
import { shouldShowAssignReviewersLink } from '../helpers/status'
import TabContext from '../tabContext'
import { grid } from '../../ui/src/_helpers'
// import { shouldShowAssignReviewersLink } from '../helpers/status'
import {
DecisionSection,
DiscussSection,
EditorPanelMetadata,
EditorPanelRibbon,
PageHeader,
PanelInfo,
ReviewerInfo,
ScienceOfficerSection,
Tabs,
} from './ui'
const Wrapper = styled.div`
padding-right: calc(${th('gridUnit')} * 2);
`
const StyledPageHeader = styled(PageHeader)`
margin-bottom: 0;
margin-top: calc(${th('gridUnit')} * 2);
/* padding-right: calc(${th('gridUnit')} * 2); */
padding-top: ${grid(1)};
`
const Decision = ({ approved, version, submitDecision }) => (
......@@ -43,17 +34,17 @@ const Decision = ({ approved, version, submitDecision }) => (
const Tab = props => {
const {
articleId,
authorChatMessages,
currentUser,
doi,
articleId, // done
authorChatMessages, // to delete
currentUser, // to delete
doi, // done
editor,
isAdmin,
isEditor,
isLastSubmittedVersion,
isScienceOfficer,
reinviteReviewer,
// requestReviewerAttention,
latest, // done
reviewerChatThreads,
scienceOfficer,
scienceOfficerChatMessages,
......@@ -67,12 +58,13 @@ const Tab = props => {
version,
} = props
console.log(version)
const {
authors,
decision,
editorSuggestedReviewers,
isApprovedByScienceOfficer,
latest,
previousReviewers,
reviewerCounts,
reviews,
......@@ -95,7 +87,7 @@ const Tab = props => {
}
return (
<Fragment>
<Wrapper>
<EditorPanelRibbon type={deriveRibbonStatus()} />
{latest && (
......@@ -129,7 +121,6 @@ const Tab = props => {
articleId={articleId}
previousReviewers={previousReviewers}
reinviteReviewer={reinviteReviewer}
// requestReviewerAttention={requestReviewerAttention}
reviewerChatThreads={reviewerChatThreads}
reviewerCounts={reviewerCounts}
reviews={reviews}
......@@ -154,108 +145,27 @@ const Tab = props => {
updateMetadata={updateMetadata}
/>
)}
</Fragment>
)
}
const EditorPanel = props => {
const {
article,
currentUser,
editor,
loading,
reinviteReviewer,
// requestReviewerAttention,
reviewerChatThreads,
scienceOfficer,
sendAuthorChatMessage,
sendReviewerChatMessage,
sendSOChatMessage,
setSOApproval,
submitDecision,
updateMetadata,
versions,
} = props
if (loading) return <Loading />
const isAdmin = currentUser.admin
const isEditor = currentUser.auth.isGlobalEditor
const isScienceOfficer = currentUser.auth.isGlobalScienceOfficer
const { id: articleId, chatThreads, doi } = article
const soChat = chatThreads.find(
thread => thread.chatType === 'scienceOfficer',
)
const soChatMessages = soChat.messages
const authorChat = chatThreads.find(thread => thread.chatType === 'author')
const authorChatMessages = authorChat.messages
const lastSubmittedVersion = last(versions.filter(v => v.submitted))
const firstSubmittedVersion = first(versions.filter(v => v.submitted))
const showReviewerAssignmentLink = shouldShowAssignReviewersLink(article)
const sections = versions.map((version, index) => ({
content: (
<Tab
articleId={articleId}
authorChatMessages={authorChatMessages}
currentUser={currentUser}
doi={doi}
editor={editor}
isAdmin={isAdmin}
isEditor={isEditor}
isLastSubmittedVersion={version.id === lastSubmittedVersion.id}
isScienceOfficer={isScienceOfficer}
reinviteReviewer={reinviteReviewer}
// requestReviewerAttention={requestReviewerAttention}
reviewerChatThreads={reviewerChatThreads}
scienceOfficer={scienceOfficer}
scienceOfficerChatMessages={soChatMessages}
sendAuthorChatMessage={sendAuthorChatMessage}
sendReviewerChatMessage={sendReviewerChatMessage}
sendSOChatMessage={sendSOChatMessage}
setSOApproval={setSOApproval}
showReviewerAssignmentLink={showReviewerAssignmentLink}
submitDecision={submitDecision}
updateMetadata={updateMetadata}
version={version}
/>
),
key: version.id,
label: (
<DateParser
dateFormat="MM.DD.YY HH:mm"
timestamp={new Date(Number(version.created))}
>
{timestamp => (
<span>
{firstSubmittedVersion.id === version.id
? `Original: ${timestamp}`
: `Revision ${index}: ${timestamp}`}
</span>
)}
</DateParser>
),
}))
return (
<TabContext.Consumer>
{({ activeTab, locked, updateActiveTab }) => (
<Wrapper>
<StyledPageHeader>Editor Panel</StyledPageHeader>
<Tabs
activeKey={activeTab || last(versions).id}
alwaysUseActiveKeyFromProps={locked}
onTabClick={updateActiveTab}
sections={sections}
/>
</Wrapper>
)}
</TabContext.Consumer>
</Wrapper>
)
}
const Composed = () => <ComposedEditorPanel render={EditorPanel} />
export default Composed
// const EditorPanel = props => {
// const soChat = chatThreads.find(
// thread => thread.chatType === 'scienceOfficer',
// )
// const soChatMessages = soChat.messages
// const authorChat = chatThreads.find(thread => thread.chatType === 'author')
// const authorChatMessages = authorChat.messages
// const lastSubmittedVersion = last(versions.filter(v => v.submitted))
// const firstSubmittedVersion = first(versions.filter(v => v.submitted))
// content: (
// <Tab
// authorChatMessages={authorChatMessages}
// isLastSubmittedVersion={version.id === lastSubmittedVersion.id}
// reviewerChatThreads={reviewerChatThreads}
// scienceOfficerChatMessages={soChatMessages}
// />
export default Tab
......@@ -50,6 +50,7 @@ const StyledBar = styled(AppBar)`
a {
color: ${th('colorTextReverse')};
font-weight: 500;
}
}
`
......@@ -60,14 +61,17 @@ const navLinks = (location, currentUser) => {
const isReviewers = location.pathname.match(/assign-reviewers/g)
const isTeamManager = location.pathname.match(/teams/g)
const isAdmin = currentUser && currentUser.admin
const isEditor = currentUser && currentUser.auth.isGlobalEditor
const path = `/${isArticle ? 'article' : 'assign-reviewers'}/:id`
const match = matchPath(location.pathname, { path })
let id
if (match) ({ id } = match.params)
const isAdmin = currentUser && currentUser.admin
const isEditor =
currentUser &&
(currentUser.auth.isGlobalEditor ||
currentUser.auth.isAssignedEditor.includes(id))
const dashboardLink = (
<Action active={isDashboard} to="/dashboard">
Dashboard
......
......@@ -13,10 +13,15 @@ const CURRENT_USER = gql`
auth {
isAcceptedReviewerForManuscript
isAcceptedReviewerForVersion
isAssignedCurator
isAssignedEditor
isAssignedScienceOfficer
isAuthor
isGlobal
isGlobalCurator
isGlobalEditor
isGlobalScienceOfficer
isGlobalSectionEditor
}
displayName
id
......
This diff is collapsed.
......@@ -15,7 +15,7 @@ import SubmitForm from './form/SubmissionForm'
import Loading from './Loading'
import SubmissionForm from './SubmissionForm'
import { ArticlePreview, PageHeader, Ribbon, Tabs } from './ui'
import { SubmissionSuccessModal } from '../ui'
import SubmissionSuccessModal from '../../ui/src/modals/SubmissionSuccessModal'
import { formValuesToData } from './formElements/helpers'
import TabContext from '../tabContext'
......
/* eslint-disable react/prop-types */
import React from 'react'
import { Query, Mutation } from '@apollo/react-components'
import styled from 'styled-components'
import { Form, Formik } from 'formik'
import { keys, sortBy } from 'lodash'
import gql from 'graphql-tag'
import { Button, H4 } from '@pubsweet/ui'
import { th } from '@pubsweet/ui-toolkit'
import { PageHeader as DefaultPageHeader, Select } from './ui'
import { UPDATE_TEAM } from './compose/pieces/updateTeam'
import { GET_TEAMS } from './compose/pieces/getTeams'
import { GET_USERS } from './compose/pieces/getUsers'
import Loading from './Loading'
const CLEAN_UP_GLOBAL_TEAM_MEMBERSHIP = gql`
mutation CleanUpGlobalTeamMembership {
cleanUpGlobalTeamMembership
}
`
const PageHeader = styled(DefaultPageHeader)`
margin-block-end: ${th('gridUnit')};
`
const teamNameMapper = {
editors: 'Editors',
scienceOfficers: 'Science Officers',
}
const TeamHeadingWrapper = styled(H4)`
font-size: ${th('fontSizeHeading4')};
line-height: ${th('lineHeightHeading3')};
margin: 0 auto ${th('gridUnit')};
`
const TeamSectionWrapper = styled.div`
padding-bottom: calc(${th('gridUnit')} * 2);
`
const Ribbon = styled.div`
background: ${th('colorSuccess')};
border-radius: 3px;
color: ${th('colorTextReverse')};
font-size: ${th('fontSizeBaseSmall')};
line-height: ${th('lineHeightBaseSmall')};
padding: calc(${th('gridUnit')} / 2);
text-align: center;
visibility: ${props => (props.hide ? 'hidden' : 'visible')};
`
const ButtonWrapper = styled.div`
padding: calc(${th('gridUnit')} * 2) 0;
`
const PageWrapper = styled.div`
margin: 0 auto;
max-width: 800px;
`
const TeamHeading = props => {
const { name } = props
return <TeamHeadingWrapper>{name}</TeamHeadingWrapper>
}
const TeamSection = props => {
const { setFieldValue, type, users, value } = props
const name = teamNameMapper[type]
const options = users
? users.map(user => ({
label: user.displayName,
value: user.id,
}))
: []
const selectValue = value.map(user => {
if (!user.label && !user.value)
return {
label: user.user.displayName, // FIX ME -- streamline how team members are structured on the server
value: user.user.id,
}
return user
})
const handleChange = newValue => {
setFieldValue(type, newValue)
}
return (
<TeamSectionWrapper>
<TeamHeading name={name} />
<Select
closeMenuOnSelect={false}
isMulti
name={type}
onChange={handleChange}
options={options}
value={selectValue}
/>
</TeamSectionWrapper>
)
}
const TeamManagerForm = props => {
const { setFieldValue, teams, users, values } = props
return (
<Form>
{teams.map(team => (
<TeamSection
key={team.id}
setFieldValue={setFieldValue}
type={team.role}
users={users}
value={values[team.role]}
/>
))}
<ButtonWrapper>
<Button disabled={!props.dirty} primary type="submit">
Save
</Button>
</ButtonWrapper>
</Form>
)
}
class TeamManager extends React.Component {
constructor(props) {
super(props)
this.state = {
hideRibbon: true,
}
}
handleSubmit = (formValues, formikBag) => {
const { cleanUp, updateTeam, teams } = this.props
const data = keys(formValues).map(role => ({
id: teams.find(t => t.role === role).id,
members: formValues[role].map(item => {
// FIX ME -- standardize this mess outside of the ui
if (item.user && item.user.id) return { user: { id: item.user.id } }
if (item.id) return { user: { id: item.id } }
return { user: { id: item.value } }
}),
}))
const promises = data.map(team =>
updateTeam({
variables: {
id: team.id,
input: { members: team.members },
},
}),
)
Promise.all(promises).then(res => {
this.showRibbon()
formikBag.resetForm(formValues)
cleanUp()
})
}
// TODO -- handle better cases like many quick saves
showRibbon = () => {
this.setState({
hideRibbon: false,
})
setTimeout(
() =>
this.setState({
hideRibbon: true,
}),
4000,
)
}
render() {
const { users, loading, teams } = this.props
const { hideRibbon } = this.state
if (loading) return <Loading />
let globalTeams = teams.filter(team => team.global)
const infoMessage = 'Your teams have been successfully updated'
const initialValues = {}
globalTeams.forEach(team => {
initialValues[team.role] = team.members
})
globalTeams = sortBy(globalTeams,