Skip to content
Snippets Groups Projects
Commit 64412045 authored by Alexandru Munteanu's avatar Alexandru Munteanu
Browse files

feat(component-user): move resolvers logic into use cases

parent 07f179c9
No related branches found
No related tags found
3 merge requests!222Sprint #26,!217Sprint #26,!200Component user add user
Showing
with 68 additions and 772 deletions
......@@ -2,48 +2,38 @@ const Notification = require('./notifications/notification')
const { parseUserFromAdmin } = require('./user')
const {
editUser,
activateUser,
deactivateUser,
createUserAsAdmin,
} = require('./use-cases')
const resolvers = {
Mutation: {
async addUserAsAdmin(_, { input }, ctx) {
const reqUser = await ctx.connectors.User.fetchOne(ctx.user, ctx)
if (!reqUser.admin) {
throw new Error('Unauthorized')
}
try {
const user = await ctx.connectors.User.create(
parseUserFromAdmin(input),
return createUserAsAdmin
.initialize({
ctx,
)
const notification = new Notification(user)
await notification.notifyUserAddedByAdmin(input.role)
return user
} catch (e) {
return e
}
Notification,
User: ctx.connectors.User,
})
.execute({ input: parseUserFromAdmin(input) })
},
async editUserAsAdmin(_, { id, input }, ctx) {
try {
const user = await ctx.connectors.User.update(
id,
parseUserFromAdmin(input),
ctx,
)
return user
} catch (e) {
return e
}
return editUser
.initialize({ User: ctx.connectors.User, ctx })
.execute({ id, input })
},
async activateUserAsAdmin(_, { id, input }, ctx) {
try {
const user = await ctx.connectors.User.update(id, input, ctx)
return user
} catch (e) {
return e
async toggleUserActiveStatusAsAdmin(_, { id, input }, ctx) {
if (input.isActive) {
return activateUser
.initialize({ User: ctx.connectors.User, ctx })
.execute({ id, input })
}
return deactivateUser
.initialize({ User: ctx.connectors.User, ctx })
.execute({ id, input })
},
},
}
......
......@@ -20,7 +20,7 @@ module.exports = `
role: AllowedRole
}
input ActivateUserInput {
input UserStatusInput {
email: String!
username: String!
isActive: Boolean
......@@ -29,7 +29,7 @@ module.exports = `
extend type Mutation {
addUserAsAdmin(input: UserInput!): User
editUserAsAdmin(id: ID!, input: UserInput!): User
activateUserAsAdmin(id: ID!, input: ActivateUserInput): User
toggleUserActiveStatusAsAdmin(id: ID!, input: UserStatusInput): User
}
enum AllowedRole {
......
module.exports.initialize = ({ User, ctx }) => ({
execute: async ({ id, input }) => {
const username = input.username.replace('invalid***', '')
return User.update(id, { ...input, username }, ctx)
},
})
module.exports.initialize = ({ User, Notification, ctx }) => ({
execute: async ({ input }) => {
const { admin } = await User.fetchOne(ctx.user, ctx)
if (!admin) {
throw new Error('Unauthorized')
}
const user = await User.create(input, ctx)
const notification = new Notification(user)
await notification.notifyUserAddedByAdmin(input.role)
return user
},
})
module.exports.initialize = ({ User, ctx }) => ({
execute: async ({ id, input }) => {
const username = `invalid***${input.username}`
return User.update(id, { ...input, username }, ctx)
},
})
module.exports.initialize = ({ User, ctx }) => ({
execute: ({ id, input }) => User.update(id, input, ctx),
})
const activateUser = require('./activateUser')
const deactivateUser = require('./deactivateUser')
const editUser = require('./editUser')
const createUserAsAdmin = require('./createUserAsAdmin')
module.exports = {
editUser,
activateUser,
deactivateUser,
createUserAsAdmin,
}
......@@ -17,6 +17,7 @@ module.exports = {
return {
...omit(input, ['role']),
...roles,
isActive: true,
isConfirmed: false,
notifications: {
email: {
......
import React, { Fragment } from 'react'
import { H1 } from '@pubsweet/ui'
import { Row, IconCard } from 'pubsweet-component-faraday-ui'
const AdminDashboard = ({ history }) => (
<Fragment>
<H1 mt={2}>Admin dashboard</H1>
<Row justify="flex-start" mt={2}>
<IconCard
icon="users"
iconSize={6}
label="Users"
onClick={() => history.push('/admin/users')}
/>
</Row>
</Fragment>
)
export default AdminDashboard
import React from 'react'
import { get } from 'lodash'
import { compose } from 'recompose'
import { connect } from 'react-redux'
import { Redirect, withRouter, Route } from 'react-router-dom'
import { AuthenticatedComponent } from 'pubsweet-client'
const AdminRoute = ({
currentUser,
redirectPath = '/',
component: Component,
...rest
}) => {
const isAdmin = get(currentUser, 'user.admin')
return (
<Route
{...rest}
render={props => (
<AuthenticatedComponent>
{isAdmin ? <Component {...props} /> : <Redirect to="/" />}
</AuthenticatedComponent>
)}
/>
)
}
export default compose(
withRouter,
connect(state => ({
currentUser: state.currentUser,
})),
)(AdminRoute)
import React, { Fragment } from 'react'
import { get } from 'lodash'
import { Formik } from 'formik'
import styled from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
import { required } from 'xpub-validators'
import {
H2,
Button,
Spinner,
TextField,
ValidatedFieldFormik,
} from '@pubsweet/ui'
import { compose, setDisplayName, withHandlers, withProps } from 'recompose'
import {
Row,
Item,
Text,
Label,
IconButton,
RowOverrideAlert,
ItemOverrideAlert,
ValidatedMenuField,
withRoles,
withFetching,
withCountries,
} from 'pubsweet-component-faraday-ui'
const FormModal = ({
roles,
title,
titles,
onClose,
onSubmit,
onConfirm,
countries,
isFetching,
fetchingError,
initialValues,
confirmText = 'OK',
cancelText = 'Cancel',
}) => (
<Root>
<IconButton icon="x" onClick={onClose} right={5} secondary top={5} />
<H2>{title}</H2>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validate={values => {
const errors = {}
if (get(values, 'email', '') === '') {
errors.email = 'Required'
}
if (get(values, 'affiliation', '') === '') {
errors.affiliation = 'Required'
}
return errors
}}
>
{({ handleSubmit, ...rest }) => (
<Fragment>
<Row alignItems="baseline" mb={1} mt={1}>
<ItemOverrideAlert mr={1} vertical>
<Label required>Email</Label>
<ValidatedFieldFormik
component={TextField}
inline
name="email"
validate={[required]}
/>
</ItemOverrideAlert>
<ItemOverrideAlert ml={1} vertical>
<Label required>Role</Label>
<ValidatedMenuField name="role" options={roles} />
</ItemOverrideAlert>
</Row>
<Row mb={2}>
<Item mr={1} vertical>
<Label>First Name</Label>
<ValidatedFieldFormik
component={TextField}
inline
name="firstName"
/>
</Item>
<Item ml={1} vertical>
<Label>Last Name</Label>
<ValidatedFieldFormik
component={TextField}
inline
name="lastName"
/>
</Item>
</Row>
<RowOverrideAlert alignItems="center" mb={2}>
<ItemOverrideAlert mr={1} vertical>
<Label>Title</Label>
<ValidatedMenuField name="title" options={titles} />
</ItemOverrideAlert>
<ItemOverrideAlert ml={1} vertical>
<Label>Country</Label>
<ValidatedMenuField name="country" options={countries} />
</ItemOverrideAlert>
</RowOverrideAlert>
<Row mb={3}>
<Item vertical>
<Label required>Affiliation</Label>
<ValidatedFieldFormik
component={TextField}
inline
name="affiliation"
/>
</Item>
</Row>
{fetchingError && (
<Row mb={1}>
<Text error>{fetchingError}</Text>
</Row>
)}
{isFetching ? (
<Spinner />
) : (
<Row>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={handleSubmit} primary>
{confirmText}
</Button>
</Row>
)}
</Fragment>
)}
</Formik>
</Root>
)
// #region FormHelpers
const setInitialRole = a => {
if (get(a, 'admin')) {
return 'admin'
}
if (get(a, 'handlingEditor')) {
return 'handlingEditor'
}
if (get(a, 'editorInChief')) {
return 'editorInChief'
}
return 'author'
}
// #endregion
export default compose(
withRoles,
withFetching,
withCountries,
withProps(({ user, edit }) => ({
initialValues: {
...user,
role: setInitialRole(user),
},
confirmText: edit ? 'EDIT USER' : 'SAVE USER',
title: edit ? 'Edit User' : 'Add User',
})),
withHandlers({
onSubmit: ({ onSubmit, ...props }) => (values, formProps) => {
if (typeof onSubmit === 'function') {
onSubmit(values, { formProps, props })
}
},
onClose: ({ onCancel, ...props }) => () => {
if (typeof onCancel === 'function') {
onCancel(props)
}
props.hideModal()
},
}),
setDisplayName('FormModal'),
)(FormModal)
// #region styles
const Root = styled.div`
align-items: center;
background: ${th('colorBackgroundHue')};
border: ${th('borderWidth')} ${th('borderStyle')} transparent;
border-radius: ${th('borderRadius')};
box-shadow: ${th('boxShadow')};
display: flex;
flex-direction: column;
position: relative;
padding: calc(${th('gridUnit')} * 3);
width: calc(${th('gridUnit')} * 60);
${H2} {
margin: 0;
text-align: center;
}
`
// #endregion
import React, { Fragment } from 'react'
import { get } from 'lodash'
import { Button } from '@pubsweet/ui'
import styled from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
import { withJournal } from 'xpub-journal'
import { compose, withHandlers, withProps } from 'recompose'
import {
Row,
Text,
Item,
Label,
OpenModal,
Pagination,
ActionLink,
handleError,
withPagination,
} from 'pubsweet-component-faraday-ui'
import { updateUserStatus } from './utils'
import { OpenUserForm, withUsersGQL } from './'
const Users = ({
page,
users,
theme,
history,
journal,
getUsers,
isFetching,
getUserName,
getUserRoles,
itemsPerPage,
deactivateUser,
getStatusLabel,
paginatedItems,
toggleUserStatus,
//
addUser,
updateUser,
toggleUserActiveStatus,
...rest
}) => (
<Fragment>
<Row alignItems="center" justify="space-between" mb={1}>
<Item alignItems="center">
<ActionLink
data-test-id="go-to-dashboard"
icon="arrow-left"
mr={2}
onClick={history.goBack}
>
Admin Dashboard
</ActionLink>
<OpenUserForm modalKey="addUser" onSubmit={addUser} />
</Item>
<Pagination {...rest} itemsPerPage={itemsPerPage} page={page} />
</Row>
<Table>
<thead>
<tr>
<th>
<Label>Full Name</Label>
</th>
<th colSpan={2}>
<Label>Email</Label>
</th>
<th>
<Label>Affiliation</Label>
</th>
<th>
<Label>Roles</Label>
</th>
<th>
<Label>Status</Label>
</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{paginatedItems.map(user => (
<UserRow key={user.id}>
<td>
<Text pl={1}>{getUserName(user)}</Text>
</td>
<td colSpan={2}>
<Text>{user.email}</Text>
</td>
<td>
<Text>{user.affiliation}</Text>
</td>
<td>
<Text customId>{getUserRoles(user)}</Text>
</td>
<td>
<Text secondary>{getStatusLabel(user)}</Text>
</td>
<HiddenCell>
<Item alignItems="center" justify="flex-end">
<OpenUserForm
edit
modalKey={`edit-${user.id}`}
onSubmit={updateUser}
user={user}
/>
<OpenModal
isFetching={isFetching}
modalKey={`deactivate-${user.id}`}
onConfirm={toggleUserActiveStatus(user)}
subtitle={`${user.firstName} ${user.lastName}`}
title={`Are you sure to ${
!user.isActive ? 'activate' : 'deactivate'
} user?`}
>
{showModal => (
<ActivateButton
ml={1}
mr={1}
onClick={showModal}
size="small"
>
{!user.isActive ? 'ACTIVATE' : 'DEACTIVATE'}
</ActivateButton>
)}
</OpenModal>
</Item>
</HiddenCell>
</UserRow>
))}
</tbody>
</Table>
</Fragment>
)
export default compose(
withJournal,
withUsersGQL,
withProps(({ journal: { roles = {} }, users }) => ({
roles: Object.keys(roles),
items: users,
})),
withPagination,
withHandlers({
getStatusLabel: () => ({ admin, isConfirmed, isActive = true }) => {
if (admin) return 'ACTIVE'
if (!isActive) {
return 'INACTIVE'
}
return isConfirmed ? 'ACTIVE' : 'INVITED'
},
deactivateUser: ({ setFetching, getUsers }) => user => ({
hideModal,
setModalError,
}) => {
setFetching(true)
updateUserStatus(user)
.then(() => {
setFetching(false)
getUsers()
hideModal()
})
.catch(err => {
setFetching(false)
handleError(setModalError)(err)
})
},
getUserName: () => user => {
if (user.admin) {
return 'Admin'
}
return `${get(user, 'firstName', '')} ${get(user, 'lastName', '')}`
},
getUserRoles: ({ journal: { roles = {} } }) => user => {
const parsedRoles = Object.entries(roles)
.reduce((acc, role) => (user[role[0]] ? [...acc, role[1]] : acc), [])
.join(', ')
return parsedRoles || 'Author'
},
}),
)(Users)
// #region styled-components
const Table = styled.table`
border-collapse: collapse;
& th,
& td {
border: none;
text-align: start;
vertical-align: middle;
height: calc(${th('gridUnit')} * 5);
}
`
const HiddenCell = styled.td`
opacity: 0;
`
const UserRow = styled.tr`
background-color: ${th('colorBackgroundHue2')};
border-bottom: 1px solid ${th('colorBorder')};
&:hover {
background-color: ${th('colorBackgroundHue3')};
${HiddenCell} {
opacity: 1;
}
}
`
const ActivateButton = styled(Button)`
width: 90px;
`
// #endregion
import React from 'react'
import {
OpenModal,
ActionLink,
IconButton,
} from 'pubsweet-component-faraday-ui'
import AdminUserForm from './AdminUserForm'
const OpenUserForm = ({ edit, user, onSubmit, modalKey }) => (
<OpenModal
component={AdminUserForm}
edit={edit}
modalKey={modalKey}
onSubmit={onSubmit}
user={user}
>
{showModal =>
edit ? (
<IconButton icon="edit-2" iconSize={2} onClick={showModal} pt={1 / 2} />
) : (
<ActionLink icon="plus" onClick={showModal}>
ADD USER
</ActionLink>
)
}
</OpenModal>
)
export default OpenUserForm
export { default as withUsersGQL } from './withUsersGQL'
export { default as AdminUsers } from './AdminUsers'
export { default as AdminRoute } from './AdminRoute'
export { default as OpenUserForm } from './OpenUserForm'
export { default as AdminDashboard } from './AdminDashboard'
import { pick, omit, isBoolean, replace } from 'lodash'
import { update } from 'pubsweet-client/src/helpers/api'
const generatePasswordHash = () =>
Array.from({ length: 4 }, () =>
Math.random()
.toString(36)
.slice(4),
).join('')
export const setAdmin = values => {
const newValues = { ...values, isActive: true }
if (newValues.roles && newValues.roles.includes('admin')) {
newValues.admin = true
} else {
newValues.admin = false
}
return {
...omit(newValues, ['role']),
username: newValues.email,
isConfirmed: false,
password: 'defaultpass',
editorInChief: newValues.role === 'editorInChief',
handlingEditor: newValues.role === 'handlingEditor',
notifications: {
email: {
system: true,
user: true,
},
},
accessTokens: {
passwordReset: generatePasswordHash(),
unsubscribe: generatePasswordHash(),
},
}
}
export const parseUpdateUser = values => {
const parsedValues = {
...values,
editorInChief: values.editorInChief || false,
handlingEditor: values.handlingEditor || false,
admin: values.admin || false,
}
const valuesToSave = [
'admin',
'firstName',
'lastName',
'affiliation',
'title',
'roles',
'editorInChief',
'handlingEditor',
'isActive',
'username',
'country',
'accessTokens',
]
return pick(parsedValues, valuesToSave)
}
const toggleUserStatus = user => {
const { isActive, username } = user
let newState = true
let newUsername = ''
if (!isBoolean(isActive) || isActive) {
newState = false
newUsername = `invalid***${username}`
} else {
newUsername = replace(username, 'invalid***', '')
}
return {
...user,
isActive: newState,
username: newUsername,
}
}
export const updateUserStatus = user => {
const updatedUser = toggleUserStatus(user)
return update(`/users/${user.id}`, parseUpdateUser(updatedUser))
}
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'
import { compose, withHandlers, withProps } from 'recompose'
const userFragment = gql`
fragment userDetails on User {
id
admin
email
title
country
username
lastName
isActive
firstName
affiliation
editorInChief
handlingEditor
}
`
const getUsersQuery = gql`
{
users {
...userDetails
}
}
${userFragment}
`
const addUserAsAdmin = gql`
mutation addUserAsAdmin($id: ID!, $input: UserInput!) {
addUserAsAdmin(id: $id, input: $input) {
...userDetails
}
}
${userFragment}
`
const editUserAsAdmin = gql`
mutation editUserAsAdmin($id: ID!, $input: UserInput!) {
editUserAsAdmin(id: $id, input: $input) {
...userDetails
}
}
${userFragment}
`
const activateUserAsAdmin = gql`
mutation activateUserAsAdmin($id: ID!, $input: ActivateUserInput) {
activateUserAsAdmin(id: $id, input: $input) {
...userDetails
}
}
${userFragment}
`
export default compose(
graphql(getUsersQuery),
graphql(addUserAsAdmin, {
name: 'addUser',
options: {
refetchQueries: [{ query: getUsersQuery }],
},
}),
graphql(editUserAsAdmin, {
name: 'updateUser',
}),
graphql(activateUserAsAdmin, {
name: 'activateUser',
}),
withHandlers({
addUser: ({ addUser }) => (
{ __typename, id, admin, ...input },
{ props: { hideModal, setFetching } },
) => {
setFetching(true)
addUser({
variables: {
id,
input: {
...input,
username: input.email,
},
},
}).then(() => {
setFetching(false)
hideModal()
})
},
updateUser: ({ updateUser }) => (
{ __typename, id, admin, handlingEditor, editorInChief, ...input },
{ props: { hideModal, setFetching, setError } },
) => {
setFetching(true)
updateUser({
variables: {
id,
input,
},
})
.then(() => {
setFetching(false)
hideModal()
})
.catch(e => {
setFetching(false)
setError(e.message)
})
},
toggleUserActiveStatus: ({ activateUser }) => ({
__typename,
admin,
handlingEditor,
editorInChief,
id,
email,
username,
isActive,
}) => ({ hideModal }) => {
activateUser({
variables: {
id,
input: {
email,
username,
isActive,
},
},
}).then(hideModal)
},
}),
withProps(({ data }) => ({
users: data.users,
})),
)
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment