diff --git a/packages/component-invite/src/helpers/helpers.js b/packages/component-invite/src/helpers/helpers.js index bf0077c7692b634c51ab168063b5b2f16e4ee3fa..1376dc9945afcf01c65692b1ae80b7ea3755f151 100644 --- a/packages/component-invite/src/helpers/helpers.js +++ b/packages/component-invite/src/helpers/helpers.js @@ -44,7 +44,7 @@ const validateEmailAndToken = async (email, token, userModel) => { message: e.details[0].message, } } - logger.error(e) + logger.error('internal server error') return { success: false, status: 500, diff --git a/packages/component-invite/src/routes/post.js b/packages/component-invite/src/routes/post.js index 52d366519669c91b566d5f8da5f96eb42c8817a2..b70c27364eca635f3e2e8dbe9efc5de0a0c4f4aa 100644 --- a/packages/component-invite/src/routes/post.js +++ b/packages/component-invite/src/routes/post.js @@ -32,13 +32,13 @@ module.exports = models => async (req, res) => { res.status(inviteRight.status).json({ error: inviteRight.message, }) - logger.error(`incorrect role when inviting a user`) + logger.error(`incorrect role when inviting a ${role}`) return } } else if (collectionId) { if (!configRoles.collection.includes(role)) { res - .status(400) + .status(403) .json({ error: `Role ${role} cannot be set on collections` }) logger.error(`invitation has been attempted with invalid role: ${role}`) return @@ -65,7 +65,7 @@ module.exports = models => async (req, res) => { error: `${reqUser.roles || 'undefined roles'} cannot invite a ${role} without a collection`, }) - logger.error(`request user does not have any defined roles`) + logger.error(`cannot invite manuscript roles without a collection`) return } @@ -74,7 +74,7 @@ module.exports = models => async (req, res) => { if (user) { res.status(400).json({ error: 'User already exists' }) - logger.error('admin tried to invite existing user') + logger.error('someone tried to invite existing user') return } } catch (e) { diff --git a/packages/component-invite/src/routes/reset.js b/packages/component-invite/src/routes/reset.js index cb498f1eb3a9434be192112df5043efefd43dd20..983177fa855832d97dfcc09bca309f27aef86af6 100644 --- a/packages/component-invite/src/routes/reset.js +++ b/packages/component-invite/src/routes/reset.js @@ -2,12 +2,30 @@ const logger = require('@pubsweet/logger') const helpers = require('../helpers/helpers') module.exports = models => async (req, res) => { - if (!helpers.checkForUndefinedParams(req.body)) { + const { + email, + firstName, + lastName, + title, + affiliation, + password, + token, + } = req.body + if ( + !helpers.checkForUndefinedParams( + email, + firstName, + lastName, + title, + affiliation, + password, + token, + ) + ) { res.status(400).json({ error: 'missing required params' }) return } - const { password } = req.body if (password.length < 7) { res .status(400) @@ -18,18 +36,9 @@ module.exports = models => async (req, res) => { return } - const updateFields = { - password, - firstName: req.body.firstName, - lastName: req.body.lastName, - affiliation: req.body.affiliation, - title: req.body.title, - isConfirmed: true, - } - const validateResponse = await helpers.validateEmailAndToken( - req.body.email, - req.body.token, + email, + token, models.User, ) if (validateResponse.success === false) { @@ -44,6 +53,15 @@ module.exports = models => async (req, res) => { return } + const updateFields = { + password, + firstName, + lastName, + affiliation, + title, + isConfirmed: true, + } + let newUser = Object.assign( validateResponse.user, updateFields, diff --git a/packages/component-invite/src/tests/fixtures/collections.js b/packages/component-invite/src/tests/fixtures/collections.js index 959e3b413fe82696d18505c60a3e5caafa5a7947..91eaf3b200444735abaac4e29d4561527829b84d 100644 --- a/packages/component-invite/src/tests/fixtures/collections.js +++ b/packages/component-invite/src/tests/fixtures/collections.js @@ -1,12 +1,10 @@ -const users = require('./users') - module.exports = { standardCollection: { id: '2c4fb766-a798-4c32-b857-c5d21a2ab331', title: 'Standard Collection', type: 'collection', fragments: [], - owners: [users.admin.id], + owners: [], save: jest.fn(), }, } diff --git a/packages/component-invite/src/tests/fixtures/fixtures.js b/packages/component-invite/src/tests/fixtures/fixtures.js index b0a926aa1427d8662addf9dd3e46c6a61147f915..0ea29e85ac3a373a20a0e82377e0b6f3dfeef51e 100644 --- a/packages/component-invite/src/tests/fixtures/fixtures.js +++ b/packages/component-invite/src/tests/fixtures/fixtures.js @@ -1,7 +1,9 @@ const users = require('./users') const collections = require('./collections') +const teams = require('./teams') module.exports = { users, collections, + teams, } diff --git a/packages/component-invite/src/tests/fixtures/teams.js b/packages/component-invite/src/tests/fixtures/teams.js new file mode 100644 index 0000000000000000000000000000000000000000..1410dc9e2ce5475bd299847a0fa1db65dee57f66 --- /dev/null +++ b/packages/component-invite/src/tests/fixtures/teams.js @@ -0,0 +1,15 @@ +const users = require('./users') + +module.exports = { + teamType: { + name: 'editorInChief', + permissions: 'editor', + }, + group: 'editor', + name: 'Editor in Chief', + object: { + type: 'collection', + id: '123', + }, + members: [users.editorInChief.id], +} diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js index 6a13d1eb223024f9bb580b5c31e96b96c20c8028..0e207530439ded9a197b616c4f0e74cd8d6ceedd 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-invite/src/tests/fixtures/users.js @@ -1,4 +1,4 @@ -module.exports = { +const users = { admin: { type: 'user', username: 'admin', @@ -7,4 +7,31 @@ module.exports = { admin: true, id: 'admin123', }, + editorInChief: { + type: 'user', + username: 'editor', + email: 'editor@example.com', + password: 'test1234', + admin: false, + id: 'editor123', + roles: ['editorInChief'], + passwordResetToken: 'token123', + firstName: 'vlad', + lastName: 'dracul', + affiliation: 'MIT', + title: 'prof', + save: jest.fn(() => users.editorInChief), + isConfirmed: false, + }, + handlingEditor: { + type: 'user', + username: 'handling', + email: 'handling@example.com', + password: 'test', + admin: false, + id: 'handling123', + roles: ['handlingEditor'], + }, } + +module.exports = users diff --git a/packages/component-invite/src/tests/get.test.js b/packages/component-invite/src/tests/get.test.js new file mode 100644 index 0000000000000000000000000000000000000000..db3b68af7effa3400e2655e0b4762af92f7b63d1 --- /dev/null +++ b/packages/component-invite/src/tests/get.test.js @@ -0,0 +1,103 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const httpMocks = require('node-mocks-http') +const fixtures = require('./fixtures/fixtures') + +const buildModels = user => { + const models = { + User: { + findByEmail: jest.fn( + () => + user instanceof Error ? Promise.reject(user) : Promise.resolve(user), + ), + }, + } + + return models +} +const user = fixtures.users.editorInChief +const query = { + email: user.email, + token: user.passwordResetToken, +} +describe('Get inivte data route handler', () => { + it('should return success email and token are correct', async () => { + const req = httpMocks.createRequest() + req.query = query + const res = httpMocks.createResponse() + const models = buildModels(user) + await require('../routes/get')(models)(req, res) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.firstName).toEqual(user.firstName) + }) + it('should return an error when some parameters are missing', async () => { + delete query.email + const req = httpMocks.createRequest() + req.query = query + + const res = httpMocks.createResponse() + const models = buildModels(user) + await require('../routes/get')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('missing required params') + query.email = user.email + }) + it('should return an error when tokens do not match', async () => { + query.token = 'wrongToken' + const req = httpMocks.createRequest() + req.query = query + + const res = httpMocks.createResponse() + const models = buildModels(user) + await require('../routes/get')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('invalid request') + query.token = user.passwordResetToken + }) + it('should return an error when user is not found', async () => { + const req = httpMocks.createRequest() + req.query = query + const error = new Error() + error.name = 'NotFoundError' + error.status = 404 + const res = httpMocks.createResponse() + const models = buildModels(error) + await require('../routes/get')(models)(req, res) + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('user not found') + }) + it('should return an error when there is a validation problem', async () => { + const req = httpMocks.createRequest() + req.query = query + const error = new Error() + error.name = 'ValidationError' + error.status = 400 + error.details = [] + error.details.push({ message: 'validation error' }) + const res = httpMocks.createResponse() + const models = buildModels(error) + await require('../routes/get')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('validation error') + }) + it('should return an error when there is a system problem', async () => { + const req = httpMocks.createRequest() + req.query = query + const error = new Error() + error.details = [] + error.details.push({ message: 'internal server error' }) + const res = httpMocks.createResponse() + const models = buildModels(error) + await require('../routes/get')(models)(req, res) + expect(res.statusCode).toBe(500) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('internal server error') + }) +}) diff --git a/packages/component-invite/src/tests/mocks/Team.js b/packages/component-invite/src/tests/mocks/Team.js new file mode 100644 index 0000000000000000000000000000000000000000..f84ef4dcf8ffa0e5056c8f0eb86573f624d74c06 --- /dev/null +++ b/packages/component-invite/src/tests/mocks/Team.js @@ -0,0 +1,16 @@ +/* eslint-disable func-names-any */ + +function Team(properties) { + this.teamType = properties.teamType + this.group = properties.group + this.name = properties.name + this.object = properties.object + this.members = properties.members +} + +Team.prototype.save = jest.fn(function saveTeam() { + this.id = '111222' + return Promise.resolve(this) +}) + +module.exports = Team diff --git a/packages/component-invite/src/tests/post.test.js b/packages/component-invite/src/tests/post.test.js index dc39ab175b4365b8363250792023d803b4c333ad..e6e2d5c2defe50f3cf3aa836bcc11137e7dea96a 100644 --- a/packages/component-invite/src/tests/post.test.js +++ b/packages/component-invite/src/tests/post.test.js @@ -6,11 +6,13 @@ const random = require('lodash/random') const fixtures = require('./fixtures/fixtures') const UserMock = require('./mocks/User') const Chance = require('chance') +const TeamMock = require('./mocks/Team') jest.mock('pubsweet-component-mail-service', () => ({ setupEmail: jest.fn() })) const chance = new Chance() const globalRoles = ['editorInChief', 'author', 'admin'] const manuscriptRoles = ['handlingEditor', 'reviewer'] + const buildModels = (collection, findUser, emailUser) => { const models = { User: {}, @@ -22,8 +24,8 @@ const buildModels = (collection, findUser, emailUser) => { : Promise.resolve(collection), ), }, + Team: {}, } - UserMock.find = jest.fn( () => findUser instanceof Error @@ -38,6 +40,7 @@ const buildModels = (collection, findUser, emailUser) => { ) models.User = UserMock + models.Team = TeamMock return models } @@ -51,22 +54,22 @@ const body = { } body.admin = body.role === 'admin' -const getNotFoundError = () => { - const error = new Error() - error.name = 'NotFoundError' - error.status = 404 +const notFoundError = new Error() +notFoundError.name = 'NotFoundError' +notFoundError.status = 404 + +const adminUser = fixtures.users.admin +const editorInChiefUser = fixtures.users.editorInChief +const handlingEditorUser = fixtures.users.handlingEditor - return error -} describe('Post invite route handler', () => { - it('should return success when the admin invites an Editor in Chief, Author or Admin', async () => { + it('should return success when the admin invites a global role', async () => { const req = httpMocks.createRequest({ body, }) - req.user = fixtures.users.admin + req.user = adminUser const res = httpMocks.createResponse() - const error = getNotFoundError() - const models = buildModels(error, fixtures.users.admin, error) + const models = buildModels(notFoundError, adminUser, notFoundError) await require('../routes/post')(models)(req, res) expect(res.statusCode).toBe(200) @@ -80,11 +83,10 @@ describe('Post invite route handler', () => { const req = httpMocks.createRequest({ body, }) - req.user = fixtures.users.admin + req.user = adminUser req.params.collectionId = '123' const res = httpMocks.createResponse() - const error = getNotFoundError() - const models = buildModels(error, fixtures.users.admin) + const models = buildModels(notFoundError, adminUser) await require('../routes/post')(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) @@ -94,13 +96,13 @@ describe('Post invite route handler', () => { }) it('should return an error when the admin invites a manuscript role', async () => { body.role = manuscriptRoles[random(0, manuscriptRoles.length - 1)] + body.admin = false const req = httpMocks.createRequest({ body, }) - req.user = fixtures.users.admin + req.user = adminUser const res = httpMocks.createResponse() - const error = getNotFoundError() - const models = buildModels(error, fixtures.users.admin) + const models = buildModels(notFoundError, adminUser) await require('../routes/post')(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) @@ -111,13 +113,105 @@ describe('Post invite route handler', () => { const req = httpMocks.createRequest({ body, }) - req.user = fixtures.users.admin + req.user = adminUser + const res = httpMocks.createResponse() + const models = buildModels(notFoundError, adminUser) + await require('../routes/post')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Email and role are required') + body.email = chance.email() + }) + it('should return an error when the a non-admin invites a globalRole on a collection', async () => { + body.role = globalRoles[random(0, globalRoles.length - 1)] + body.admin = body.role === 'admin' + const req = httpMocks.createRequest({ + body, + }) + req.user = editorInChiefUser + req.params.collectionId = '123' + const res = httpMocks.createResponse() + const models = buildModels(notFoundError, editorInChiefUser) + await require('../routes/post')(models)(req, res) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`Role ${body.role} cannot be set on collections`) + }) + it('should return an error when an non-admin invites without a collection', async () => { + body.role = manuscriptRoles[random(0, manuscriptRoles.length - 1)] + body.admin = false + const req = httpMocks.createRequest({ + body, + }) + req.user = editorInChiefUser + const res = httpMocks.createResponse() + + const models = buildModels(notFoundError, editorInChiefUser) + await require('../routes/post')(models)(req, res) + expect(res.statusCode).toBe(403) + // console.log(res._getData()) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `${req.user.roles} cannot invite a ${body.role} without a collection`, + ) + }) + it('should return an error when inviting an existing user', async () => { + body.role = globalRoles[random(0, globalRoles.length - 1)] + body.admin = body.role === 'admin' + const req = httpMocks.createRequest({ + body, + }) + req.user = adminUser const res = httpMocks.createResponse() - const error = getNotFoundError() - const models = buildModels(error, fixtures.users.admin) + const models = buildModels(notFoundError, adminUser, editorInChiefUser) await require('../routes/post')(models)(req, res) + expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`Email and role are required`) + expect(data.error).toEqual('User already exists') + }) + it('should return success when the editor in chief invites a handling Editor with a collection', async () => { + body.role = 'handlingEditor' + body.admin = false + const req = httpMocks.createRequest({ + body, + }) + req.user = editorInChiefUser + req.params.collectionId = '123' + const res = httpMocks.createResponse() + const models = buildModels( + fixtures.collections.standardCollection, + editorInChiefUser, + notFoundError, + ) + await require('../routes/post')(models)(req, res) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.roles[0]).toEqual(body.role) + expect(data.firstName).toEqual(body.firstName) + expect(data.email).toEqual(body.email) + }) + it('should return success when the handlintEditor invites a reviewer with a collection', async () => { + body.role = 'reviewer' + body.admin = false + const req = httpMocks.createRequest({ + body, + }) + req.user = handlingEditorUser + req.params.collectionId = '123' + const res = httpMocks.createResponse() + const models = buildModels( + fixtures.collections.standardCollection, + handlingEditorUser, + notFoundError, + ) + await require('../routes/post')(models)(req, res) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.roles[0]).toEqual(body.role) + expect(data.firstName).toEqual(body.firstName) + expect(data.email).toEqual(body.email) }) }) diff --git a/packages/component-invite/src/tests/reset.test.js b/packages/component-invite/src/tests/reset.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b2ea34c15e50105b1e904834bb9c58fb695ce1c8 --- /dev/null +++ b/packages/component-invite/src/tests/reset.test.js @@ -0,0 +1,125 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const httpMocks = require('node-mocks-http') +const fixtures = require('./fixtures/fixtures') +const UserMock = require('./mocks/User') + +const buildModels = user => { + const models = { + User: {}, + } + UserMock.findByEmail = jest.fn( + () => + user instanceof Error ? Promise.reject(user) : Promise.resolve(user), + ) + + models.User = UserMock + return models +} + +const editorInChiefUser = fixtures.users.editorInChief +const body = { + email: editorInChiefUser.email, + firstName: editorInChiefUser.firstName, + lastName: editorInChiefUser.lastName, + title: editorInChiefUser.email, + affiliation: editorInChiefUser.email, + password: editorInChiefUser.password, + token: editorInChiefUser.passwordResetToken, + isConfirmed: false, +} + +const notFoundError = new Error() +notFoundError.name = 'NotFoundError' +notFoundError.status = 404 + +describe('Password reset after invite route handler', () => { + it('should return success when the body is correct', async () => { + const req = httpMocks.createRequest({ body }) + const res = httpMocks.createResponse() + const models = buildModels(editorInChiefUser) + await require('../routes/reset')(models)(req, res) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.firstName).toEqual(body.firstName) + expect(data.email).toEqual(body.email) + editorInChiefUser.passwordResetToken = 'token123' + }) + it('should return an error when some parameters are missing', async () => { + delete body.email + const req = httpMocks.createRequest({ body }) + + const res = httpMocks.createResponse() + const models = buildModels(editorInChiefUser) + await require('../routes/reset')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('missing required params') + body.email = editorInChiefUser.email + }) + it('should return an error when the password is too small', async () => { + body.password = 'small' + const req = httpMocks.createRequest({ body }) + + const res = httpMocks.createResponse() + const models = buildModels(editorInChiefUser) + await require('../routes/reset')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + 'password needs to be at least 7 characters long', + ) + body.password = editorInChiefUser.password + }) + it('should return an error when user is not found', async () => { + const req = httpMocks.createRequest({ body }) + const error = new Error() + error.name = 'NotFoundError' + error.status = 404 + const res = httpMocks.createResponse() + const models = buildModels(error) + await require('../routes/reset')(models)(req, res) + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('user not found') + }) + it('should return an error when there is a validation problem', async () => { + const req = httpMocks.createRequest({ body }) + const error = new Error() + error.name = 'ValidationError' + error.status = 400 + error.details = [] + error.details.push({ message: 'validation error' }) + const res = httpMocks.createResponse() + const models = buildModels(error) + await require('../routes/reset')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('validation error') + }) + it('should return an error when there is a system problem', async () => { + const req = httpMocks.createRequest({ body }) + const error = new Error() + error.details = [] + error.details.push({ message: 'internal server error' }) + const res = httpMocks.createResponse() + const models = buildModels(error) + await require('../routes/reset')(models)(req, res) + expect(res.statusCode).toBe(500) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('internal server error') + }) + it('should return an error when the user is already confirmed', async () => { + editorInChiefUser.isConfirmed = true + const req = httpMocks.createRequest({ body }) + const res = httpMocks.createResponse() + const models = buildModels(editorInChiefUser) + await require('../routes/reset')(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('User is already confirmed') + editorInChiefUser.isConfirmed = false + }) +}) diff --git a/packages/components-faraday/src/components/Dashboard/Dashboard.js b/packages/components-faraday/src/components/Dashboard/Dashboard.js index fbf0cdcb4767a840ceba2db862f9d584d549b3e9..6fb0797f1e44e513604adfb3cc14886fd4738687 100644 --- a/packages/components-faraday/src/components/Dashboard/Dashboard.js +++ b/packages/components-faraday/src/components/Dashboard/Dashboard.js @@ -10,12 +10,10 @@ import DashboardItems from './DashboardItems' import DashboardFilters from './DashboardFilters' const Dashboard = ({ - changeViewMode, createDraftSubmission, currentUser, dashboard, deleteProject, - listView, filters, getItems, getFilterOptions, @@ -36,14 +34,11 @@ const Dashboard = ({ </div> <DashboardFilters changeFilterValue={changeFilterValue} - changeView={changeViewMode} getFilterOptions={getFilterOptions} - listView={listView} /> <DashboardItems deleteProject={deleteProject} list={getItems()} - listView={listView} showAbstractModal={showAbstractModal} /> </div> diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index 08596722864c7401152e8ed74f8b983dda189ac8..e0e31e49ee212dea98f30329d2e4fe8909e20ff5 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -1,125 +1,353 @@ import React from 'react' -import classnames from 'classnames' +import PropTypes from 'prop-types' import { get, isEmpty } from 'lodash' import styled from 'styled-components' import { Button, Icon } from '@pubsweet/ui' +import { compose, getContext } from 'recompose' import { parseVersion, getFilesURL, downloadAll } from './utils' -import classes from './Dashboard.local.scss' const DashboardCard = ({ deleteProject, history, - listView, project, version, showAbstractModal, + journal, + ...rest }) => { - const { submitted, author, title, type, version: vers } = parseVersion( - version, - ) + const { submitted, title, type, version: vers } = parseVersion(version) const files = getFilesURL(get(version, 'files')) const status = get(project, 'status') || 'Draft' const hasFiles = !isEmpty(files) const abstract = get(version, 'metadata.abstract') const metadata = get(version, 'metadata') - return ( - <div className={classes.card}> - <div className={classes.leftSide}> - <div - className={classes.title} - dangerouslySetInnerHTML={{ __html: title }} // eslint-disable-line - /> - - <div className={classes.quickInfo}> - <div className={classes.status}>{status}</div> - <div className={classes.version}>{`v${vers} ${ - submitted ? `- updated on ${submitted}` : '' - }`}</div> - </div> - </div> - <div className={classes.rightSide}> - <div - className={classnames({ - [classes.disabled]: !hasFiles, - [classes.pointer]: true, - })} - onClick={() => (hasFiles ? downloadAll(files) : null)} - > - <Icon>download</Icon> - </div> - <div className={classes.pointer} onClick={() => deleteProject(project)}> - <Icon>trash-2</Icon> - </div> - <div - className={classes.pointer} - onClick={() => - history.push( - `/projects/${project.id}/versions/${version.id}/submit`, - ) - } - > - <Icon>file-text</Icon> - </div> - </div> - {!listView && ( - <div className={classes.expandedView}> - <div className={classes.column3}> - <div className={classes.column2}> - <div>Submission author</div> - <div>Abstract</div> + <Card> + <ListView> + <Left> + <Title + dangerouslySetInnerHTML={{ __html: title }} // eslint-disable-line + /> + <ManuscriptInfo> + <div> + <Status>{status}</Status> + <DateField>{submitted || ''}</DateField> </div> - <div className={classes.column2}> - <div>{author}</div> - {abstract && ( - <ViewAbstractContainer onClick={showAbstractModal(metadata)}> - <Icon color="#667080" size={18}> - eye - </Icon> - <span>View</span> - </ViewAbstractContainer> - )} + <div> + <Version>{`v${vers} - `}</Version> + <ManuscriptId>{`ID ${version.id.split('-')[0]}`}</ManuscriptId> + <ManuscriptType>{type}</ManuscriptType> </div> + </ManuscriptInfo> + </Left> + <Right> + <ClickableIcon + disabled={!hasFiles} + onClick={() => (hasFiles ? downloadAll(files) : null)} + > + <Icon>download</Icon> + </ClickableIcon> + <ClickableIcon onClick={() => deleteProject(project)}> + <Icon>trash-2</Icon> + </ClickableIcon> + <ClickableIcon> + <Icon>more-horizontal</Icon> + </ClickableIcon> + <Details + onClick={() => + history.push( + `/projects/${project.id}/versions/${version.id}/submit`, + ) + } + > + Details + <Icon color="#667080">chevron-right</Icon> + </Details> + </Right> + </ListView> + <DetailsView> + <LeftDetails> + <JournalTitle>{journal.metadata.nameText}</JournalTitle> + <Issue> + {journal.issueTypes.find(t => t.value === metadata.issue).label} + </Issue> + {get(version, 'authors') && ( + <Authors> + <span>Authors:</span> + <AuthorList> + {version.authors + .map(({ firstName, lastName }) => `${firstName} ${lastName}`) + .join(', ')} + </AuthorList> + </Authors> + )} + <PreviewContainer> + {abstract && ( + <ClickableIconContainer onClick={showAbstractModal(metadata)}> + <Icon color="#667080" size={18}> + eye + </Icon> + <span>Abstract</span> + </ClickableIconContainer> + )} + <ClickableIconContainer> + <Icon color="#667080" size={18}> + eye + </Icon> + <span>Cover letter</span> + </ClickableIconContainer> + </PreviewContainer> + </LeftDetails> + <RightDetails> + <div> + <Label>Handling editor</Label> + <ActionButtons>ASSIGN</ActionButtons> </div> - <div className={classes.column3}> - <div className={classes.column2}> - <div>Submitted On</div> - <div>Type</div> - </div> - <div className={classes.column2}> - <div>{submitted}</div> - <div> - <span className={classes.status}>{type}</span> - </div> - </div> + <div> + <Label>Reviewers</Label> + <ActionButtons>INVITE</ActionButtons> </div> - <div className={classes.column3}> - <div className={classes.column2}> - <div>Handling Editor</div> - <div>Reviewers</div> - </div> - <div className={classes.column2}> - <Button className={classes.button} primary> - Invite - </Button> - <Button className={classes.button} primary> - Invite - </Button> - </div> - </div> - </div> - )} - </div> + </RightDetails> + </DetailsView> + </Card> ) } -const ViewAbstractContainer = styled.div` +export default compose(getContext({ journal: PropTypes.object }))(DashboardCard) + +// #region styled-components +const PreviewContainer = styled.div` + display: flex; + margin-top: 18px; +` + +const AuthorList = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + white-space: nowrap; + max-width: 400px; + width: 400px; +` + +const Authors = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-top: 15px; + + span:first-child { + color: #667080; + font-family: Helvetica; + font-size: 12px; + margin-right: 8px; + text-align: left; + text-transform: uppercase; + } +` + +const ActionButtons = styled(Button)` + align-items: center; + background-color: #667080; + display: flex; + height: 20px; + padding: 4px 8px; + font-family: Helvetica; + font-size: 12px; + text-align: center; + color: #ffffff; +` + +const LeftDetails = styled.div` + display: flex; + flex: 3; + flex-direction: column; + justify-content: flex-start; + padding: 10px 20px; +` + +const RightDetails = styled.div` + display: flex; + flex: 2; + flex-direction: column; + + div { + align-items: center; + display: flex; + flex-direction: row; + margin: 6px 0; + } +` + +const Label = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 12px; + text-align: left; + text-transform: uppercase; + width: 150px; +` + +const JournalTitle = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 14px; + font-weight: bold; + text-align: left; +` + +const Issue = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 14px; + text-align: left; +` + +const DetailsView = styled.div` + align-items: center; + border-top: 1px solid #667080; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +` + +const ListView = styled.div` align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +` + +const ManuscriptId = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 12px; + margin-left: 8px; + text-align: left; + text-decoration: underline; + text-transform: uppercase; +` + +const Version = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 13px; + text-align: left; +` +const Details = styled.div` + align-items: center; + color: #667080; cursor: pointer; display: flex; + font-family: Helvetica; + font-size: 14px; + margin-left: 8px; + text-decoration: underline; + text-align: center; +` + +const ClickableIcon = styled.div` + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + margin: 0 7px; + + svg { + stroke: ${({ disabled }) => (disabled ? '#eee' : '#667080')}; + } +` + +const Card = styled.div` + align-items: center; + border: 1px solid #667080; + display: flex; + flex-direction: column; + justify-content: flex-start; + margin-bottom: 10px; +` - & > span { +const Right = styled.div` + align-items: center; + flex: 1; + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 0 15px; +` + +const Left = styled.div` + border-right: 1px solid #667080; + display: flex; + flex-direction: column; + flex: 5; + margin: 10px 0; + padding: 0 10px; +` + +const ManuscriptInfo = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + + div { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } +` + +const ManuscriptType = styled.div` + border: 1px solid #667080; + color: #667080; + font-family: Helvetica; + font-size: 12px; + font-weight: bold; + padding: 6px 4px; + margin-left: 10px; + text-align: left; + text-transform: uppercase; +` + +const Title = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 18px; + text-align: left; +` + +const Status = styled.div` + border: 1px solid #667080; + color: #667080; + font-family: Helvetica; + font-size: 12px; + font-weight: bold; + text-align: left; + margin: 0.5em 0; + padding: 0.2em 0.5em; + text-transform: uppercase; +` + +const DateField = styled.span` + color: #667080; + font-family: Helvetica; + font-size: 13px; + margin: 0 8px; + text-align: left; +` + +const ClickableIconContainer = styled.div` + align-items: center; + cursor: pointer; + display: flex; + margin-right: 8px; + + span:last-child { color: #667080; font-family: Helvetica; font-size: 14px; @@ -128,5 +356,4 @@ const ViewAbstractContainer = styled.div` text-decoration: underline; } ` - -export default DashboardCard +// #endregion diff --git a/packages/components-faraday/src/components/Dashboard/DashboardFilters.js b/packages/components-faraday/src/components/Dashboard/DashboardFilters.js index 3a2ae74005be770013c39138013a188174111c5b..b7ca2b2afd595966fbbc1eb96f26ab3313ac3e73 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardFilters.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardFilters.js @@ -1,5 +1,5 @@ import React from 'react' -import { Icon, Menu } from '@pubsweet/ui' +import { Menu } from '@pubsweet/ui' import { compose, withHandlers } from 'recompose' import classes from './Dashboard.local.scss' @@ -13,7 +13,6 @@ const DashboardFilters = ({ view, status, createdAt, - changeView, listView, changeFilter, changeSort, @@ -42,12 +41,6 @@ const DashboardFilters = ({ <Menu onChange={changeSort} options={sortOptions} /> </div> </div> - <div className={classes.viewMode} onClick={changeView}> - <div className={classes.icon}> - {listView ? <Icon>list</Icon> : <Icon>credit-card</Icon>} - </div> - {listView ? ' List' : ' Card'} View - </div> </div> ) diff --git a/packages/components-faraday/src/components/Dashboard/DashboardPage.js b/packages/components-faraday/src/components/Dashboard/DashboardPage.js index 4154a7b1ba1bfcf8084436e3f45bd38d9535aa0c..f183c434e5ebc1f5bc323565b7b3b6480634c1f5 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardPage.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardPage.js @@ -1,9 +1,11 @@ import { get } from 'lodash' +import PropTypes from 'prop-types' import { connect } from 'react-redux' import { actions } from 'pubsweet-client' +import { withJournal } from 'xpub-journal' import { ConnectPage } from 'xpub-connect' import { withRouter } from 'react-router-dom' -import { compose, withState, withHandlers } from 'recompose' +import { compose, withContext } from 'recompose' import { newestFirst, selectCurrentUser } from 'xpub-selectors' import { createDraftSubmission } from 'pubsweet-component-wizard/src/redux/conversion' @@ -17,10 +19,6 @@ export default compose( actions.getTeams(), actions.getUsers(), ]), - withState('listView', 'changeView', true), - withHandlers({ - changeViewMode: ({ changeView }) => () => changeView(listView => !listView), - }), connect( state => { const { collections, conversion } = state @@ -52,6 +50,7 @@ export default compose( }), ), withRouter, + withJournal, withFilters({ status: { options: [ @@ -86,4 +85,10 @@ export default compose( }, }, }), + withContext( + { + journal: PropTypes.object, + }, + ({ journal }) => ({ journal }), + ), )(Dashboard) diff --git a/packages/components-faraday/src/components/Dashboard/utils.js b/packages/components-faraday/src/components/Dashboard/utils.js index 50d39d73b6082c05a4efe3bda58ae82d0335cccc..84a1700491ccc80f1a0cd3a4cccaa80a9b68c962 100644 --- a/packages/components-faraday/src/components/Dashboard/utils.js +++ b/packages/components-faraday/src/components/Dashboard/utils.js @@ -53,7 +53,14 @@ export const parseType = version => { export const parseSubmissionDate = version => { const submitted = get(version, 'submitted') - return submitted ? moment(submitted).format('DD-MM-YYYY') : 'N/A' + const submittedDate = moment(submitted) + const today = moment() + const daysAgo = moment.duration(today - moment(submitted)).days() + return submitted + ? `${submittedDate.format('DD.MM.YYYY')} ${ + daysAgo > 0 ? `(${daysAgo} days)` : '' + }` + : 'N/A' } export const parseVersion = version => ({ diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index fd1592d877fbab18168c974cf4fe37cba7fd327c..c78cdecc488485f1bf753ea57efbc6ce4dd2da05 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -26,7 +26,7 @@ module.exports = { 'pubsweet-client': { API_ENDPOINT: '/api', 'login-redirect': '/', - 'redux-log': true, + 'redux-log': false, theme: process.env.PUBSWEET_THEME, }, 'mail-transport': {