diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index d95643452340c8c5607971adb3da46092e2a4801..db6ee1a0981b71a520e57c8f2b7c9437cc4873d9 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get, last } from 'lodash' export const isHEToManuscript = (state, collectionId) => { const currentUserId = get(state, 'currentUser.user.id') @@ -7,9 +7,10 @@ export const isHEToManuscript = (state, collectionId) => { return get(collection, 'handlingEditor.id') === currentUserId } -export const canMakeRecommendation = (state, project) => { - const isHE = isHEToManuscript(state, project.id) - return isHE && get(project, 'status') === 'reviewCompleted' +export const canMakeRecommendation = (state, collection, fragment = {}) => { + if (fragment.id !== last(collection.fragments)) return false + const isHE = isHEToManuscript(state, collection.id) + return isHE && get(collection, 'status') === 'reviewCompleted' } export const currentUserIs = ({ currentUser: { user } }, role) => { @@ -57,7 +58,8 @@ export const getHERecommendation = (state, collectionId, fragmentId) => { } const cantMakeDecisionStatuses = ['rejected', 'published', 'draft'] -export const canMakeDecision = (state, collection) => { +export const canMakeDecision = (state, collection, fragment = {}) => { + if (fragment.id !== last(collection.fragments)) return false const status = get(collection, 'status') if (!status || cantMakeDecisionStatuses.includes(status)) return false diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index ea90b83967e7dbbd429f89747956d3c7f97968f9..b19fd29804eb846d78f727332c4ce29a2ca405ea 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -20,6 +20,7 @@ import { ManuscriptDetails, ManuscriptVersion, EditorialComments, + ResponseToReviewers, } from './' const ManuscriptLayout = ({ @@ -29,6 +30,8 @@ const ManuscriptLayout = ({ editorInChief, canMakeRevision, editorialRecommendations, + responseToReviewers, + hasResponseToReviewers, project = {}, version = {}, }) => ( @@ -45,7 +48,7 @@ const ManuscriptLayout = ({ <ManuscriptId>{`- ID ${project.customId}`}</ManuscriptId> </LeftDetails> <RightDetails> - <ManuscriptVersion project={project} /> + <ManuscriptVersion project={project} version={version} /> </RightDetails> </Header> <ManuscriptHeader @@ -57,10 +60,6 @@ const ManuscriptLayout = ({ fragment={version} startExpanded={isEmpty(version.revision)} /> - {canMakeRevision && ( - <SubmitRevision project={project} version={version} /> - )} - <ReviewsAndReports project={project} version={version} /> {editorialRecommendations.length > 0 && ( <EditorialComments editorInChief={editorInChief} @@ -68,6 +67,11 @@ const ManuscriptLayout = ({ recommendations={editorialRecommendations} /> )} + <ReviewsAndReports project={project} version={version} /> + {canMakeRevision && ( + <SubmitRevision project={project} version={version} /> + )} + {hasResponseToReviewers && <ResponseToReviewers version={version} />} </Container> <SideBar flex={1}> <SideBarActions project={project} version={version} /> diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index b6ff4faa6b97eed352a2ce4468fc2e97328484c8..b09fe2d5c3a1b7491b7ad99e394f68cb0116f7af 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux' -import { head, get } from 'lodash' import { actions } from 'pubsweet-client' import { ConnectPage } from 'xpub-connect' import { withJournal } from 'xpub-journal' +import { head, get, isEmpty } from 'lodash' import { replace } from 'react-router-redux' import { withRouter } from 'react-router-dom' import { @@ -13,8 +13,9 @@ import { import { get as apiGet } from 'pubsweet-client/src/helpers/api' import { compose, - lifecycle, withState, + lifecycle, + withProps, withHandlers, setDisplayName, } from 'recompose' @@ -129,4 +130,9 @@ export default compose( ) }, }), + withProps(({ version }) => ({ + hasResponseToReviewers: + !isEmpty(get(version, 'files.responseToReviewers')) || + get(version, 'commentsToReviewers'), + })), )(ManuscriptLayout) diff --git a/packages/component-manuscript/src/components/ManuscriptVersion.js b/packages/component-manuscript/src/components/ManuscriptVersion.js index 7da222858c5e88a9860e934b3671867c643732dd..2e85ed689403deae18e0518ae04a03c43063da98 100644 --- a/packages/component-manuscript/src/components/ManuscriptVersion.js +++ b/packages/component-manuscript/src/components/ManuscriptVersion.js @@ -1,28 +1,28 @@ import React from 'react' import { get } from 'lodash' import { Menu } from '@pubsweet/ui' -import { compose } from 'recompose' -import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' -import { selectFragments } from 'xpub-selectors' import { parseVersionOptions } from './utils' -const ManuscriptVersion = ({ project = {}, fragments = [], history, match }) => - !!fragments.length && ( - <Menu - inline - onChange={v => - history.push(`/projects/${project.id}/versions/${v}/details`) - } - options={parseVersionOptions(fragments)} - value={get(match, 'params.version')} - /> +const ManuscriptVersion = ({ + history, + version = {}, + project = { fragments: [] }, +}) => { + const fragments = get(project, 'fragments') + return ( + !!fragments.length && ( + <Menu + inline + onChange={v => + history.push(`/projects/${project.id}/versions/${v}/details`) + } + options={parseVersionOptions(fragments)} + value={get(version, 'id')} + /> + ) ) +} -export default compose( - withRouter, - connect((state, { project }) => ({ - fragments: selectFragments(state, get(project, 'fragments') || []), - })), -)(ManuscriptVersion) +export default withRouter(ManuscriptVersion) diff --git a/packages/component-manuscript/src/components/ResponseToReviewers.js b/packages/component-manuscript/src/components/ResponseToReviewers.js new file mode 100644 index 0000000000000000000000000000000000000000..f2f200fb98ae56aaa73a4ca1f3f12842ac0afe9a --- /dev/null +++ b/packages/component-manuscript/src/components/ResponseToReviewers.js @@ -0,0 +1,67 @@ +import React, { Fragment } from 'react' +import { get, isEmpty } from 'lodash' +import { withProps } from 'recompose' +import { th } from '@pubsweet/ui-toolkit' +import styled, { css } from 'styled-components' +import { FileItem } from 'pubsweet-components-faraday/src/components/Files' + +import { Expandable } from '../molecules' + +const ResponseToReviewers = ({ + responseToReviewers: { comments = '', files }, +}) => ( + <Root> + <Expandable label="Respone to reviewers"> + {comments && ( + <Fragment> + <Label>Comments to reviewers</Label> + <Text>{comments}</Text> + </Fragment> + )} + {!isEmpty(files) && ( + <Fragment> + <Label>Files</Label> + {files.map(file => ( + <FileItem compact id={file.id} key={file.id} {...file} /> + ))} + </Fragment> + )} + </Expandable> + </Root> +) + +export default withProps(({ version }) => ({ + responseToReviewers: { + comments: get(version, 'commentsToReviewers'), + files: get(version, 'files.responseToReviewers'), + }, +}))(ResponseToReviewers) + +// #region styled-components +const defaultText = css` + color: ${th('colorPrimary')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` +const Root = styled.div` + background-color: ${th('colorBackground')}; + margin-top: calc(${th('subGridUnit')} * 2); + transition: height 0.3s; +` + +const Label = styled.div` + ${defaultText}; + text-transform: uppercase; + i { + text-transform: none; + margin-left: ${th('gridUnit')}; + } +` + +const Text = styled.div` + ${defaultText}; + span { + margin-left: calc(${th('subGridUnit')}*3); + } +` +// #endregion diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js index b3e9036a0a353e26d401bd021cd2de8622f9a6d7..d5e8b16b033cf934842da48a3126b4fd686545d0 100644 --- a/packages/component-manuscript/src/components/SideBarActions.js +++ b/packages/component-manuscript/src/components/SideBarActions.js @@ -57,8 +57,8 @@ export default compose( withRouter, connect( (state, { project, version }) => ({ - canMakeDecision: canMakeDecision(state, project), - canMakeRecommendation: canMakeRecommendation(state, project), + canMakeDecision: canMakeDecision(state, project, version), + canMakeRecommendation: canMakeRecommendation(state, project, version), }), (dispatch, { project, version, history }) => ({ createRevision: () => dispatch(createRevision(project, version, history)), diff --git a/packages/component-manuscript/src/components/SubmitRevision.js b/packages/component-manuscript/src/components/SubmitRevision.js index 0d5c6d5889f9ca5a24c07d002812b3bae4a7dacb..057f2a36d5a86563f779bd11c03d6c062f1b5809 100644 --- a/packages/component-manuscript/src/components/SubmitRevision.js +++ b/packages/component-manuscript/src/components/SubmitRevision.js @@ -1,7 +1,7 @@ // #region imports import React from 'react' -import { get } from 'lodash' import PropTypes from 'prop-types' +import { get, isEmpty } from 'lodash' import { connect } from 'react-redux' import { th } from '@pubsweet/ui-toolkit' import { required } from 'xpub-validators' @@ -56,9 +56,11 @@ const SubmitRevision = ({ project, version, formError, + formValues, removeFile, handleSubmit, responseFiles, + submitFailed, }) => ( <Root> <Expandable label="Submit Revision" startExpanded> @@ -104,7 +106,11 @@ const SubmitRevision = ({ <ValidatedField component={TextAreaField} name="commentsToReviewers" - validate={[required]} + validate={ + isEmpty(get(formValues, 'files.responseToReviewers')) + ? [required] + : [] + } /> </FullWidth> </Row> @@ -127,7 +133,8 @@ const SubmitRevision = ({ </FilePicker> </Expandable> <SubmitContainer> - {formError && <Error>There are some errors above.</Error>} + {submitFailed && + formError && <Error>There are some errors above.</Error>} <AutosaveIndicator formName="revision" /> <Button onClick={handleSubmit} primary> SUBMIT REVISION diff --git a/packages/component-manuscript/src/components/index.js b/packages/component-manuscript/src/components/index.js index 8c668d3d54aff7712e20f3e6060f601772499f61..402226d9ce88607be4c13e78350c622edbb82f9a 100644 --- a/packages/component-manuscript/src/components/index.js +++ b/packages/component-manuscript/src/components/index.js @@ -15,3 +15,4 @@ export { default as ReviewsAndReports } from './ReviewsAndReports' export { default as EditorialComments } from './EditorialComments' export { default as ReviewReportsList } from './ReviewReportsList' export { default as ReviewerReportForm } from './ReviewerReportForm' +export { default as ResponseToReviewers } from './ResponseToReviewers' diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 3ad73509bb38957d078a00e46797914f37e5bbc3..bbe6eb5ec3fefc5575447214e3279fbea151b2cf 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -80,10 +80,12 @@ export const parseSearchParams = url => { } export const parseVersionOptions = (fragments = []) => - fragments.map(f => ({ - value: f.id, - label: `Version ${f.version}`, - })) + fragments + .map((f, index) => ({ + value: f, + label: `Version ${index + 1}`, + })) + .reverse() const alreadyAnswered = `You have already answered this invitation.` export const redirectToError = redirectFn => err => { diff --git a/packages/components-faraday/src/components/Admin/AdminUsers.js b/packages/components-faraday/src/components/Admin/AdminUsers.js index ead06f97a1cd69e06bd4c5125f185613e2cca280..ecaf18ab7d11af51e6ef304bd2f252b78a111dc0 100644 --- a/packages/components-faraday/src/components/Admin/AdminUsers.js +++ b/packages/components-faraday/src/components/Admin/AdminUsers.js @@ -4,36 +4,34 @@ import { connect } from 'react-redux' import { actions } from 'pubsweet-client' import { ConnectPage } from 'xpub-connect' import { withJournal } from 'xpub-journal' -import { Icon, Menu, th } from '@pubsweet/ui' +import { Icon, th } from '@pubsweet/ui' import { withRouter, Link } from 'react-router-dom' import styled, { withTheme } from 'styled-components' import { compose, withState, withHandlers } from 'recompose' +import { + withModal, + ConfirmationModal, +} from 'pubsweet-component-modal/src/components' import { Pagination } from './' +import { updateUserStatusModal } from './utils' const TableRow = ({ - toggleUser, - selected, id, email, - roles, - username, - title = '', firstName = '', lastName = '', + isActive = true, affiliation, - isConfirmed, editorInChief, handlingEditor, admin, - roleOptions, + toggleUserStatus, + getStatusLabel, }) => ( <Row> - <td> - <Input checked={selected} onClick={toggleUser} type="checkbox" /> - </td> - <td>{email}</td> <td>{`${firstName} ${lastName}`}</td> + <td>{email}</td> <td>{affiliation}</td> <td> <Role>{`Author${isEqual(editorInChief, true) ? ', Editor in Chief' : ''}${ @@ -41,25 +39,28 @@ const TableRow = ({ }${isEqual(admin, true) ? ', Admin' : ''}`}</Role> </td> <td> - <Tag>{isConfirmed ? 'Confirmed' : 'Invited'}</Tag> + <Tag>{getStatusLabel()}</Tag> </td> - <td> + <TD> <Action to={`/admin/users/edit/${id}`}>Edit</Action> - </td> + <ActionButton onClick={toggleUserStatus}> + {isActive ? 'Deactivate' : 'Reactivate'} + </ActionButton> + </TD> </Row> ) const Users = ({ - users, - toggleUser, - toggleAllUsers, - incrementPage, - decrementPage, page, - itemsPerPage, + users, + theme, history, journal, - theme, + itemsPerPage, + incrementPage, + decrementPage, + getStatusLabel, + toggleUserStatus, }) => { const slicedUsers = users.slice( page * itemsPerPage, @@ -83,32 +84,6 @@ const Users = ({ </AddButton> </Header> <SubHeader> - <div> - <span>Bulk actions: </span> - <Menu - inline - onChange={value => value} - options={[ - { value: 'deactivate', label: 'Deactivate' }, - { value: 'activate', label: 'Activate' }, - ]} - value="activate" - /> - - <Menu - inline - onChange={value => value} - options={[ - { value: 'sort', label: 'SORT' }, - { value: 'unsort', label: 'UNSORT' }, - ]} - value="sort" - /> - - <Icon color={theme.colorPrimary} size={4}> - search - </Icon> - </div> <Pagination decrementPage={decrementPage} hasMore={itemsPerPage * (page + 1) < users.length} @@ -122,19 +97,12 @@ const Users = ({ <Table> <thead> <tr> - <td> - <Input - checked={users.every(u => u.selected)} - onClick={toggleAllUsers} - type="checkbox" - /> - </td> - <td>Email</td> <td>Full name</td> + <td>Email</td> <td>Affiliation</td> <td width="220">Roles</td> <td>Status</td> - <td width="50" /> + <td width="125" /> </tr> </thead> <tbody> @@ -142,8 +110,9 @@ const Users = ({ <TableRow key={u.id} {...u} + getStatusLabel={getStatusLabel(u)} roleOptions={journal.roles} - toggleUser={toggleUser(u)} + toggleUserStatus={toggleUserStatus(u)} /> ))} </tbody> @@ -157,10 +126,10 @@ export default compose( withRouter, withJournal, withTheme, - connect(state => ({ currentUsers: get(state, 'users.users') })), - withState('users', 'setUsers', props => - props.currentUsers.map(u => ({ ...u, selected: false })), - ), + withModal(props => ({ + modalComponent: ConfirmationModal, + })), + connect(state => ({ users: get(state, 'users.users') })), withState('itemsPerPage', 'setItemsPerPage', 20), withState('page', 'setPage', 0), withHandlers({ @@ -172,13 +141,25 @@ export default compose( decrementPage: ({ setPage }) => () => { setPage(p => (p > 0 ? p - 1 : p)) }, - toggleUser: ({ setUsers }) => user => () => { - setUsers(prev => - prev.map(u => (u.id === user.id ? { ...u, selected: !u.selected } : u)), - ) + getStatusLabel: () => ({ isConfirmed, isActive = true }) => () => { + if (isConfirmed) { + return isActive ? 'Active' : 'Inactive' + } + return 'Invited' }, - toggleAllUsers: ({ setUsers }) => () => { - setUsers(users => users.map(u => ({ ...u, selected: !u.selected }))) + toggleUserStatus: ({ + dispatch, + showModal, + hideModal, + setModalError, + }) => user => () => { + updateUserStatusModal({ + dispatch, + showModal, + hideModal, + setModalError, + user, + }) }, }), )(Users) @@ -282,7 +263,8 @@ const Row = styled.tr` text-align: left; &:hover { background-color: ${th('backgroundColorReverse')}; - a { + a, + i { display: block; } } @@ -312,10 +294,18 @@ const Action = styled(Link)` color: ${th('colorPrimary')}; display: none; ` +const TD = styled.td` + display: inline-flex; + width: 100%; + min-height: 20px; +` -const Input = styled.input` - height: 20px; - width: 20px; +const ActionButton = styled.i` + cursor: pointer; + display: none; + font-style: unset; + text-decoration: underline; + margin: 0 calc(${th('subGridUnit')} * 2); ` // #endregion diff --git a/packages/components-faraday/src/components/Admin/utils.js b/packages/components-faraday/src/components/Admin/utils.js index 1029db45e7ae91d1919fa005dbb927398badd507..99934cd10843f0d3332497abf7c6223c9b6ca63d 100644 --- a/packages/components-faraday/src/components/Admin/utils.js +++ b/packages/components-faraday/src/components/Admin/utils.js @@ -1,5 +1,9 @@ +import { actions } from 'pubsweet-client' import { SubmissionError } from 'redux-form' -import { pick, map, omit, get } from 'lodash' +import { update } from 'pubsweet-client/src/helpers/api' +import { pick, map, omit, get, isBoolean, replace } from 'lodash' + +import { handleError } from '../utils' const generatePasswordHash = () => Array.from({ length: 4 }, () => @@ -31,6 +35,12 @@ export const setAdmin = values => { } export const parseUpdateUser = values => { + const parsedValues = { + ...values, + editorInChief: values.editorInChief || false, + handlingEditor: values.handlingEditor || false, + admin: values.admin || false, + } const valuesToSave = [ 'admin', 'firstName', @@ -40,9 +50,11 @@ export const parseUpdateUser = values => { 'roles', 'editorInChief', 'handlingEditor', + 'isActive', + 'username', ] - return pick(values, valuesToSave) + return pick(parsedValues, valuesToSave) } export const handleFormError = error => { @@ -55,3 +67,47 @@ export const handleFormError = error => { }) } } + +const toggleUserStatus = user => { + const { isActive, username } = user + let newState = true + let newUsername = '' + + if (!isBoolean(isActive) || isActive) { + newState = false + newUsername = `invalid***${username}` + } else { + newUsername = replace(username, 'invalid***', '') + } + + return { + ...user, + isActive: newState, + username: newUsername, + } +} + +export const updateUserStatus = user => { + const updatedUser = toggleUserStatus(user) + return update(`/users/${user.id}`, parseUpdateUser(updatedUser)) +} + +export const updateUserStatusModal = ({ + user, + showModal, + hideModal, + setModalError, + dispatch, +}) => + showModal({ + title: `${user.isActive === false ? 'Reactivate' : 'Deactivate'} user?`, + subtitle: user.email, + confirmText: 'Confirm', + onConfirm: () => { + updateUserStatus(user).then(() => { + dispatch(actions.getUsers()) + hideModal() + }, handleError(setModalError)) + }, + onCancel: hideModal, + }) diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index 59638dda62cd0f0059f530d3a11105723c954d82..b3e877877e64a2776973abf0287a77f8f588c9b4 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -193,11 +193,11 @@ export default compose( getContext({ journal: PropTypes.object, currentUser: PropTypes.object }), withTheme, connect((state, { project, version }) => ({ - canMakeDecision: canMakeDecision(state, project), isHE: isHEToManuscript(state, get(project, 'id')), canInviteReviewers: canInviteReviewers(state, project), invitation: selectInvitation(state, get(version, 'id')), - canMakeRecommendation: canMakeRecommendation(state, project), + canMakeDecision: canMakeDecision(state, project, version), + canMakeRecommendation: canMakeRecommendation(state, project, version), })), )(DashboardCard) diff --git a/packages/components-faraday/src/components/utils.js b/packages/components-faraday/src/components/utils.js index b6f31756513a0ac1ed01748b36126eec7ec912d9..b09e279905facba7e1d5b9bf7db2521395be22f0 100644 --- a/packages/components-faraday/src/components/utils.js +++ b/packages/components-faraday/src/components/utils.js @@ -133,6 +133,8 @@ export const parseUpdateUser = values => { 'roles', 'editorInChief', 'handlingEditor', + 'isActive', + 'username', ] return pick(values, valuesToSave)