diff --git a/Dockerfile-production b/Dockerfile-production index 57dc80537a12d810753aa045b6310e32475ec4d4..d8d3986321eecee878db3ae43d28517b73ef17d4 100644 --- a/Dockerfile-production +++ b/Dockerfile-production @@ -22,11 +22,17 @@ ARG node_env ARG server_protocol ARG server_host ARG server_port +ARG client_protocol +ARG client_host +ARG client_port ENV NODE_ENV=$node_env ENV SERVER_PROTOCOL=$server_protocol ENV SERVER_HOST=$server_host ENV SERVER_PORT=$server_port +ENV CLIENT_PROTOCOL=$client_protocol +ENV CLIENT_HOST=$client_host +ENV CLIENT_PORT=$client_port RUN yarn pubsweet build @@ -42,6 +48,7 @@ COPY --chown=node:node ./config ./config COPY --chown=node:node ./public ./public COPY --chown=node:node ./scripts ./scripts COPY --chown=node:node ./server ./server +COPY --chown=node:node ./app/storage ./app/storage COPY --chown=node:node ./startServer.js . COPY --from=build /home/node/app/_build/assets ./_build diff --git a/app/components/AdminPage.js b/app/components/AdminPage.js index 04fffaed8378a7b9d39ce3a2f86223346fa975e8..cd3548780ecf395f8c82169cda14a8496f95a1bc 100644 --- a/app/components/AdminPage.js +++ b/app/components/AdminPage.js @@ -32,8 +32,7 @@ import { Spinner } from './shared' import currentRolesVar from '../shared/currentRolesVar' import RolesUpdater from './RolesUpdater' -const getParams = routerPath => { - const path = '/journal/versions/:version' +const getParams = ({ routerPath, path }) => { return matchPath(routerPath, path).params } @@ -55,14 +54,14 @@ const Root = styled.div` ` // TODO: Redirect if token expires -const PrivateRoute = ({ component: Component, ...rest }) => ( +const PrivateRoute = ({ component: Component, redirectLink, ...rest }) => ( <Route {...rest} render={props => localStorage.getItem('token') ? ( <Component {...props} /> ) : ( - <Redirect to="/login?next=/journal/dashboard" /> + <Redirect to={redirectLink} /> ) } /> @@ -70,6 +69,7 @@ const PrivateRoute = ({ component: Component, ...rest }) => ( PrivateRoute.propTypes = { component: PropTypes.func.isRequired, + redirectLink: PropTypes.string.isRequired, } const updateStuff = data => { @@ -113,16 +113,20 @@ const AdminPage = () => { previousDataRef.current = data + const urlFrag = journal.metadata.toplevel_urlfragment const { pathname } = history.location const showLinks = pathname.match(/^\/(submit|manuscript)/g) let links = [] - const formBuilderLink = `/journal/admin/form-builder` - const homeLink = '/journal/dashboard' - const profileLink = '/journal/profile' + const formBuilderLink = `${urlFrag}/admin/form-builder` + const homeLink = `${urlFrag}/dashboard` + const profileLink = `${urlFrag}/profile` + const loginLink = `/login?next=${homeLink}` + const path = `${urlFrag}/versions/:version` + const redirectLink = `/login?next=${homeLink}` if (showLinks) { - const params = getParams(pathname) - const baseLink = `/journal/versions/${params.version}` + const params = getParams(pathname, path) + const baseLink = `${urlFrag}/versions/${params.version}` const submitLink = `${baseLink}/submit` const manuscriptLink = `${baseLink}/manuscript` @@ -139,11 +143,10 @@ const AdminPage = () => { } if (currentUser && currentUser.admin) { - // links.push({ link: '/journal/admin/teams', name: 'Teams', icon: 'grid' }) links.push({ link: formBuilderLink, name: 'Forms', icon: 'check-square' }) - links.push({ link: '/journal/admin/users', name: 'Users', icon: 'users' }) + links.push({ link: `${urlFrag}/admin/users`, name: 'Users', icon: 'users' }) links.push({ - link: '/journal/admin/manuscripts', + link: `${urlFrag}/admin/manuscripts`, name: 'Manuscripts', icon: 'file-text', }) @@ -157,55 +160,79 @@ const AdminPage = () => { <Root converting={conversion.converting}> <Menu brand={journal.metadata.name} - brandLink="/journal/dashboard" - loginLink="/login?next=/journal/dashboard" + brandLink={homeLink} + className="" + loginLink={loginLink} navLinkComponents={links} notice={notice} + profileLink={profileLink} user={currentUser} /> <Switch> - <PrivateRoute component={Dashboard} exact path="/journal/dashboard" /> + <PrivateRoute + component={Dashboard} + exact + path={homeLink} + redirectLink={redirectLink} + /> <PrivateRoute component={NewSubmissionPage} exact - path="/journal/newSubmission" + path={`${urlFrag}/newSubmission`} + redirectLink={redirectLink} /> <PrivateRoute component={SubmitPage} exact - path="/journal/versions/:version/submit" + path={`${urlFrag}/versions/:version/submit`} + redirectLink={redirectLink} /> <PrivateRoute component={FormBuilderPage} exact - path="/journal/admin/form-builder" + path={`${urlFrag}/admin/form-builder`} + redirectLink={redirectLink} /> <PrivateRoute component={ManuscriptPage} exact - path="/journal/versions/:version/manuscript" + path={`${urlFrag}/versions/:version/manuscript`} + redirectLink={redirectLink} /> <PrivateRoute component={ReviewersPage} exact - path="/journal/versions/:version/reviewers" + path={`${urlFrag}/versions/:version/reviewers`} + redirectLink={redirectLink} /> <PrivateRoute component={ReviewPage} exact - path="/journal/versions/:version/review" + path={`${urlFrag}/versions/:version/review`} + redirectLink={redirectLink} /> <PrivateRoute component={DecisionPage} exact - path="/journal/versions/:version/decision" + path={`${urlFrag}/versions/:version/decision`} + redirectLink={redirectLink} + /> + <PrivateRoute + component={Profile} + exact + path={`${urlFrag}/profile`} + redirectLink={redirectLink} + /> + <PrivateRoute + component={UsersManager} + path={`${urlFrag}/admin/users`} + redirectLink={redirectLink} /> - <PrivateRoute component={Profile} exact path="/journal/profile" /> - <PrivateRoute component={UsersManager} path="/journal/admin/users" /> <PrivateRoute component={Manuscripts} - path="/journal/admin/manuscripts" + path={`${urlFrag}/admin/manuscripts`} + redirectLink={redirectLink} /> </Switch> <RolesUpdater /> diff --git a/app/components/Menu.js b/app/components/Menu.js index 8c39a1ede00ec449d3d2d46ddd631ad32a96fcc6..163b3be5b6867b15dc699659b42592374c408aea 100644 --- a/app/components/Menu.js +++ b/app/components/Menu.js @@ -1,31 +1,24 @@ import React from 'react' import styled, { css } from 'styled-components' -// import PropTypes from 'prop-types' +import PropTypes from 'prop-types' import { th, grid, lighten } from '@pubsweet/ui-toolkit' import { Link, useLocation } from 'react-router-dom' import { Icon } from '@pubsweet/ui' -import { UserAvatar } from '../components/component-avatar/src' +import { UserAvatar } from './component-avatar/src' const Root = styled.nav` - grid-area: menu; - padding: ${grid(2)}; - // display: flex; - // align-items: center; - // justify-content: space-between; + background: linear-gradient( + 134deg, + ${th('colorPrimary')}, + ${lighten('colorPrimary', 0.3)} + ); border-right: 1px solid ${th('colorFurniture')}; - // background: ${th('colorPrimary')}; - // background: linear-gradient(45deg, #191654, #43C6AC); - background: linear-gradient(134deg, ${th('colorPrimary')}, ${lighten( - 'colorPrimary', - 0.3, -)}); + grid-area: menu; max-height: 100vh; + padding: ${grid(2)}; ` -const Section = styled.div` - // display: flex; - // align-items: center; -` +const Section = styled.div`` // const Logo = styled.span` // // margin: ${grid(2)} 1rem ${grid(2)} 1rem; @@ -48,33 +41,21 @@ const NavItem = ({ className, link, name, icon }) => ( </Link> ) +NavItem.propTypes = { + className: PropTypes.string.isRequired, + link: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, +} + const Item = styled(NavItem)` + align-items: center; border-radius: 10px; - padding-left: ${grid(1)}; + color: ${th('colorTextReverse')}; + display: flex; height: ${grid(5)}; line-height: ${grid(3)}; - display: flex; - align-items: center; - color: ${th('colorTextReverse')}; - - &:hover { - color: ${th('colorText')}; - stroke: ${th('colorText')}; - background-color: ${lighten('colorPrimary', 0.5)}; - svg { - stroke: ${th('colorText')}; - - } - } - - svg { - &:hover { - } - width: 1em; - stroke: ${th('colorTextReverse')}; - } - ${props => props.active && css` @@ -88,26 +69,36 @@ const Item = styled(NavItem)` stroke: ${th('colorText')}; } `} - // align-items: center; - // display: inline-flex; - // margin: calc(${th('gridUnit')} * 3) 1rem calc(${th('gridUnit')} * 3) 0; + + padding-left: ${grid(1)}; + + svg { + stroke: ${th('colorTextReverse')}; + width: 1em; + } + + &:hover { + background-color: ${lighten('colorPrimary', 0.5)}; + color: ${th('colorText')}; + stroke: ${th('colorText')}; + + svg { + stroke: ${th('colorText')}; + } + } ` const UserItem = styled(Link)` - // height: ${grid(5)}; - // line-height: ${grid(2)}; color: ${th('colorTextReverse')}; display: flex; padding-bottom: ${grid(2)}; - // margin-bottom: ${grid(2)}; - // border-bottom: 1px solid ${th('colorFurniture')}; ` const UserInfo = styled.div` - margin-left: ${grid(1)}; display: flex; - justify-content: center; flex-direction: column; + justify-content: center; + margin-left: ${grid(1)}; ` const Menu = ({ @@ -116,6 +107,7 @@ const Menu = ({ navLinkComponents, user, notice, + profileLink, }) => { const location = useLocation() return ( @@ -123,7 +115,11 @@ const Menu = ({ <Section> {/* TODO: Place this notice (used for offline notification) better */} {notice} - <UserComponent loginLink={loginLink} user={user} /> + <UserComponent + loginLink={loginLink} + profileLink={profileLink} + user={user} + /> {navLinkComponents && navLinkComponents.map((navInfo, idx) => ( <Item @@ -137,10 +133,10 @@ const Menu = ({ ) } -const UserComponent = ({ user, loginLink }) => ( +const UserComponent = ({ user, loginLink, profileLink }) => ( <Section> {user && ( - <UserItem title="Go to your profile" to="/journal/profile"> + <UserItem title="Go to your profile" to={profileLink}> <UserAvatar isClickable={false} size={48} user={user} /> <UserInfo> <p>{user.defaultIdentity.name || user.username}</p> @@ -154,13 +150,27 @@ const UserComponent = ({ user, loginLink }) => ( </Section> ) -// Menu.propTypes = { -// brandLink: PropTypes.string, -// brand: PropTypes.node, -// loginLink: PropTypes.string, -// onLogoutClick: PropTypes.func, -// user: PropTypes.object, -// navLinkComponents: PropTypes.arrayOf(PropTypes.element), -// } +Menu.propTypes = { + className: PropTypes.string.isRequired, + loginLink: PropTypes.string.isRequired, + navLinkComponents: PropTypes.arrayOf(PropTypes.object).isRequired, + user: PropTypes.oneOfType([PropTypes.object]), + notice: PropTypes.node.isRequired, + profileLink: PropTypes.string.isRequired, +} + +Menu.defaultProps = { + user: undefined, +} + +UserComponent.propTypes = { + user: PropTypes.oneOfType([PropTypes.object]), + loginLink: PropTypes.string.isRequired, + profileLink: PropTypes.string.isRequired, +} + +UserComponent.defaultProps = { + user: undefined, +} export default Menu diff --git a/app/components/component-dashboard/src/components/Dashboard.js b/app/components/component-dashboard/src/components/Dashboard.js index 11a02f0a3cc40f895840092a1376fc3d3809b185..94c4b691b8f8b0247ab52bc71f799e85a7309f23 100644 --- a/app/components/component-dashboard/src/components/Dashboard.js +++ b/app/components/component-dashboard/src/components/Dashboard.js @@ -1,10 +1,10 @@ -/* eslint-disable react/prop-types */ - import React from 'react' import { useQuery, useMutation } from '@apollo/client' import { Button } from '@pubsweet/ui' // import Authorize from 'pubsweet-client/src/helpers/Authorize' +import config from 'config' +import ReactRouterPropTypes from 'react-router-prop-types' import queries from '../graphql/queries' import mutations from '../graphql/mutations' import { Container, Placeholder } from '../style' @@ -72,11 +72,16 @@ const Dashboard = ({ history, ...props }) => { ) .map(latestVersion) + const urlFrag = config.journal.metadata.toplevel_urlfragment + return ( <Container> <HeadingWithAction> <Heading>Dashboard</Heading> - <Button onClick={() => history.push('/journal/newSubmission')} primary> + <Button + onClick={() => history.push(`${urlFrag}/newSubmission`)} + primary + > + New submission </Button> </HeadingWithAction> @@ -145,4 +150,8 @@ const Dashboard = ({ history, ...props }) => { ) } +Dashboard.propTypes = { + history: ReactRouterPropTypes.history.isRequired, +} + export default Dashboard diff --git a/app/components/component-dashboard/src/components/sections/EditorItem.js b/app/components/component-dashboard/src/components/sections/EditorItem.js index 827eb9075af4cab1f89093e88cda71ebe2812b10..8dfbb1e03f3eb7d23c3d220c6b576cb26f7760eb 100644 --- a/app/components/component-dashboard/src/components/sections/EditorItem.js +++ b/app/components/component-dashboard/src/components/sections/EditorItem.js @@ -1,10 +1,9 @@ -/* eslint-disable react/prop-types */ /* eslint-disable no-underscore-dangle */ -/* eslint-disable no-param-reassign */ - import React from 'react' import styled from 'styled-components' import { Action, ActionGroup } from '@pubsweet/ui' +import config from 'config' +import PropTypes from 'prop-types' import { Item, StatusBadge } from '../../style' import Meta from '../metadata/Meta' import MetadataSubmittedDate from '../metadata/MetadataSubmittedDate' @@ -27,14 +26,16 @@ const getUserFromTeam = (version, role) => { return teams.length ? teams[0].members : [] } +const urlFrag = config.journal.metadata.toplevel_urlfragment + const EditorItemLinks = ({ version }) => ( <ActionGroup> - <Action to={`/journal/versions/${version.parentId || version.id}/submit`}> + <Action to={`${urlFrag}/versions/${version.parentId || version.id}/submit`}> Summary Info </Action> <Action data-testid="control-panel" - to={`/journal/versions/${version.parentId || version.id}/decision`} + to={`${urlFrag}/versions/${version.parentId || version.id}/decision`} > {version.decision && version.decision.status === 'submitted' ? `Decision: ${version.decision.recommendation}` @@ -43,7 +44,12 @@ const EditorItemLinks = ({ version }) => ( </ActionGroup> ) +EditorItemLinks.propTypes = { + version: PropTypes.element.isRequired, +} + const getDeclarationsObject = (version, value) => { + // eslint-disable-next-line no-param-reassign if (!version.meta) version.meta = {} const declarations = version.meta.declarations || {} @@ -95,4 +101,8 @@ const EditorItem = ({ version }) => ( // </Authorize> ) +EditorItem.propTypes = { + version: PropTypes.element.isRequired, +} + export default EditorItem diff --git a/app/components/component-dashboard/src/components/sections/OwnerItem.js b/app/components/component-dashboard/src/components/sections/OwnerItem.js index 18f36cc3dc06a0744e702efe99ff762b2176b33b..c0ba0332d1b99db9bc1d975ff6ae9e015c954d18 100644 --- a/app/components/component-dashboard/src/components/sections/OwnerItem.js +++ b/app/components/component-dashboard/src/components/sections/OwnerItem.js @@ -1,36 +1,42 @@ -/* eslint-disable react/prop-types */ - import React from 'react' import { Link } from 'react-router-dom' +import config from 'config' +import PropTypes from 'prop-types' import { Item, StatusBadge } from '../../style' import VersionTitle from './VersionTitle' import { Icon, ClickableSectionRow } from '../../../../shared' import theme from '../../../../../theme' -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> -) +const urlFrag = config.journal.metadata.toplevel_urlfragment + +const OwnerItem = ({ version }) => { + return ( + <Link + key={`version-${version.id}`} + to={`${urlFrag}/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> + </Item> + </ClickableSectionRow> + </Link> + ) +} + +OwnerItem.propTypes = { + version: PropTypes.oneOfType([PropTypes.object]).isRequired, +} export default OwnerItem diff --git a/app/components/component-dashboard/src/components/sections/ReviewerItem.js b/app/components/component-dashboard/src/components/sections/ReviewerItem.js index 9b8fd16934d355513f268051fb049ccc409012df..28b34d9ffa13aacc5b8f7c94494e558e195d6f0a 100644 --- a/app/components/component-dashboard/src/components/sections/ReviewerItem.js +++ b/app/components/component-dashboard/src/components/sections/ReviewerItem.js @@ -1,9 +1,8 @@ -/* eslint-disable react/prop-types */ -/* eslint-disable no-shadow */ - import React from 'react' import { Action, ActionGroup } from '@pubsweet/ui' // import Authorize from 'pubsweet-client/src/helpers/Authorize' +import PropTypes from 'prop-types' +import config from 'config' import { Item } from '../../style' import VersionTitle from './VersionTitle' @@ -12,9 +11,9 @@ import VersionTitle from './VersionTitle' // TODO: only return actions if not accepted or rejected // TODO: review id in link -const ReviewerItem = ({ version, journals, currentUser, reviewerRespond }) => { +const ReviewerItem = ({ version, currentUser, reviewerRespond }) => { const team = - (version.teams || []).find(team => team.role === 'reviewer') || {} + (version.teams || []).find(team_ => team_.role === 'reviewer') || {} const currentMember = team.members && @@ -22,13 +21,15 @@ const ReviewerItem = ({ version, journals, currentUser, reviewerRespond }) => { const status = currentMember && currentMember.status + const urlFrag = config.journal.metadata.toplevel_urlfragment + return ( <Item> <VersionTitle version={version} /> {(status === 'accepted' || status === 'completed') && ( <ActionGroup> - <Action to={`/journal/versions/${version.id}/review`}> + <Action to={`${urlFrag}/versions/${version.id}/review`}> {status === 'completed' ? 'Completed' : 'Do Review'} </Action> </ActionGroup> @@ -70,4 +71,10 @@ const ReviewerItem = ({ version, journals, currentUser, reviewerRespond }) => { ) } +ReviewerItem.propTypes = { + version: PropTypes.string.isRequired, + currentUser: PropTypes.oneOfType([PropTypes.object]).isRequired, + reviewerRespond: PropTypes.func.isRequired, +} + export default ReviewerItem diff --git a/app/components/component-login/src/Login.jsx b/app/components/component-login/src/Login.jsx index 75fb49aab29fa83a660a0864a41abca76dfbc85e..9ca02b42bf61afb306f699b6b590c5033e8d7a50 100644 --- a/app/components/component-login/src/Login.jsx +++ b/app/components/component-login/src/Login.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { Redirect } from 'react-router-dom' import config from 'config' import { th, grid, lighten } from '@pubsweet/ui-toolkit' -import { H1, Button } from '@pubsweet/ui' +import { Button } from '@pubsweet/ui' import styled from 'styled-components' const getNextUrl = () => { @@ -57,6 +57,12 @@ const Content = styled.div` margin-bottom: ${grid(2)}; } margin-bottom: 1rem; + img { + max-width: 475px; + max-height: 307px; + width: auto; + height: auto; + } ` const Centered = styled.div` @@ -114,10 +120,9 @@ const Login = ({ logo = null, ...props }) => { {journalName === 'Aperture' && ( <img alt="Aperture" src="/public/logo-aperture.png" /> )} - <H1>Login to {journalName}</H1> - {journalName} uses ORCID <StyledORCIDIcon /> to identify authors and - staff. Login with your ORCID account below or{' '} - <a href="https://orcid.org/signin">register at the ORCID website.</a> + {journalName === 'Kotahi' && ( + <img alt="Kotahi" src="/public/logo-kotahi.png" /> + )} <LoginButton onClick={() => (window.location = '/auth/orcid')} primary @@ -125,7 +130,6 @@ const Login = ({ logo = null, ...props }) => { Login with ORCID </LoginButton> </Content> - <div>Powered by Kotahi</div> </Centered> </Container> ) diff --git a/app/components/component-manuscripts/src/Manuscript.jsx b/app/components/component-manuscripts/src/Manuscript.jsx index 00e2856a3f11da5ee774f918a666f29b0123a342..529e267e8ca0c5fc47a855c95e25e1f1a46a0b12 100644 --- a/app/components/component-manuscripts/src/Manuscript.jsx +++ b/app/components/component-manuscripts/src/Manuscript.jsx @@ -1,7 +1,10 @@ +/* eslint-disable react/jsx-filename-extension */ import React from 'react' import gql from 'graphql-tag' import { useMutation } from '@apollo/client' // import { Action } from '@pubsweet/ui' +import config from 'config' +import PropTypes from 'prop-types' import { UserAvatar } from '../../component-avatar/src' import { Row, @@ -26,14 +29,17 @@ const DELETE_MANUSCRIPT = gql` } ` +const urlFrag = config.journal.metadata.toplevel_urlfragment + // manuscriptId is always the parent manuscript's id const User = ({ manuscriptId, manuscript, submitter }) => { const [deleteManuscript] = useMutation(DELETE_MANUSCRIPT, { - update(cache, { data: { deleteManuscript } }) { + update(cache, { data: { deleteManuscriptId } }) { const id = cache.identify({ __typename: 'Manuscript', - id: deleteManuscript, + id: deleteManuscriptId, }) + cache.evict({ id }) }, }) @@ -60,10 +66,10 @@ const User = ({ manuscriptId, manuscript, submitter }) => { )} </Cell> <LastCell> - <Action to={`/journal/versions/${manuscriptId}/decision`}> + <Action to={`${urlFrag}/versions/${manuscriptId}/decision`}> Control </Action> - <Action to={`/journal/versions/${manuscriptId}/manuscript`}> + <Action to={`${urlFrag}/versions/${manuscriptId}/manuscript`}> View </Action> <Action @@ -76,4 +82,10 @@ const User = ({ manuscriptId, manuscript, submitter }) => { ) } +User.propTypes = { + manuscriptId: PropTypes.number.isRequired, + manuscript: PropTypes.element.isRequired, + submitter: PropTypes.element.isRequired, +} + export default User diff --git a/app/components/component-review/src/components/ReviewPage.js b/app/components/component-review/src/components/ReviewPage.js index 9640ac9061164d67798b570ad815487aa3cbed16..8c23b05ef5cb8cfdf26c91a5c7dc92d17630b248 100644 --- a/app/components/component-review/src/components/ReviewPage.js +++ b/app/components/component-review/src/components/ReviewPage.js @@ -3,7 +3,9 @@ import { useMutation, useQuery } from '@apollo/client' import gql from 'graphql-tag' import { Formik } from 'formik' // import { cloneDeep } from 'lodash' -import ReviewLayout from '../components/review/ReviewLayout' +import config from 'config' +import ReactRouterPropTypes from 'react-router-prop-types' +import ReviewLayout from './review/ReviewLayout' import { Spinner } from '../../../shared' import useCurrentUser from '../../../../hooks/useCurrentUser' @@ -152,7 +154,9 @@ const updateReviewMutationQuery = gql` } ` -export default ({ match, ...props }) => { +const urlFrag = config.journal.metadata.toplevel__urlfragment + +const ReviewPage = ({ match, ...props }) => { const currentUser = useCurrentUser() const [updateReviewMutation] = useMutation(updateReviewMutationQuery) const [completeReview] = useMutation(completeReviewMutation) @@ -186,13 +190,11 @@ export default ({ match, ...props }) => { const { manuscript } = data const channelId = manuscript.channels.find(c => c.type === 'editorial').id - // eslint-disable-next-line - const status = ( + const { status } = ( (manuscript.teams.find(team => team.role === 'reviewer') || {}).status || [] - ).find(status => status.user === currentUser.id) || {} - ).status + ).find(statusTemp => statusTemp.user === currentUser.id) || {} const updateReview = (review, file) => { const reviewData = { @@ -215,13 +217,13 @@ export default ({ match, ...props }) => { id: existingReview.current.id || undefined, input: reviewData, }, - update: (cache, { data: { updateReview } }) => { + update: (cache, { data: { updateReviewTemp } }) => { cache.modify({ id: cache.identify(manuscript), fields: { reviews(existingReviewRefs = [], { readField }) { const newReviewRef = cache.writeFragment({ - data: updateReview, + data: updateReviewTemp, fragment: gql` fragment NewReview on Review { id @@ -252,7 +254,7 @@ export default ({ match, ...props }) => { }, }) - history.push('/journal/dashboard') + history.push(`${urlFrag}/dashboard`) } return ( @@ -296,3 +298,10 @@ export default ({ match, ...props }) => { </Formik> ) } + +ReviewPage.propTypes = { + match: ReactRouterPropTypes.match.isRequired, + history: ReactRouterPropTypes.history.isRequired, +} + +export default ReviewPage diff --git a/app/components/component-review/src/components/decision/DecisionReviews.js b/app/components/component-review/src/components/decision/DecisionReviews.js index 7994f0be7e33062dfad397d62cb03e1ac400dd52..c4a741dc07f36dd81c8a3155bc009c2ee4a22ae7 100644 --- a/app/components/component-review/src/components/decision/DecisionReviews.js +++ b/app/components/component-review/src/components/decision/DecisionReviews.js @@ -1,8 +1,11 @@ import React from 'react' import { Action } from '@pubsweet/ui' +import PropTypes from 'prop-types' +import config from 'config' import DecisionReview from './DecisionReview' import { SectionHeader, SectionRow, Title } from '../style' import { SectionContent } from '../../../../shared' + // TODO: read reviewer ordinal and name from project reviewer // const { status } = // getUserFromTeam(manuscript, 'reviewer').filter( @@ -11,14 +14,18 @@ import { SectionContent } from '../../../../shared' // return status const getCompletedReviews = (manuscript, currentUser) => { - const team = manuscript.teams.find(team => team.role === 'reviewer') || {} + const team = manuscript.teams.find(team_ => team_.role === 'reviewer') || {} + if (!team.members) { return null } + const currentMember = team.members.find(m => m.user?.id === currentUser?.id) return currentMember && currentMember.status } +const urlFrag = config.journal.metadata.toplevel_urlfragment + const DecisionReviews = ({ manuscript }) => ( <SectionContent> <SectionHeader> @@ -47,11 +54,15 @@ const DecisionReviews = ({ manuscript }) => ( <SectionRow>No reviews completed yet.</SectionRow> )} <SectionRow> - <Action to={`/journal/versions/${manuscript.id}/reviewers`}> + <Action to={`${urlFrag}/versions/${manuscript.id}/reviewers`}> Manage Reviewers </Action> </SectionRow> </SectionContent> ) +DecisionReviews.propTypes = { + manuscript: PropTypes.element.isRequired, +} + export default DecisionReviews diff --git a/app/components/component-review/src/components/reviewers/Reviewers.js b/app/components/component-review/src/components/reviewers/Reviewers.js index 5ff9ad3024d1df2a45274171f193e96e9f320f56..95356d1e74f180119649c4c340155d65c28c5f7e 100644 --- a/app/components/component-review/src/components/reviewers/Reviewers.js +++ b/app/components/component-review/src/components/reviewers/Reviewers.js @@ -2,6 +2,8 @@ import React from 'react' import styled from 'styled-components' import { Action, Button } from '@pubsweet/ui' import { grid } from '@pubsweet/ui-toolkit' +import PropTypes from 'prop-types' +import config from 'config' import ReviewerForm from './ReviewerForm' import { Container, @@ -19,12 +21,14 @@ import { UserAvatar } from '../../../../component-avatar/src' const ReviewersList = styled.div` display: grid; - grid-template-columns: repeat(auto-fill, minmax(${grid(15)}, 1fr)); grid-gap: ${grid(2)}; + grid-template-columns: repeat(auto-fill, minmax(${grid(15)}, 1fr)); ` const Reviewer = styled.div`` +const urlFrag = config.journal.metadata.toplevel_urlfragment + const Reviewers = ({ journal, isValid, @@ -43,7 +47,7 @@ const Reviewers = ({ <Heading>Reviewers</Heading> <Button onClick={() => - history.push(`/journal/versions/${manuscript.id}/decision`) + history.push(`${urlFrag}/versions/${manuscript.id}/decision`) } primary > @@ -73,7 +77,7 @@ const Reviewers = ({ {reviewers && reviewers.length ? ( <ReviewersList> {reviewers.map(reviewer => ( - <Reviewer> + <Reviewer key={reviewer.id}> <StatusBadge minimal status={reviewer.status} /> <UserAvatar key={reviewer.id} user={reviewer.user} /> {reviewer.user.defaultIdentity.name} @@ -102,4 +106,18 @@ const Reviewers = ({ </Container> ) +Reviewers.propTypes = { + journal: PropTypes.node.isRequired, + isValid: PropTypes.node.isRequired, + loadOptions: PropTypes.node.isRequired, + version: PropTypes.node.isRequired, + reviewers: PropTypes.node.isRequired, + reviewerUsers: PropTypes.node.isRequired, + manuscript: PropTypes.node.isRequired, + handleSubmit: PropTypes.node.isRequired, + removeReviewer: PropTypes.node.isRequired, + teams: PropTypes.node.isRequired, + history: PropTypes.node.isRequired, +} + export default Reviewers diff --git a/app/components/component-submit/src/components/FormTemplate.js b/app/components/component-submit/src/components/FormTemplate.js index a4e76b2a108058f70f05219c9c9ff4c6ba0f39f7..2ebd32a672b68d8c7bc747e65c9531cf5fc38ff4 100644 --- a/app/components/component-submit/src/components/FormTemplate.js +++ b/app/components/component-submit/src/components/FormTemplate.js @@ -11,6 +11,8 @@ import { } from '@pubsweet/ui' import * as validators from 'xpub-validators' import { AbstractEditor } from 'xpub-edit' +import PropTypes, { array } from 'prop-types' +import config from 'config' import { Section as Container, Select, FilesUpload } from '../../../shared' import { Heading1, Section, Legend, SubNote } from '../style' import AuthorsInput from './AuthorsInput' @@ -74,6 +76,14 @@ elements.AbstractEditor = ({ /> ) +elements.AbstractEditor.propTypes = { + validationStatus: PropTypes.node.isRequired, + setTouched: PropTypes.node.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.node.isRequired, + values: PropTypes.node.isRequired, +} + elements.AuthorsInput = AuthorsInput elements.Select = Select elements.LinksInput = LinksInput @@ -81,17 +91,17 @@ elements.LinksInput = LinksInput const rejectProps = (obj, keys) => Object.keys(obj) .filter(k => !keys.includes(k)) - .map(k => Object.assign({}, { [k]: obj[k] })) + .map(k => ({ [k]: obj[k] })) .reduce( (res, o) => - Object.values(o).includes('false') - ? Object.assign({}, res) - : Object.assign(res, o), + Object.values(o).includes('false') ? { ...res } : Object.assign(res, o), {}, ) +const urlFrag = config.journal.metadata.toplevel_urlfragment + const link = (journal, manuscript) => - String.raw`<a href=/journal/versions/${manuscript.id}/manuscript>view here</a>` + String.raw`<a href=${urlFrag}/versions/${manuscript.id}/manuscript>view here</a>` const createMarkup = encodedHtml => ({ __html: unescape(encodedHtml), @@ -109,16 +119,18 @@ const composeValidate = (vld = [], valueField = {}) => value => { validatorFn === 'required' ? validators[validatorFn](value) : validators[validatorFn](valueField[validatorFn])(value) + if (error) { errors.push(error) } + return validatorFn }) return errors.length > 0 ? errors[0] : undefined } -const groupElements = elements => { - const grouped = groupBy(elements, n => n.group || 'default') +const groupElements = els => { + const grouped = groupBy(els, n => n.group || 'default') Object.keys(grouped).forEach(element => { grouped[element].sort( @@ -135,7 +147,7 @@ const groupElements = elements => { startArr = startArr .slice(0, first) .concat([grouped[element]]) - .concat(startArr.slice(first)) // eslint-disable-line no-use-before-define + .concat(startArr.slice(first)) }) return startArr } @@ -149,6 +161,7 @@ const renderArray = (elementsComponentArray, onChange) => ({ const element = elementsComponentArray.find(elv => Object.values(elValues).includes(elv.type), ) + return ( <Section cssOverrides={JSON.parse(element.sectioncss || '{}')} @@ -180,6 +193,7 @@ const renderArray = (elementsComponentArray, onChange) => ({ notesType: element.type, content: value, } + replace(index, data, `${name}.[${index}]`, true) const notes = cloneDeep(values) set(notes, `${name}.[${index}]`, data) @@ -195,18 +209,19 @@ const renderArray = (elementsComponentArray, onChange) => ({ ) }) -const ElementComponentArray = ({ - elementsComponentArray, - onChange, - uploadFile, -}) => ( +const ElementComponentArray = ({ elementsComponentArray, onChange }) => ( <FieldArray name={elementsComponentArray[0].group} render={renderArray(elementsComponentArray, onChange)} /> ) -export default ({ +ElementComponentArray.propTypes = { + elementsComponentArray: PropTypes.oneOfType([array]).isRequired, + onChange: PropTypes.func.isRequired, +} + +const FormTemplate = ({ form, handleSubmit, journal, @@ -299,6 +314,7 @@ export default ({ onChange={value => { // TODO: Perhaps split components remove conditions here let val + if (value.target) { val = value.target.value } else if (value.value) { @@ -306,6 +322,7 @@ export default ({ } else { val = value } + setFieldValue(element.name, val, true) onChange(val, element.name) }} @@ -338,7 +355,7 @@ export default ({ <ElementComponentArray elementsComponentArray={element} // eslint-disable-next-line - key={i} + key={i} onChange={onChange} setFieldValue={setFieldValue} setTouched={setTouched} @@ -384,3 +401,23 @@ export default ({ </Container> ) } + +FormTemplate.propTypes = { + form: PropTypes.element.isRequired, + handleSubmit: PropTypes.element.isRequired, + journal: PropTypes.element.isRequired, + toggleConfirming: PropTypes.element.isRequired, + confirming: PropTypes.element.isRequired, + manuscript: PropTypes.element.isRequired, + setTouched: PropTypes.element.isRequired, + values: PropTypes.element.isRequired, + setFieldValue: PropTypes.element.isRequired, + createSupplementaryFile: PropTypes.element.isRequired, + onChange: PropTypes.element.isRequired, + onSubmit: PropTypes.element.isRequired, + submitSubmission: PropTypes.element.isRequired, + errors: PropTypes.element.isRequired, + validateForm: PropTypes.element.isRequired, +} + +export default FormTemplate diff --git a/app/components/component-submit/src/components/SubmitPage.js b/app/components/component-submit/src/components/SubmitPage.js index 9aefa914562851bf8f188c2530c7095468bfc455..99a7b382acf34063a6362b55150f2ac58191846d 100644 --- a/app/components/component-submit/src/components/SubmitPage.js +++ b/app/components/component-submit/src/components/SubmitPage.js @@ -1,6 +1,8 @@ import React, { useState } from 'react' import { debounce, cloneDeep, set } from 'lodash' import { gql, useQuery, useMutation } from '@apollo/client' +import config from 'config' +import ReactRouterPropTypes from 'react-router-prop-types' import Submit from './Submit' import { Spinner } from '../../../shared' import gatherManuscriptVersions from '../../../../shared/manuscript_versions' @@ -170,11 +172,13 @@ const createNewVersionMutation = gql` } ` +const urlFrag = config.journal.metadata.toplevel_urlfragment + const SubmitPage = ({ match, history, ...props }) => { const [confirming, setConfirming] = useState(false) const toggleConfirming = () => { - setConfirming(confirming => !confirming) + setConfirming(confirm => !confirm) } const { data, loading, error } = useQuery(query, { @@ -192,11 +196,11 @@ const SubmitPage = ({ match, history, ...props }) => { const manuscript = data?.manuscript const form = data?.getFile - const updateManuscript = (versionId, manuscript) => + const updateManuscript = (versionId, manuscriptInput) => update({ variables: { id: versionId, - input: JSON.stringify(manuscript), + input: JSON.stringify(manuscriptInput), }, }) @@ -211,18 +215,18 @@ const SubmitPage = ({ match, history, ...props }) => { return debouncers[path](versionId, input) } - const onSubmit = async (versionId, manuscript) => { - const updateManuscript = { + const onSubmit = async versionId => { + const updateManuscriptInput = { status: 'submitted', } await submit({ variables: { id: versionId, - input: JSON.stringify(updateManuscript), + input: JSON.stringify(updateManuscriptInput), }, }) - history.push('/journal/dashboard') + history.push(`${urlFrag}/dashboard`) } const versions = gatherManuscriptVersions(manuscript) @@ -242,4 +246,9 @@ const SubmitPage = ({ match, history, ...props }) => { ) } +SubmitPage.propTypes = { + history: ReactRouterPropTypes.history.isRequired, + match: ReactRouterPropTypes.match.isRequired, +} + export default SubmitPage diff --git a/app/components/component-submit/src/upload.js b/app/components/component-submit/src/upload.js index 7e6f76a3e484f1b1c6de6322dc3548643dc091aa..8e4eb1d50af7f61e6d56d2c8bab1f0701b3e5883 100644 --- a/app/components/component-submit/src/upload.js +++ b/app/components/component-submit/src/upload.js @@ -3,6 +3,8 @@ import request from 'pubsweet-client/src/helpers/api' import gql from 'graphql-tag' import currentRolesVar from '../../../shared/currentRolesVar' +const urlFrag = config.journal.metadata.toplevel_urlfragment + const generateTitle = name => name .replace(/[_-]+/g, ' ') // convert hyphens/underscores to space @@ -184,7 +186,7 @@ const createManuscriptPromise = ( const redirectPromise = (setConversionState, journals, history, data) => { setConversionState(() => ({ converting: false, completed: true })) - const route = `/journal/versions/${data.createManuscript.id}/submit` + const route = `${urlFrag}/versions/${data.createManuscript.id}/submit` // redirect after a short delay window.setTimeout(() => { history.push(route) diff --git a/app/routes.js b/app/routes.js index f427249776032b4e3f582529b04a75874a947118..e9a6019e4e806cc24332e41477c776da6da6f368 100644 --- a/app/routes.js +++ b/app/routes.js @@ -1,6 +1,7 @@ import React from 'react' import { Route, Switch } from 'react-router-dom' +import config from 'config' import Login from './components/component-login/src' import AdminPage from './components/AdminPage' @@ -13,7 +14,7 @@ import { export default ( <Switch> {/* AdminPage has nested routes within */} - <Route path="/journal"> + <Route path={config.journal.metadata.toplevel_urlfragment}> <AdminPage /> </Route> <Route component={Login} path="/login" /> diff --git a/config/default.js b/config/default.js index f6f93f7ed11e9e7d0d9bc2bc71381b355c94bcf5..478b96d0c4be5e9bf9eeb24b2b2836ffe5b2d56c 100644 --- a/config/default.js +++ b/config/default.js @@ -136,7 +136,7 @@ module.exports = { }, 'pubsweet-client': { API_ENDPOINT: '/api', - 'login-redirect': '/journal/dashboard', + 'login-redirect': `${journal.metadata.toplevel_urlfragment}/dashboard`, theme: process.env.PUBSWEET_THEME, baseUrl: deferConfig(cfg => { const { protocol, host, port } = cfg['pubsweet-client'] diff --git a/config/journal/metadata.js b/config/journal/metadata.js index 47074571b820d9123bb8fdec3442a8e024c0a669..27f8f71e190c7291482462f08f08b45523558de7 100644 --- a/config/journal/metadata.js +++ b/config/journal/metadata.js @@ -1,4 +1,5 @@ module.exports = { issn: '0000-0001', - name: 'Aperture', + name: 'Kotahi', + toplevel_urlfragment: '/kotahi', } diff --git a/docker-compose.production.yml b/docker-compose.production.yml index c384fe6f56b66126f1731887e20f564f357f4bda..93fd3222948c874d31b3667daab9264719fbfa2d 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -11,6 +11,9 @@ services: - server_protocol=${SERVER_PROTOCOL} - server_host=${SERVER_HOST} - server_port=${SERVER_PORT} + - client_protocol=${CLIENT_PROTOCOL} + - client_host=${CLIENT_HOST} + - client_port=${CLIENT_PORT} ports: - ${SERVER_PORT:-3000}:${SERVER_PORT:-3000} environment: diff --git a/docker-compose.yml b/docker-compose.yml index 661d365e11a8ce522d6e832cc457cf033693db94..a9e21fb04ac462009a8ac256e22b817d7620495d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - CLIENT_PORT=${CLIENT_PORT:-4000} - SERVER_PROTOCOL=http - SERVER_HOST=server - - SERVER_PORT=3000 + - SERVER_PORT=${SERVER_PORT:-3000} volumes: - ./app:/home/node/app/app - ./config:/home/node/app/config diff --git a/package.json b/package.json index 3514b8f4038c9374f405255f7e12d96c402bddd1..bd2b0358d8a512a7c2ea5c823d9718aa5a0daccc 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "express": "^4.17.1", "faker": "4.1.0", "font-awesome": "4.7.0", + "formik": "^2.2.6", "fs-extra": "4.0.3", "got": "11.7.0", "graphql": "14.7.0", @@ -120,6 +121,7 @@ "react": "16.13.1", "react-dom": "16.13.1", "react-dropzone": "10.2.2", + "react-hot-loader": "^4.13.0", "react-html-parser": "2.0.2", "react-image": "4.0.3", "react-js-pagination": "^3.0.3", diff --git a/public/logo-kotahi.png b/public/logo-kotahi.png new file mode 100644 index 0000000000000000000000000000000000000000..369d79aa7c859f414475c8d08bb9bd95f53316c3 Binary files /dev/null and b/public/logo-kotahi.png differ diff --git a/webpack/plugins.js b/webpack/plugins.js index 9a2c29675264d7660c14ee29a71d195f44e4c003..c0c78395ce80326b96001043bb93e8b3ef25124a 100644 --- a/webpack/plugins.js +++ b/webpack/plugins.js @@ -5,8 +5,8 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const CompressionPlugin = require('compression-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') - .BundleAnalyzerPlugin +// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') +// .BundleAnalyzerPlugin module.exports = (opts = {}) => { const plugins = [] @@ -38,9 +38,15 @@ module.exports = (opts = {}) => { } plugins.push( - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': `"${opts.env}"`, - }), + new webpack.EnvironmentPlugin([ + 'NODE_ENV', + 'SERVER_PROTOCOL', + 'SERVER_HOST', + 'SERVER_PORT', + 'CLIENT_PROTOCOL', + 'CLIENT_HOST', + 'CLIENT_PORT', + ]), ) // put dynamically required modules into the build diff --git a/yarn.lock b/yarn.lock index 0c0d80940dcd75e08b174e394f96c204a91fc73f..1c288bfd52dc4dfb81809f1fd6cfed4de133281c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10946,6 +10946,19 @@ formik@^1.4.2, formik@^2.0.0: tiny-warning "^1.0.2" tslib "^1.10.0" +formik@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.6.tgz#378a4bafe4b95caf6acf6db01f81f3fe5147559d" + integrity sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA== + dependencies: + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.14" + lodash-es "^4.17.14" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^1.10.0" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"