From 881f424892636f4187c53d724a92f55f5f79e40e Mon Sep 17 00:00:00 2001 From: Jure Triglav <juretriglav@gmail.com> Date: Tue, 29 Sep 2020 00:56:09 +0200 Subject: [PATCH] feat(app): add support for multiple manuscript versions --- app/Root.jsx | 12 + .../src/components/Dashboard.js | 86 +++--- .../src/components/sections/EditorItem.js | 6 +- .../src/components/sections/OwnerItem.js | 69 ++--- .../src/graphql/queries/index.js | 102 +++--- .../component-manuscripts/src/Manuscript.jsx | 19 +- .../component-manuscripts/src/Manuscripts.jsx | 42 ++- .../src/components/DecisionPage.js | 290 ++---------------- .../src/components/DecisionVersion.js | 229 ++++++++++++++ .../components/assignEditors/AssignEditor.js | 40 +-- .../assignEditors/AssignEditorsReviewers.js | 2 +- .../src/components/decision/DecisionForm.js | 9 +- .../src/components/metadata/ReviewMetadata.js | 2 +- .../src/components/queries.js | 6 + .../component-review/src/components/style.js | 7 +- .../src/components/CreateANewVersion.js | 83 +++++ .../src/components/CurrentVersion.js | 100 ++---- .../src/components/DecisionAndReviews.js | 83 +++++ .../src/components/DecisionReviewColumn.js | 65 ---- .../src/components/FormTemplate.js | 274 +++++++++-------- .../component-submit/src/components/Submit.js | 158 +++++++--- .../src/components/SubmitPage.js | 145 +++++---- .../src/components/atoms/Columns.js | 6 +- 23 files changed, 997 insertions(+), 838 deletions(-) create mode 100644 app/components/component-review/src/components/DecisionVersion.js create mode 100644 app/components/component-submit/src/components/CreateANewVersion.js create mode 100644 app/components/component-submit/src/components/DecisionAndReviews.js delete mode 100644 app/components/component-submit/src/components/DecisionReviewColumn.js diff --git a/app/Root.jsx b/app/Root.jsx index 1cbe6be395..583822f5cb 100644 --- a/app/Root.jsx +++ b/app/Root.jsx @@ -93,6 +93,18 @@ const makeApolloClient = (makeConfig, connectToWebSocket) => { }, }, }, + ManuscriptVersion: { + fields: { + _currentRoles: { + read(existing, { cache, args, readField }) { + const currentRoles = currentRolesVar() + const currentId = readField('id') + const r = currentRoles.find(r => r.id === currentId) + return (r && r.roles) || [] + }, + }, + }, + }, }, }), } diff --git a/app/components/component-dashboard/src/components/Dashboard.js b/app/components/component-dashboard/src/components/Dashboard.js index ff23f5449e..26a09eb7f9 100644 --- a/app/components/component-dashboard/src/components/Dashboard.js +++ b/app/components/component-dashboard/src/components/Dashboard.js @@ -25,42 +25,50 @@ const Dashboard = ({ history, ...props }) => { const { loading, data, error } = useQuery(queries.dashboard) const [reviewerRespond] = useMutation(mutations.reviewerResponseMutation) - const [deleteManuscript] = useMutation(mutations.deleteManuscriptMutation, { - update: (proxy, { data: { deleteManuscript } }) => { - const data = proxy.readQuery({ query: queries.dashboard }) - const manuscripts = data.manuscripts.filter( - manuscript => manuscript.id !== deleteManuscript, - ) - proxy.writeQuery({ - query: queries.dashboard, - data: { - manuscripts, - }, - }) - }, - }) + // const [deleteManuscript] = useMutation(mutations.deleteManuscriptMutation, { + // update: (cache, { data: { deleteManuscript } }) => { + // const data = cache.readQuery({ query: queries.dashboard }) + // const manuscripts = data.manuscripts.filter( + // manuscript => manuscript.id !== deleteManuscript, + // ) + // cache.writeQuery({ + // query: queries.dashboard, + // data: { + // manuscripts, + // }, + // }) + // }, + // }) if (loading) return <Spinner /> if (error) return JSON.stringify(error) const dashboard = (data && data.manuscripts) || [] const currentUser = data && data.currentUser - const mySubmissions = dashboard.filter(submission => - hasRole(submission, 'author'), - ) + const latestVersion = manuscript => + manuscript.manuscriptVersions?.[0] || manuscript - const toReview = dashboard.filter(submission => - hasRole(submission, [ - 'reviewer', - 'invited:reviewer', - 'accepted:reviewer', - 'completed:reviewer', - ]), - ) + const mySubmissions = dashboard + .filter(submission => hasRole(submission, 'author')) + .map(latestVersion) - const manuscriptsImEditorOf = dashboard.filter(submission => - hasRole(submission, ['seniorEditor', 'handlingEditor']), - ) + const toReview = dashboard + .map(latestVersion) + .filter(submission => + hasRole(submission, [ + 'reviewer', + 'invited:reviewer', + 'accepted:reviewer', + 'completed:reviewer', + ]), + ) + + // Editors are always linked to the parent/original manuscript, not to versions + const manuscriptsImEditorOf = dashboard + .filter(submission => + hasRole(submission, ['seniorEditor', 'handlingEditor']), + ) + .map(latestVersion) return ( <Container> @@ -77,17 +85,17 @@ const Dashboard = ({ history, ...props }) => { </SectionHeader> {mySubmissions.length > 0 ? ( mySubmissions.map(submission => ( - <SectionRow key={`submission-${submission.id}`}> - <OwnerItem - deleteManuscript={() => - // eslint-disable-next-line no-alert - window.confirm( - 'Are you sure you want to delete this submission?', - ) && deleteManuscript({ variables: { id: submission.id } }) - } - version={submission} - /> - </SectionRow> + // Links are based on the original/parent manuscript version + <OwnerItem + key={submission.id} + // deleteManuscript={() => + // // eslint-disable-next-line no-alert + // window.confirm( + // 'Are you sure you want to delete this submission?', + // ) && deleteManuscript({ variables: { id: submission.id } }) + // } + version={submission} + /> )) ) : ( <Placeholder>You have not submitted any manuscripts yet</Placeholder> diff --git a/app/components/component-dashboard/src/components/sections/EditorItem.js b/app/components/component-dashboard/src/components/sections/EditorItem.js index fb3910e5ce..1c6da2d327 100644 --- a/app/components/component-dashboard/src/components/sections/EditorItem.js +++ b/app/components/component-dashboard/src/components/sections/EditorItem.js @@ -25,10 +25,12 @@ const getUserFromTeam = (version, role) => { const EditorItemLinks = ({ version }) => ( <ActionGroup> - <Action to={`/journal/versions/${version.id}/submit`}>Summary Info</Action> + <Action to={`/journal/versions/${version.parentId || version.id}/submit`}> + Summary Info + </Action> <Action data-testid="control-panel" - to={`/journal/versions/${version.id}/decision`} + to={`/journal/versions/${version.parentId || version.id}/decision`} > {version.decision && version.decision.status === 'submitted' ? `Decision: ${version.decision.recommendation}` diff --git a/app/components/component-dashboard/src/components/sections/OwnerItem.js b/app/components/component-dashboard/src/components/sections/OwnerItem.js index 13c70bbf60..a9f8b498d3 100644 --- a/app/components/component-dashboard/src/components/sections/OwnerItem.js +++ b/app/components/component-dashboard/src/components/sections/OwnerItem.js @@ -1,47 +1,34 @@ import React from 'react' -import { Action, ActionGroup } from '@pubsweet/ui' +import { Link } from 'react-router-dom' import { Item, StatusBadge } from '../../style' import VersionTitle from './VersionTitle' +import { Icon, ClickableSectionRow } from '../../../../shared' +import theme from '../../../../../theme' -const OwnerItem = ({ version, journals, deleteManuscript }) => { - const baseLink = `/journal/versions/${version.id}` - const submitLink = `${baseLink}/submit` - const manuscriptLink = `${baseLink}/manuscript` - - const actionButtons = { - submit: ( - <Action key="submit-action" to={submitLink}> - Summary Info - </Action> - ), - manuscript: ( - <Action key="manuscript-action" to={manuscriptLink}> - Manuscript - </Action> - ), - delete: ( - <Action key="delete-action" onClick={() => deleteManuscript(version)}> - Delete - </Action> - ), - } - - const actions = <ActionGroup>{Object.values(actionButtons)}</ActionGroup> - - return ( - <Item> - <div> - {' '} - <StatusBadge - minimal - published={version.published} - status={version.status} - /> - <VersionTitle version={version} /> - </div> - {actions} - </Item> - ) -} +const OwnerItem = ({ version, journals, deleteManuscript }) => ( + // Links are based on the original/parent manuscript version + <Link + key={`version-${version.id}`} + to={`/journal/versions/${version.parentId || version.id}/submit`} + > + <ClickableSectionRow> + <Item> + <div> + {' '} + <StatusBadge + minimal + published={version.published} + status={version.status} + /> + <VersionTitle version={version} /> + </div> + <Icon color={theme.colorSecondary} noPadding size={2.5}> + chevron_right + </Icon> + {/* {actions} */} + </Item> + </ClickableSectionRow> + </Link> +) export default OwnerItem diff --git a/app/components/component-dashboard/src/graphql/queries/index.js b/app/components/component-dashboard/src/graphql/queries/index.js index 8e7e2dc098..8e26a9b9d9 100644 --- a/app/components/component-dashboard/src/graphql/queries/index.js +++ b/app/components/component-dashboard/src/graphql/queries/index.js @@ -1,5 +1,56 @@ import gql from 'graphql-tag' +const manuscriptFragment = ` +reviews { + id + open + recommendation + created + isDecision + user { + id + username + } +} +teams { + id + role + name + manuscript { + id + } + members { + id + user { + id + username + } + status + } +} +status +meta { + manuscriptId + title + declarations { + openData + openPeerReview + preregistered + previouslySubmitted + researchNexus + streamlinedReview + } + articleSections + articleType + history { + type + date + } +} +published +_currentRoles @client +` + export default { dashboard: gql` { @@ -13,55 +64,10 @@ export default { id manuscriptVersions { id + parentId + ${manuscriptFragment} } - reviews { - id - open - recommendation - created - isDecision - user { - id - username - } - } - teams { - id - role - name - manuscript { - id - } - members { - id - user { - id - username - } - status - } - } - status - meta { - manuscriptId - title - declarations { - openData - openPeerReview - preregistered - previouslySubmitted - researchNexus - streamlinedReview - } - articleSections - articleType - history { - type - date - } - } - published - _currentRoles @client + ${manuscriptFragment} } } `, diff --git a/app/components/component-manuscripts/src/Manuscript.jsx b/app/components/component-manuscripts/src/Manuscript.jsx index 4af2aaa9a5..00e2856a3f 100644 --- a/app/components/component-manuscripts/src/Manuscript.jsx +++ b/app/components/component-manuscripts/src/Manuscript.jsx @@ -26,7 +26,8 @@ const DELETE_MANUSCRIPT = gql` } ` -const User = ({ manuscript }) => { +// manuscriptId is always the parent manuscript's id +const User = ({ manuscriptId, manuscript, submitter }) => { const [deleteManuscript] = useMutation(DELETE_MANUSCRIPT, { update(cache, { data: { deleteManuscript } }) { const id = cache.identify({ @@ -41,32 +42,32 @@ const User = ({ manuscript }) => { <Row> <Cell>{manuscript.meta && manuscript.meta.title}</Cell> <Cell>{convertTimestampToDate(manuscript.created)}</Cell> + <Cell>{convertTimestampToDate(manuscript.updated)}</Cell> <Cell> <StatusBadge status={manuscript.status} /> </Cell> <Cell> - {manuscript.submitter && ( + {submitter && ( <UserCombo> - <UserAvatar user={manuscript.submitter} /> + <UserAvatar user={submitter} /> <UserInfo> - <Primary>{manuscript.submitter.defaultIdentity.name}</Primary> + <Primary>{submitter.defaultIdentity.name}</Primary> <Secondary> - {manuscript.submitter.email || - `(${manuscript.submitter.username})`} + {submitter.email || `(${submitter.username})`} </Secondary> </UserInfo> </UserCombo> )} </Cell> <LastCell> - <Action to={`/journal/versions/${manuscript.id}/decision`}> + <Action to={`/journal/versions/${manuscriptId}/decision`}> Control </Action> - <Action to={`/journal/versions/${manuscript.id}/manuscript`}> + <Action to={`/journal/versions/${manuscriptId}/manuscript`}> View </Action> <Action - onClick={() => deleteManuscript({ variables: { id: manuscript.id } })} + onClick={() => deleteManuscript({ variables: { id: manuscriptId } })} > Delete </Action> diff --git a/app/components/component-manuscripts/src/Manuscripts.jsx b/app/components/component-manuscripts/src/Manuscripts.jsx index b07e1bd23d..4f940b0720 100644 --- a/app/components/component-manuscripts/src/Manuscripts.jsx +++ b/app/components/component-manuscripts/src/Manuscripts.jsx @@ -39,6 +39,25 @@ const GET_MANUSCRIPTS = gql` created updated status + manuscriptVersions { + id + meta { + manuscriptId + title + } + created + updated + status + submitter { + username + online + defaultIdentity { + id + name + } + profilePicture + } + } submitter { username online @@ -114,20 +133,27 @@ const Manuscripts = () => { <Header> <tr> <SortHeader thisSortName="meta:title">Title</SortHeader> - <SortHeader thisSortName="created">Submitted</SortHeader> + <SortHeader thisSortName="created">Created</SortHeader> + <SortHeader thisSortName="updated">Updated</SortHeader> <SortHeader thisSortName="status">Status</SortHeader> <SortHeader thisSortName="submitterId">Author</SortHeader> <th /> </tr> </Header> <tbody> - {manuscripts.map((manuscript, key) => ( - <Manuscript - key={manuscript.id} - manuscript={manuscript} - number={key + 1} - /> - ))} + {manuscripts.map((manuscript, key) => { + const latestVersion = + manuscript.manuscriptVersions?.[0] || manuscript + return ( + <Manuscript + key={latestVersion.id} + manuscript={latestVersion} + manuscriptId={manuscript.id} + number={key + 1} + submitter={manuscript.submitter} + /> + ) + })} </tbody> </Table> <Pagination diff --git a/app/components/component-review/src/components/DecisionPage.js b/app/components/component-review/src/components/DecisionPage.js index d838e680bf..f091700a8a 100644 --- a/app/components/component-review/src/components/DecisionPage.js +++ b/app/components/component-review/src/components/DecisionPage.js @@ -1,160 +1,18 @@ -import React, { useRef, useEffect } from 'react' -import moment from 'moment' +import React from 'react' +import { useQuery } from '@apollo/client' +import DecisionVersion from './DecisionVersion' -import { Tabs } from '@pubsweet/ui' -import { Formik } from 'formik' -import { useMutation, useQuery, gql } from '@apollo/client' -import DecisionForm from './decision/DecisionForm' -import DecisionReviews from './decision/DecisionReviews' -import AssignEditorsReviewers from './assignEditors/AssignEditorsReviewers' -import AssignEditor from './assignEditors/AssignEditor' -import ReviewMetadata from './metadata/ReviewMetadata' -import Decision from './decision/Decision' -import EditorSection from './decision/EditorSection' -import Publish from './Publish' +import { Columns, Manuscript, Chat } from './style' -import { AdminSection, Columns, Manuscript, Chat } from './style' +import { Spinner, VersionSwitcher, ErrorBoundary } from '../../../shared' -import { Spinner } from '../../../shared' +import gatherManuscriptVersions from '../../../../shared/manuscript_versions' import MessageContainer from '../../../component-chat/src' -import { query, updateReviewMutation, makeDecisionMutation } from './queries' - -const addEditor = (manuscript, label) => ({ - content: <EditorSection manuscript={manuscript} />, - key: `editor_${manuscript.id}`, - label, -}) - -const dateLabel = date => moment(date).format('YYYY-MM-DD') - -const decisionSections = ({ - manuscript, - handleSubmit, - isValid, - updateReview, - uploadFile, - isSubmitting, - submitCount, - dirty, -}) => { - const decisionSections = [] - const manuscriptVersions = manuscript.manuscriptVersions || [] - manuscriptVersions.forEach(manuscript => { - decisionSections.push({ - content: ( - <> - <ReviewMetadata manuscript={manuscript} /> - <DecisionReviews manuscript={manuscript} /> - <Decision - review={manuscript.reviews.find(review => review.isDecision)} - /> - </> - ), - key: manuscript.id, - label: dateLabel(manuscript.updated), - }) - }, []) - - const decisionSection = { - content: ( - <> - <AdminSection key="assign-editors"> - <AssignEditorsReviewers - AssignEditor={AssignEditor} - manuscript={manuscript} - /> - </AdminSection> - <AdminSection key="review-metadata"> - <ReviewMetadata manuscript={manuscript} /> - </AdminSection> - <AdminSection key="decision-review"> - <DecisionReviews manuscript={manuscript} /> - </AdminSection> - <AdminSection key="decision-form"> - <DecisionForm - dirty={dirty} - handleSubmit={handleSubmit} - isSubmitting={isSubmitting} - isValid={isValid} - submitCount={submitCount} - updateReview={updateReview} - uploadFile={uploadFile} - /> - </AdminSection> - <AdminSection> - <Publish manuscript={manuscript} /> - </AdminSection> - </> - ), - key: manuscript.id, - label: 'Metadata', - } - - const editorSection = addEditor(manuscript, 'Content') - - if (manuscript.status !== 'revising') { - decisionSections.push({ - content: ( - <Tabs - activeKey={manuscript.id} - sections={[decisionSection, editorSection]} - title="Manuscript" - /> - ), - /* - - <AdminSection key="assign-editors"> - <AssignEditorsReviewers - AssignEditor={AssignEditor} - manuscript={manuscript} - /> - </AdminSection> - <AdminSection key="review-metadata"> - <ReviewMetadata manuscript={manuscript} /> - </AdminSection> - <AdminSection key="decision-review"> - <DecisionReviews manuscript={manuscript} /> - </AdminSection> - <AdminSection key="decision-form"> - <DecisionForm - handleSubmit={handleSubmit} - isValid={isValid} - updateReview={updateReview} - uploadFile={uploadFile} - /> - </AdminSection> - </> - ), */ - - key: manuscript.id, - label: dateLabel(), - }) - } - - return decisionSections -} - -// const editorSections = ({ manuscript }) => { -// const editorSections = [] -// const manuscriptVersions = manuscript.manuscriptVersions || [] -// manuscriptVersions.forEach(manuscript => { -// editorSections.push(addEditor(manuscript, dateLabel(manuscript.updated))) -// }, []) - -// if (manuscript.status !== 'revising') { -// editorSections.push(addEditor(manuscript, dateLabel())) -// } - -// return editorSections -// } +import { query } from './queries' const DecisionPage = ({ match }) => { - // Hooks from the old world - const [makeDecision] = useMutation(makeDecisionMutation) - const [doUpdateReview] = useMutation(updateReviewMutation) - const { loading, error, data } = useQuery(query, { variables: { id: match.params.version, @@ -162,27 +20,11 @@ const DecisionPage = ({ match }) => { // fetchPolicy: 'cache-and-network', }) - const reviewOrInitial = manuscript => - (manuscript && - manuscript.reviews && - manuscript.reviews.find(review => review.isDecision)) || { - decisionComment: {}, - isDecision: true, - recommendation: null, - } - - // Find an existing review or create a placeholder, and hold a ref to it - const existingReview = useRef(reviewOrInitial(data?.manuscript)) - - // Update the value of that ref if the manuscript object changes - useEffect(() => { - existingReview.current = reviewOrInitial(data?.manuscript) - }, [data?.manuscript?.reviews]) - if (loading) return <Spinner /> if (error) return `Error! ${error.message}` const { manuscript } = data + const versions = gatherManuscriptVersions(manuscript) // Protect if channels don't exist for whatever reason let channelId @@ -190,114 +32,22 @@ const DecisionPage = ({ match }) => { channelId = manuscript.channels.find(c => c.type === 'editorial').id } - const updateReview = review => { - const reviewData = { - recommendation: review.recommendation, - manuscriptId: manuscript.id, - isDecision: true, - decisionComment: review.decisionComment && { - id: existingReview.current.decisionComment?.id, - commentType: 'decision', - content: review.decisionComment.content, - }, - } - - return doUpdateReview({ - variables: { - id: existingReview.current.id || undefined, - input: reviewData, - }, - update: (cache, { data: { updateReview } }) => { - cache.modify({ - id: cache.identify(manuscript), - fields: { - reviews(existingReviewRefs = [], { readField }) { - const newReviewRef = cache.writeFragment({ - data: updateReview, - fragment: gql` - fragment NewReview on Review { - id - } - `, - }) - - if ( - existingReviewRefs.some( - ref => readField('id', ref) === updateReview.id, - ) - ) { - return existingReviewRefs - } - - return [...existingReviewRefs, newReviewRef] - }, - }, - }) - }, - }) - } - // const editorSectionsResult = editorSections({ manuscript }) - - const sections = props => - decisionSections({ - manuscript, - handleSubmit: props.handleSubmit, - isValid: props.isValid, - updateReview, - isSubmitting: props.isSubmitting, - submitCount: props.submitCount, - dirty: props.dirty, - }) - return ( <Columns> <Manuscript> - <Formik - displayName="decision" - initialValues={reviewOrInitial(data.manuscript)} - onSubmit={values => - makeDecision({ - variables: { - id: manuscript.id, - decision: values.recommendation, - }, - }) - } - validate={(values, props) => { - const errors = {} - if ( - ['', '<p></p>', undefined].includes( - values.decisionComment?.content, - ) - ) { - errors.decisionComment = 'Decision letter is required' - } - - if (values.recommendation === null) { - errors.recommendation = 'Decision is required' - } - return errors - }} - // validateOnMount - > - {props => ( - // TODO: Find a nicer way to display the contents of a manuscript - <> - {/* <Tabs - activeKey={ - editorSectionsResult[editorSectionsResult.length - 1].key - } - sections={editorSectionsResult} - title="Versions" - /> */} - <Tabs - activeKey={sections(props)[decisionSections.length - 1].key} - sections={sections(props)} - title="Versions" + <ErrorBoundary> + <VersionSwitcher> + {versions.map((version, index) => ( + <DecisionVersion + current={index === 0} + key={version.manuscript.id} + label={version.label} + parent={manuscript} + version={version.manuscript} /> - </> - )} - </Formik> + ))} + </VersionSwitcher> + </ErrorBoundary> </Manuscript> <Chat>{channelId && <MessageContainer channelId={channelId} />}</Chat> diff --git a/app/components/component-review/src/components/DecisionVersion.js b/app/components/component-review/src/components/DecisionVersion.js new file mode 100644 index 0000000000..a002de3919 --- /dev/null +++ b/app/components/component-review/src/components/DecisionVersion.js @@ -0,0 +1,229 @@ +import React, { useRef, useEffect } from 'react' +import { Formik } from 'formik' +import { useMutation, useQuery, gql } from '@apollo/client' +import config from 'config' +import { get } from 'lodash' +import DecisionForm from './decision/DecisionForm' +import DecisionReviews from './decision/DecisionReviews' +import AssignEditorsReviewers from './assignEditors/AssignEditorsReviewers' +import AssignEditor from './assignEditors/AssignEditor' +import ReviewMetadata from './metadata/ReviewMetadata' +import EditorSection from './decision/EditorSection' +import Publish from './Publish' +import { AdminSection } from './style' +import { + Spinner, + Tabs, + SectionContent, + SectionHeader, + SectionRow, + Title, +} from '../../../shared' +import { query, updateReviewMutation, makeDecisionMutation } from './queries' +import DecisionAndReviews from '../../../component-submit/src/components/DecisionAndReviews' + +const addEditor = (manuscript, label) => ({ + content: <EditorSection manuscript={manuscript} />, + key: `editor_${manuscript.id}`, + label, +}) + +const DecisionVersion = ({ label, current, version, parent }) => { + // Hooks from the old world + const [makeDecision] = useMutation(makeDecisionMutation) + const [doUpdateReview] = useMutation(updateReviewMutation) + + const reviewOrInitial = manuscript => + (manuscript && + manuscript.reviews && + manuscript.reviews.find(review => review.isDecision)) || { + decisionComment: {}, + isDecision: true, + recommendation: null, + } + + const { loading, error, data } = useQuery(query, { + variables: { + id: version.id, + }, + // fetchPolicy: 'cache-and-network', + }) + + // Find an existing review or create a placeholder, and hold a ref to it + const existingReview = useRef(reviewOrInitial(data?.manuscript)) + + // Update the value of that ref if the manuscript object changes + useEffect(() => { + existingReview.current = reviewOrInitial(data?.manuscript) + }, [data?.manuscript?.reviews]) + + if (loading) return <Spinner /> + if (error) return `Error! ${error.message}` + + const { manuscript } = data + + const updateReview = manuscriptId => review => { + const reviewData = { + recommendation: review.recommendation, + manuscriptId, + isDecision: true, + decisionComment: review.decisionComment && { + id: existingReview.current.decisionComment?.id, + commentType: 'decision', + content: review.decisionComment.content, + }, + } + + return doUpdateReview({ + variables: { + id: existingReview.current.id || undefined, + input: reviewData, + }, + update: (cache, { data: { updateReview } }) => { + cache.modify({ + id: cache.identify(manuscript), + fields: { + reviews(existingReviewRefs = [], { readField }) { + const newReviewRef = cache.writeFragment({ + data: updateReview, + fragment: gql` + fragment NewReview on Review { + id + } + `, + }) + + if ( + existingReviewRefs.some( + ref => readField('id', ref) === updateReview.id, + ) + ) { + return existingReviewRefs + } + + return [...existingReviewRefs, newReviewRef] + }, + }, + }) + }, + }) + } + + const editorSection = addEditor(manuscript, 'Manuscript text') + + const decisionSection = ({ + handleSubmit, + dirty, + isValid, + submitCount, + isSubmitting, + }) => ({ + content: ( + <> + {!current && ( + <SectionContent> + <SectionHeader> + <Title>Archived version</Title> + </SectionHeader> + <SectionRow> + This is not the current, but an archived read-only version of the + manuscript. + </SectionRow> + </SectionContent> + )} + {current && ( + <AdminSection> + <AssignEditorsReviewers + AssignEditor={AssignEditor} + manuscript={parent} + /> + </AdminSection> + )} + {!current && ( + <SectionContent> + <SectionHeader> + <Title>Assigned editors</Title> + </SectionHeader> + <SectionRow> + {parent.teams?.map(team => { + if (['seniorEditor', 'handlingEditor'].includes(team.role)) { + return ( + <p key={team.id}> + {get(config, `teams.${team.role}.name`)}:{' '} + {team.members?.[0]?.user?.defaultIdentity?.name} + </p> + ) + } + })} + </SectionRow> + </SectionContent> + )} + {!current && <DecisionAndReviews manuscript={version} />} + <AdminSection key="review-metadata"> + <ReviewMetadata manuscript={version} /> + </AdminSection> + {version.status === 'submitted' && ( + <AdminSection key="decision-review"> + <DecisionReviews manuscript={version} /> + </AdminSection> + )} + {version.status === 'submitted' && ( + <AdminSection key="decision-form"> + <DecisionForm + dirty={dirty} + handleSubmit={handleSubmit} + isSubmitting={isSubmitting} + isValid={isValid} + submitCount={submitCount} + updateReview={updateReview(version.id)} + /> + </AdminSection> + )} + {version.status === 'submitted' && ( + <AdminSection> + <Publish manuscript={version} /> + </AdminSection> + )} + </> + ), + key: version.id, + label: 'Workflow & metadata', + }) + + return ( + <Formik + displayName="decision" + initialValues={reviewOrInitial(version)} + onSubmit={values => + makeDecision({ + variables: { + id: version.id, + decision: values.recommendation, + }, + }) + } + validate={(values, props) => { + const errors = {} + if ( + ['', '<p></p>', undefined].includes(values.decisionComment?.content) + ) { + errors.decisionComment = 'Decision letter is required' + } + + if (values.recommendation === null) { + errors.recommendation = 'Decision is required' + } + return errors + }} + > + {props => ( + <Tabs + defaultActiveKey={version.id} + sections={[decisionSection({ ...props }), editorSection]} + /> + )} + </Formik> + ) +} + +export default DecisionVersion diff --git a/app/components/component-review/src/components/assignEditors/AssignEditor.js b/app/components/component-review/src/components/assignEditors/AssignEditor.js index f3fb20ce82..3380e28c38 100644 --- a/app/components/component-review/src/components/assignEditors/AssignEditor.js +++ b/app/components/component-review/src/components/assignEditors/AssignEditor.js @@ -62,7 +62,7 @@ const AssignEditor = ({ teamRole, manuscript }) => { const members = team.members || [] const value = members.length > 0 ? members[0].user.id : undefined - const teamName = get(config, `authsome.teams.${teamRole}.name`) + const teamName = get(config, `teams.${teamRole}.name`) const { data, loading, error } = useQuery(query) @@ -88,6 +88,7 @@ const AssignEditor = ({ teamRole, manuscript }) => { }) } else { const input = { + // Editors are always linked to the parent manuscript manuscriptId: manuscript.id, name: teamRole === 'seniorEditor' ? 'Senior Editor' : 'Handling Editor', role: teamRole, @@ -116,40 +117,3 @@ const AssignEditor = ({ teamRole, manuscript }) => { } export default AssignEditor -// export default compose( -// graphql(query), -// graphql(updateTeam, { -// props: ({ mutate, ownProps }) => { -// const updateTeam = (userId, teamRole) => {} - -// return { -// updateTeam, -// } -// }, -// }), -// graphql(createTeamMutation, { -// props: ({ mutate, ownProps }) => { -// const createTeam = (userId, teamRole) => { -// const input = { -// manuscriptId: ownProps.manuscript.id, -// name: -// teamRole === 'seniorEditor' ? 'Senior Editor' : 'Handling Editor', -// role: teamRole, -// members: [{ user: { id: userId } }], -// } - -// mutate({ -// variables: { -// input, -// }, -// }) -// } - -// return { -// createTeam, -// } -// }, -// }), - -// withLoader(), -// )(AssignEditor) diff --git a/app/components/component-review/src/components/assignEditors/AssignEditorsReviewers.js b/app/components/component-review/src/components/assignEditors/AssignEditorsReviewers.js index 2d00884c20..ffc73a8644 100644 --- a/app/components/component-review/src/components/assignEditors/AssignEditorsReviewers.js +++ b/app/components/component-review/src/components/assignEditors/AssignEditorsReviewers.js @@ -2,7 +2,7 @@ import React from 'react' import { Container, SectionHeader, SectionRowGrid, Title } from '../style' const AssignEditorsReviewers = ({ manuscript, AssignEditor }) => ( - <Container> + <Container flatTop> <SectionHeader> <Title>Assign Editors</Title> </SectionHeader> diff --git a/app/components/component-review/src/components/decision/DecisionForm.js b/app/components/component-review/src/components/decision/DecisionForm.js index 3bbb971bfa..8bc88603cc 100644 --- a/app/components/component-review/src/components/decision/DecisionForm.js +++ b/app/components/component-review/src/components/decision/DecisionForm.js @@ -32,7 +32,7 @@ import { const NoteDecision = ({ updateReview }) => ( <> - <Field key="noteField" name="decisionComment"> + <Field name="decisionComment"> {formikBag => ( <> <NoteInput updateReview={updateReview} {...formikBag} /> @@ -90,7 +90,6 @@ const NoteInput = ({ <NoteEditor data-testid="decisionComment" debounceDelay={300} - key="note-input" onBlur={() => setFieldTouched('decisionComment')} onChange={value => { setFieldValue('decisionComment', { content: value }) @@ -146,12 +145,12 @@ const DecisionForm = ({ } return ( - <Container key="decisionform"> + <Container> <form onSubmit={handleSubmit}> <SectionHeader> <Title>Decision</Title> </SectionHeader> - <SectionRow key="note"> + <SectionRow> <NoteDecision updateReview={updateReview} /> </SectionRow> <SectionRowGrid> @@ -162,7 +161,7 @@ const DecisionForm = ({ validate={required} /> <FormStatus>{status}</FormStatus> - <SectionAction key="submit"> + <SectionAction> <Button disabled={!isValid || isSubmitting || !dirty} primary diff --git a/app/components/component-review/src/components/metadata/ReviewMetadata.js b/app/components/component-review/src/components/metadata/ReviewMetadata.js index 0b418eaf98..2d7d39b157 100644 --- a/app/components/component-review/src/components/metadata/ReviewMetadata.js +++ b/app/components/component-review/src/components/metadata/ReviewMetadata.js @@ -47,7 +47,7 @@ const showFieldData = (manuscript, fieldName) => { // TODO: Make this generic somehow. Perhaps with an additional fieldType? if (Array.isArray(data) && fieldName === 'submission.links') { return data.map(link => ( - <p> + <p key={link.url}> <a href={link.url} rel="noopener noreferrer" target="_blank"> {link.url} </a> diff --git a/app/components/component-review/src/components/queries.js b/app/components/component-review/src/components/queries.js index f8ecf9790b..5c742c8e06 100644 --- a/app/components/component-review/src/components/queries.js +++ b/app/components/component-review/src/components/queries.js @@ -34,6 +34,9 @@ const reviewFields = ` user { id username + defaultIdentity { + name + } } ` @@ -66,6 +69,9 @@ const fragmentFields = ` user { id username + defaultIdentity { + name + } } status } diff --git a/app/components/component-review/src/components/style.js b/app/components/component-review/src/components/style.js index c80e460955..427c9708d0 100644 --- a/app/components/component-review/src/components/style.js +++ b/app/components/component-review/src/components/style.js @@ -67,10 +67,13 @@ export const EditorWrapper = styled.div` ` export const Container = styled.div` - max-width: 90rem; + // max-width: 90rem; box-shadow: ${th('boxShadow')}; background-color: ${th('colorBackground')}; - border-radius: ${th('borderRadius')}; + border-radius: ${({ flatTop }) => + flatTop + ? css`0 ${th('borderRadius')} ${th('borderRadius')}` + : th('borderRadius')}; // padding: ${grid(2)} ${grid(3)}; &:not(:first-of-type) { margin-top: ${grid(4)}; diff --git a/app/components/component-submit/src/components/CreateANewVersion.js b/app/components/component-submit/src/components/CreateANewVersion.js new file mode 100644 index 0000000000..ae3faf0590 --- /dev/null +++ b/app/components/component-submit/src/components/CreateANewVersion.js @@ -0,0 +1,83 @@ +import React from 'react' +// // import styled from 'styled-components' +// // TODO: Sort out the imports, perhaps make DecisionReview a shared component? +// import Review from '../../../component-review/src/components/decision/DecisionReview' +// import { UserAvatar } from '../../../../components/component-avatar/src' +import { Button } from '@pubsweet/ui' +import { gql } from '@apollo/client' + +import { + SectionHeader, + SectionRow, + Title, + SectionContent, + HeadingWithAction, +} from '../../../shared' + +const CreateANewVersion = ({ manuscript, currentVersion, createNewVersion }) => + currentVersion && manuscript.status === 'revise' ? ( + <> + <SectionContent> + <SectionHeader> + <Title>Create a new version</Title> + </SectionHeader> + <SectionRow> + <HeadingWithAction> + <p> + You have been asked to <strong>revise</strong> your manuscript and + the corresponding reviews and decision are available below. You + can create a new version of your manuscript and resubmit it. + </p> + <Button + onClick={() => + createNewVersion({ + variables: { id: manuscript.id }, + update: (cache, { data: { createNewVersion } }) => { + // Always modify the main/original/parent manuscript + const parentId = manuscript.parentId || manuscript.id + cache.modify({ + id: cache.identify({ + id: parentId, + __typename: 'Manuscript', + }), + fields: { + manuscriptVersions( + existingVersionRefs = [], + { readField }, + ) { + const newVersionRef = cache.writeFragment({ + data: createNewVersion, + fragment: gql` + fragment NewManuscriptVersion on Manuscript { + id + } + `, + }) + + if ( + existingVersionRefs.some( + ref => + readField('id', ref) === createNewVersion.id, + ) + ) { + return existingVersionRefs + } + + return [newVersionRef, ...existingVersionRefs] + }, + }, + }) + }, + }) + } + primary + > + Create a new version + </Button> + </HeadingWithAction> + </SectionRow> + </SectionContent> + </> + ) : null + +export default CreateANewVersion diff --git a/app/components/component-submit/src/components/CurrentVersion.js b/app/components/component-submit/src/components/CurrentVersion.js index cc1ff5f429..c2ae538d4b 100644 --- a/app/components/component-submit/src/components/CurrentVersion.js +++ b/app/components/component-submit/src/components/CurrentVersion.js @@ -1,79 +1,41 @@ import React from 'react' -import styled from 'styled-components' -import { Link } from 'react-router-dom' -import { Attachment } from '@pubsweet/ui' -import { th } from '@pubsweet/ui-toolkit' -import Metadata from './MetadataFields' -import Declarations from './Declarations' -import Suggestions from './Suggestions' -import SupplementaryFiles from './SupplementaryFiles' - -import { Heading1, Section, Legend } from '../style' - -const Wrapper = styled.div` - font-family: ${th('fontInterface')}; - line-height: 1.3; - margin: auto; - max-width: 60em; - - overflow: ${({ confirming }) => confirming && 'hidden'}; -` - -const Intro = styled.div` - font-style: italic; - line-height: 1.4; -` - -const filterFileManuscript = files => - files.filter( - file => - file.type === 'manuscript' && - file.mimeType === - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - ) +// import styled from 'styled-components' +// import { Link } from 'react-router-dom' +// import { Attachment } from '@pubsweet/ui' +// import { th } from '@pubsweet/ui-toolkit' +// import Metadata from './MetadataFields' +// import Declarations from './Declarations' +// import Suggestions from './Suggestions' +// import SupplementaryFiles from './SupplementaryFiles' +import Metadata from '../../../component-review/src/components/metadata/ReviewMetadata' +// import { Legend } from '../style' + +// import { +// Section, +// SectionHeader, +// SectionContent, +// SectionRow, +// Title, +// } from '../../../shared' + +// const filterFileManuscript = files => +// files.filter( +// file => +// file.type === 'manuscript' && +// file.mimeType === +// 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', +// ) // Due to migration to new Data Model // Attachement component needs different data structure to work // needs to change the pubsweet ui Attachement to support the new Data Model -const filesToAttachment = file => ({ - name: file.filename, - url: file.url, -}) +// const filesToAttachment = file => ({ +// name: file.filename, +// url: file.url, +// }) const CurrentVersion = ({ journal, forms, manuscript }) => ( - <Wrapper> - <Heading1>Submission information</Heading1> - - <Intro> - <div> - We have ingested your manuscript. To access your manuscript in an - editor, please{' '} - <Link to={`/journal/versions/${manuscript.id}/manuscript`}> - view here - </Link> - . - </div> - <div> - To complete your submission, please answer the following questions. - </div> - <div>The answers will be automatically saved.</div> - </Intro> - - <Metadata manuscript={manuscript} /> - <Declarations forms={forms} manuscript={manuscript} /> - <Suggestions manuscript={manuscript} /> - <SupplementaryFiles manuscript={manuscript} /> - {filterFileManuscript(manuscript.files || []).length > 0 && ( - <Section id="files.manuscript"> - <Legend space>Submitted Manuscript</Legend> - <Attachment - file={filesToAttachment(filterFileManuscript(manuscript.files)[0])} - key={filterFileManuscript(manuscript.files)[0].url} - uploaded - /> - </Section> - )} - </Wrapper> + <Metadata manuscript={manuscript} /> ) export default CurrentVersion diff --git a/app/components/component-submit/src/components/DecisionAndReviews.js b/app/components/component-submit/src/components/DecisionAndReviews.js new file mode 100644 index 0000000000..b9a5bb1e8d --- /dev/null +++ b/app/components/component-submit/src/components/DecisionAndReviews.js @@ -0,0 +1,83 @@ +import React from 'react' +// import styled from 'styled-components' +// TODO: Sort out the imports, perhaps make DecisionReview a shared component? +import Review from '../../../component-review/src/components/decision/DecisionReview' +import { UserAvatar } from '../../../../components/component-avatar/src' + +import { + SectionHeader, + SectionRow, + Title, + SectionContent, +} from '../../../shared' + +const Decision = ({ decision, editor }) => + decision ? ( + <> + <SectionRow> + <p>Decision: {decision.recommendation}.</p> + </SectionRow> + <SectionRow> + <p>Comment:</p> + <p + // eslint-disable-next-line react/no-danger + dangerouslySetInnerHTML={{ + __html: decision?.decisionComment?.content, + }} + /> + </SectionRow> + <SectionRow> + <UserAvatar username={editor?.username} /> + Written by {editor?.defaultIdentity?.name} + </SectionRow> + </> + ) : ( + <SectionRow>Pending.</SectionRow> + ) + +const DecisionAndReviews = ({ manuscript }) => { + const decision = + manuscript.reviews && + !!manuscript.reviews.length && + manuscript.reviews.find(review => review.isDecision) + + const reviews = + manuscript.reviews && + !!manuscript.reviews.length && + manuscript.reviews.filter(review => !review.isDecision) + + return ( + <> + <SectionContent> + <SectionHeader> + <Title>Decision</Title> + </SectionHeader> + <Decision decision={decision} editor={decision?.user} /> + </SectionContent> + <SectionContent> + <SectionHeader> + <Title>Reviews</Title> + </SectionHeader> + + {reviews && reviews.length ? ( + reviews.map((review, index) => ( + <SectionRow key={review.id}> + <Review + open + review={review} + reviewer={{ + name: review.user.username, + ordinal: index + 1, + }} + /> + </SectionRow> + )) + ) : ( + <SectionRow>No completed reviews.</SectionRow> + )} + </SectionContent> + </> + ) +} + +export default DecisionAndReviews diff --git a/app/components/component-submit/src/components/DecisionReviewColumn.js b/app/components/component-submit/src/components/DecisionReviewColumn.js deleted file mode 100644 index 3b7aa91e34..0000000000 --- a/app/components/component-submit/src/components/DecisionReviewColumn.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { Section } from '../style' -import { Review } from './atoms/Columns' -import Accordion from './molecules/Accordion' - -const ReviewAccord = styled.div`` - -const ReviewsItem = styled.div` - margin-left: 1em; -` - -const ReviewAccordion = ({ reviews }) => ( - <ReviewAccord> - {reviews.length > 0 && - reviews.map( - (review, reviewId) => - review.comments.length && - review.comments.map((comment, commentId) => ( - <Accordion - Component={comment.content} - key={`accordion-review-${review.id}`} - ordinal={reviewId + 1} - title="Review" - withDots="true" - /> - )), - )} - </ReviewAccord> -) - -const DecisionReviewColumn = ({ - manuscript, - handleSubmit, - toggleOpen, - open, -}) => ( - <Review> - <Accordion - Component={<ReviewsItem>{manuscript.decision}</ReviewsItem>} - key="decision" - status="revise" - title="Decision" - /> - <ReviewsItem> - {manuscript.reviews && ( - <Section id="accordion.review"> - <Accordion - Component={ - <ReviewAccordion - reviews={manuscript.reviews.filter( - review => !review.isDecision, - )} - /> - } - key="review" - title="Reviews" - /> - </Section> - )} - </ReviewsItem> - </Review> -) - -export default DecisionReviewColumn diff --git a/app/components/component-submit/src/components/FormTemplate.js b/app/components/component-submit/src/components/FormTemplate.js index b5620371a7..60ed9ba313 100644 --- a/app/components/component-submit/src/components/FormTemplate.js +++ b/app/components/component-submit/src/components/FormTemplate.js @@ -168,6 +168,7 @@ const renderArray = (elementsComponentArray, onChange) => ({ 'description', 'order', 'value', + 'shortDescription', ])} aria-label={element.shortDescription} component={elements[element.component]} @@ -222,142 +223,153 @@ export default ({ errors, validateForm, ...props -}) => ( - <Container> - <Heading1>{form.name}</Heading1> - <Intro - dangerouslySetInnerHTML={createMarkup( - (form.description || '').replace( - '###link###', - link(journal, manuscript), - ), - )} - /> - <form onSubmit={handleSubmit}> - {groupElements(form.children || []).map((element, i) => - !isArray(element) ? ( - <Section - cssOverrides={JSON.parse(element.sectioncss || '{}')} - key={`${element.id}`} - > - {/* <p>{JSON.stringify(element)}</p> */} - <Legend dangerouslySetInnerHTML={createMarkup(element.title)} /> - {element.component === 'SupplementaryFiles' && ( - <FilesUpload - containerId={manuscript.id} - containerName="manuscript" - fileType="supplementary" - onChange={onChange} - /> - )} - {element.component === 'AuthorsInput' && ( - <AuthorsInput data-testid={element.name} onChange={onChange} /> - )} - {element.component !== 'AuthorsInput' && - element.component !== 'SupplementaryFiles' && ( - <ValidatedFieldFormik - aria-label={element.placeholder || element.title} - component={elements[element.component]} - data-testid={element.name} // TODO: Improve this - key={`validate-${element.id}`} - name={element.name} - onChange={value => { - // TODO: Perhaps split components remove conditions here - let val - if (value.target) { - val = value.target.value - } else if (value.value) { - val = value.value - } else { - val = value - } - setFieldValue(element.name, val, true) - onChange(val, element.name) - }} - readonly={false} - setTouched={setTouched} - {...rejectProps(element, [ - 'component', - 'title', - 'sectioncss', - 'parse', - 'format', - 'validate', - 'validateValue', - 'description', - 'order', - ])} - validate={composeValidate( - element.validate, - element.validateValue, - )} - values={values} +}) => { + const submitButton = text => ( + <div> + <Button + onClick={async () => { + const hasErrors = Object.keys(await validateForm()).length !== 0 + + // If there are errors, do a fake submit + // to focus on the error + if (hasErrors) { + handleSubmit() + } else { + toggleConfirming() + } + }} + primary + type="button" + > + {text} + </Button> + </div> + ) + + return ( + <Container> + <Heading1>{form.name}</Heading1> + <Intro + dangerouslySetInnerHTML={createMarkup( + (form.description || '').replace( + '###link###', + link(journal, manuscript), + ), + )} + /> + <form onSubmit={handleSubmit}> + {groupElements(form.children || []).map((element, i) => + !isArray(element) ? ( + <Section + cssOverrides={JSON.parse(element.sectioncss || '{}')} + key={`${element.id}`} + > + {/* <p>{JSON.stringify(element)}</p> */} + <Legend dangerouslySetInnerHTML={createMarkup(element.title)} /> + {element.component === 'SupplementaryFiles' && ( + <FilesUpload + containerId={manuscript.id} + containerName="manuscript" + fileType="supplementary" + onChange={onChange} /> )} - <SubNote - dangerouslySetInnerHTML={createMarkup(element.description)} + {element.component === 'AuthorsInput' && ( + <AuthorsInput data-testid={element.name} onChange={onChange} /> + )} + {element.component !== 'AuthorsInput' && + element.component !== 'SupplementaryFiles' && ( + <ValidatedFieldFormik + aria-label={element.placeholder || element.title} + component={elements[element.component]} + data-testid={element.name} // TODO: Improve this + key={`validate-${element.id}`} + name={element.name} + onChange={value => { + // TODO: Perhaps split components remove conditions here + let val + if (value.target) { + val = value.target.value + } else if (value.value) { + val = value.value + } else { + val = value + } + setFieldValue(element.name, val, true) + onChange(val, element.name) + }} + readonly={false} + setTouched={setTouched} + {...rejectProps(element, [ + 'component', + 'title', + 'sectioncss', + 'parse', + 'format', + 'validate', + 'validateValue', + 'description', + 'shortDescription', + 'order', + ])} + validate={composeValidate( + element.validate, + element.validateValue, + )} + values={values} + /> + )} + <SubNote + dangerouslySetInnerHTML={createMarkup(element.description)} + /> + </Section> + ) : ( + <ElementComponentArray + elementsComponentArray={element} + // eslint-disable-next-line + key={i} + onChange={onChange} + setFieldValue={setFieldValue} + setTouched={setTouched} + /> + ), + )} + + {filterFileManuscript(values.files || []).length > 0 ? ( + <Section id="files.manuscript"> + <Legend space>Submitted Manuscript</Legend> + <Attachment + file={filesToAttachment(filterFileManuscript(values.files)[0])} + key={filterFileManuscript(values.files)[0].url} + uploaded /> </Section> - ) : ( - <ElementComponentArray - elementsComponentArray={element} - // eslint-disable-next-line - key={i} - onChange={onChange} - setFieldValue={setFieldValue} - setTouched={setTouched} - /> - ), - )} + ) : null} - {filterFileManuscript(values.files || []).length > 0 ? ( - <Section id="files.manuscript"> - <Legend space>Submitted Manuscript</Legend> - <Attachment - file={filesToAttachment(filterFileManuscript(values.files)[0])} - key={filterFileManuscript(values.files)[0].url} - uploaded - /> - </Section> - ) : null} + {!['submitted', 'revise'].includes(values.status) && + form.haspopup === 'false' && ( + <Button onClick={() => handleSubmit()} primary type="submit"> + Submit your research object + </Button> + )} - {values.status !== 'submitted' && form.haspopup === 'false' && ( - <Button onClick={() => handleSubmit()} primary type="submit"> - Submit your research object - </Button> - )} + {!['submitted', 'revise'].includes(values.status) && + form.haspopup === 'true' && + submitButton('Submit your research object')} - {values.status !== 'submitted' && form.haspopup === 'true' && ( - <div> - <Button - onClick={async () => { - const hasErrors = Object.keys(await validateForm()).length !== 0 + {values.status === 'revise' && submitButton('Submit your revision')} - // If there are errors, do a fake submit - // to focus on the error - if (hasErrors) { - handleSubmit() - } else { - toggleConfirming() - } - }} - primary - type="button" - > - Submit your research object - </Button> - </div> - )} - {confirming && ( - <ModalWrapper> - <Confirm - errors={errors} - form={form} - submit={handleSubmit} - toggleConfirming={toggleConfirming} - /> - </ModalWrapper> - )} - </form> - </Container> -) + {confirming && ( + <ModalWrapper> + <Confirm + errors={errors} + form={form} + submit={handleSubmit} + toggleConfirming={toggleConfirming} + /> + </ModalWrapper> + )} + </form> + </Container> + ) +} diff --git a/app/components/component-submit/src/components/Submit.js b/app/components/component-submit/src/components/Submit.js index d1390b346d..0687a23158 100644 --- a/app/components/component-submit/src/components/Submit.js +++ b/app/components/component-submit/src/components/Submit.js @@ -1,61 +1,127 @@ import React from 'react' -import { Tabs } from '@pubsweet/ui' -import moment from 'moment' +import { Formik } from 'formik' +import { set } from 'lodash' import CurrentVersion from './CurrentVersion' -import DecisionReviewColumn from './DecisionReviewColumn' -import { Columns, SubmissionVersion } from './atoms/Columns' +import DecisionAndReviews from './DecisionAndReviews' +import CreateANewVersion from './CreateANewVersion' import FormTemplate from './FormTemplate' -import { Container, Content } from '../../../shared' - -const SubmittedVersionColumns = props => ( - <Container> - <Columns> - <SubmissionVersion> - <CurrentVersion - forms={props.forms} - journal={props.journal} - manuscript={props.manuscript} - readonly - /> - , - </SubmissionVersion> - <DecisionReviewColumn {...props} /> - </Columns> - </Container> +import { Container, Content, VersionSwitcher, Tabs } from '../../../shared' +// TODO: Improve the import, perhaps a shared component? +import EditorSection from '../../../component-review/src/components/decision/EditorSection' + +const SubmittedVersion = ({ manuscript, currentVersion, createNewVersion }) => ( + <> + <CreateANewVersion + createNewVersion={createNewVersion} + currentVersion={currentVersion} + manuscript={manuscript} + /> + <DecisionAndReviews manuscript={manuscript} /> + <CurrentVersion manuscript={manuscript} /> + </> ) -const Submit = ({ manuscript, forms, ...formProps }) => { +const Submit = ({ + versions = [], + form, + createNewVersion, + toggleConfirming, + confirming, + onChange, + onSubmit, +}) => { const decisionSections = [] - const manuscriptVersions = manuscript.manuscriptVersions || [] - manuscriptVersions.forEach(versionElem => { - const submittedMoment = moment(versionElem.submitted) - const label = submittedMoment.format('YYYY-MM-DD') - decisionSections.push({ - content: ( - <SubmittedVersionColumns forms={forms} manuscript={versionElem} /> - ), - key: versionElem.id, - label, - }) + + const currentVersion = versions[0] + + const addEditor = (manuscript, label) => ({ + content: <EditorSection manuscript={manuscript} />, + key: `editor_${manuscript.id}`, + label, }) - decisionSections.push({ - content: ( - <Content> - <FormTemplate {...formProps} form={forms} manuscript={manuscript} /> - </Content> - ), - key: manuscript.id, - label: 'Current Version', + // Set the initial values based on the form + const initialValues = {} + const fieldNames = form.children.map(field => field.name) + fieldNames.forEach(fieldName => set(initialValues, fieldName, '')) + + versions.forEach((version, index) => { + const { manuscript, label } = version + const versionId = manuscript.id + + const editorSection = addEditor(manuscript, 'Manuscript text') + let decisionSection + + if (['new', 'revising'].includes(manuscript.status)) { + const versionValues = Object.assign({}, manuscript, { + submission: Object.assign( + initialValues.submission, + JSON.parse(manuscript.submission), + ), + }) + decisionSection = { + content: ( + <Content> + <Formik + displayName="submit" + // handleChange={props.handleChange} + initialValues={versionValues} + onSubmit={async ( + values, + { validateForm, setSubmitting, ...other }, + ) => { + // TODO: Change this to a more Formik idiomatic form + const isValid = Object.keys(await validateForm()).length === 0 + return isValid + ? onSubmit(versionId, values) + : setSubmitting(false) + }} + > + {formProps => ( + <FormTemplate + confirming={confirming} + onChange={onChange(versionId)} + toggleConfirming={toggleConfirming} + {...formProps} + form={form} + manuscript={manuscript} + /> + )} + </Formik> + </Content> + ), + key: versionId, + label: 'Edit submission info', + } + } else { + decisionSection = { + content: ( + <SubmittedVersion + createNewVersion={createNewVersion} + currentVersion={version === currentVersion} + manuscript={manuscript} + /> + ), + key: versionId, + label: 'Submitted info', + } + + decisionSections.push({ + content: ( + <Tabs + defaultActiveKey={version.id} + sections={[decisionSection, editorSection]} + /> + ), + key: manuscript.id, + label, + }) + } }) return ( <Container> - <Tabs - activeKey={manuscript.id} - sections={decisionSections} - title="Versions" - /> + <VersionSwitcher versions={decisionSections} /> </Container> ) } diff --git a/app/components/component-submit/src/components/SubmitPage.js b/app/components/component-submit/src/components/SubmitPage.js index da4fcb32c6..8657ef8246 100644 --- a/app/components/component-submit/src/components/SubmitPage.js +++ b/app/components/component-submit/src/components/SubmitPage.js @@ -1,10 +1,49 @@ import React, { useState } from 'react' import { debounce, cloneDeep, set } from 'lodash' -// import { compose, withProps, withState, withHandlers } from 'recompose' import { gql, useQuery, useMutation } from '@apollo/client' -import { Formik } from 'formik' import Submit from './Submit' import { Spinner } from '../../../shared' +import gatherManuscriptVersions from '../../../../shared/manuscript_versions' + +const commentFields = ` + id + commentType + content + files { + id + created + label + filename + fileType + mimeType + size + url + } +` + +const reviewFields = ` + id + created + updated + decisionComment { + ${commentFields} + } + reviewComment { + ${commentFields} + } + confidentialComment { + ${commentFields} + } + isDecision + recommendation + user { + id + defaultIdentity { + name + } + username + } +` const fragmentFields = ` id @@ -20,15 +59,7 @@ const fragmentFields = ` url } reviews { - id - open - recommendation - created - isDecision - user { - id - username - } + ${reviewFields} } teams { id @@ -46,6 +77,7 @@ const fragmentFields = ` meta { manuscriptId title + source abstract declarations { openData @@ -97,6 +129,7 @@ const query = gql` manuscript(id: $id) { ${fragmentFields} manuscriptVersions { + parentId ${fragmentFields} } } @@ -114,13 +147,23 @@ const updateMutation = gql` } ` -// const uploadSuplementaryFilesMutation = gql` -// mutation($file: Upload!) { -// upload(file: $file) { -// url -// } -// } -// ` +const submitMutation = gql` + mutation($id: ID!, $input: String) { + submitManuscript(id: $id, input: $input) { + id + ${fragmentFields} + } + } +` + +const createNewVersionMutation = gql` + mutation($id: ID!) { + createNewVersion(id: $id) { + id + ${fragmentFields} + } + } +` const SubmitPage = ({ match, history, ...props }) => { const [confirming, setConfirming] = useState(false) @@ -131,9 +174,12 @@ const SubmitPage = ({ match, history, ...props }) => { const { data, loading, error } = useQuery(query, { variables: { id: match.params.version, form: 'submit' }, + partialRefetch: true, }) const [update] = useMutation(updateMutation) + const [submit] = useMutation(submitMutation) + const [createNewVersion] = useMutation(createNewVersionMutation) if (loading) return <Spinner /> if (error) return JSON.stringify(error) @@ -141,69 +187,52 @@ const SubmitPage = ({ match, history, ...props }) => { const manuscript = data?.manuscript const form = data?.getFile - // Set the initial values based on the form - let initialValues = {} - const fieldNames = form.children.map(field => field.name) - fieldNames.forEach(fieldName => set(initialValues, fieldName, null)) - initialValues = Object.assign({}, manuscript, { - submission: Object.assign( - initialValues.submission, - JSON.parse(manuscript.submission), - ), - }) - const updateManuscript = input => + const updateManuscript = (versionId, manuscript) => update({ variables: { - id: match.params.version, - input: JSON.stringify(input), + id: versionId, + input: JSON.stringify(manuscript), }, }) const debouncers = {} - const handleChange = (value, path) => { + // This is passed as a custom onChange prop (not belonging/originating from Formik) + // to support continuous auto-saving + const handleChange = versionId => (value, path) => { const input = {} set(input, path, value) - debouncers[path] = debouncers[path] || debounce(updateManuscript, 300) - return debouncers[path](input) + debouncers[path] = debouncers[path] || debounce(updateManuscript, 3000) + return debouncers[path](versionId, input) } - const onSubmit = async manuscript => { + const onSubmit = async (versionId, manuscript) => { const updateManuscript = { status: 'submitted', } - await update({ + await submit({ variables: { - id: match.params.version, + id: versionId, input: JSON.stringify(updateManuscript), }, }) history.push('/journal/dashboard') } + const versions = gatherManuscriptVersions(manuscript) + return ( - <Formik - displayName="submit" - handleChange={handleChange} - initialValues={initialValues} - onSubmit={async (values, { validateForm, setSubmitting, ...other }) => { - // TODO: Change this to a more Formik idiomatic form - const isValid = Object.keys(await validateForm()).length === 0 - return isValid ? onSubmit(values) : setSubmitting(false) - }} - > - {props => ( - <Submit - confirming={confirming} - forms={cloneDeep(form)} - manuscript={manuscript} - onChange={handleChange} - toggleConfirming={toggleConfirming} - {...props} - /> - )} - </Formik> + <Submit + confirming={confirming} + createNewVersion={createNewVersion} + form={cloneDeep(form)} + onChange={handleChange} + onSubmit={onSubmit} + toggleConfirming={toggleConfirming} + versions={versions} + {...props} + /> ) } diff --git a/app/components/component-submit/src/components/atoms/Columns.js b/app/components/component-submit/src/components/atoms/Columns.js index 220e4c451f..758129c626 100644 --- a/app/components/component-submit/src/components/atoms/Columns.js +++ b/app/components/component-submit/src/components/atoms/Columns.js @@ -8,12 +8,8 @@ const Columns = styled.div` justify-content: center; ` -const SubmissionVersion = styled.div` - grid-area: SubmissionVersion; -` - const Review = styled.div` grid-area: Review; ` -export { Columns, SubmissionVersion, Review } +export { Columns, Review } -- GitLab