From 07f179c958804b50ff0d5635a821540e0f2274d8 Mon Sep 17 00:00:00 2001
From: malexsan <alexandru.munt@gmail.com>
Date: Thu, 13 Dec 2018 13:25:36 +0200
Subject: [PATCH] feat(adminUsers): move components and client graphql into
 component-user module

---
 packages/component-user/app/AdminRoute.js     |  32 +++
 .../app/components/AdminUserForm.js           | 204 ++++++++++++++
 .../app/components/OpenStatusModal.js         |  46 +++
 .../app/components/OpenUserForm.js            |  31 ++
 .../component-user/app/components/index.js    |   3 +
 .../component-user/app/graphql/fragments.js   |  18 ++
 packages/component-user/app/graphql/index.js  |   6 +
 .../component-user/app/graphql/mutations.js   |  30 ++
 .../component-user/app/graphql/queries.js     |  12 +
 .../app/graphql/withUsersGQL.js               |  24 ++
 packages/component-user/app/index.js          |   3 +
 .../app/pages/AdminDashboard.js               |  19 ++
 .../component-user/app/pages/AdminUsers.js    | 264 ++++++++++++++++++
 packages/component-user/index.js              |   7 +-
 packages/component-user/package.json          |  20 +-
 .../{ => server}/notifications/emailCopy.js   |   0
 .../notifications/notification.js             |   0
 .../component-user/{ => server}/resolvers.js  |   0
 .../component-user/{ => server}/typeDefs.js   |   2 +-
 packages/component-user/{ => server}/user.js  |   0
 packages/xpub-faraday/app/routes.js           |  11 +-
 yarn.lock                                     |  66 +++++
 22 files changed, 785 insertions(+), 13 deletions(-)
 create mode 100644 packages/component-user/app/AdminRoute.js
 create mode 100644 packages/component-user/app/components/AdminUserForm.js
 create mode 100644 packages/component-user/app/components/OpenStatusModal.js
 create mode 100644 packages/component-user/app/components/OpenUserForm.js
 create mode 100644 packages/component-user/app/components/index.js
 create mode 100644 packages/component-user/app/graphql/fragments.js
 create mode 100644 packages/component-user/app/graphql/index.js
 create mode 100644 packages/component-user/app/graphql/mutations.js
 create mode 100644 packages/component-user/app/graphql/queries.js
 create mode 100644 packages/component-user/app/graphql/withUsersGQL.js
 create mode 100644 packages/component-user/app/index.js
 create mode 100644 packages/component-user/app/pages/AdminDashboard.js
 create mode 100644 packages/component-user/app/pages/AdminUsers.js
 rename packages/component-user/{ => server}/notifications/emailCopy.js (100%)
 rename packages/component-user/{ => server}/notifications/notification.js (100%)
 rename packages/component-user/{ => server}/resolvers.js (100%)
 rename packages/component-user/{ => server}/typeDefs.js (93%)
 rename packages/component-user/{ => server}/user.js (100%)

diff --git a/packages/component-user/app/AdminRoute.js b/packages/component-user/app/AdminRoute.js
new file mode 100644
index 000000000..b243ce527
--- /dev/null
+++ b/packages/component-user/app/AdminRoute.js
@@ -0,0 +1,32 @@
+import React from 'react'
+import { get } from 'lodash'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { AuthenticatedComponent } from 'pubsweet-client'
+import { Redirect, withRouter, Route } from 'react-router-dom'
+
+const AdminRoute = ({
+  currentUser,
+  redirectPath = '/',
+  component: Component,
+  ...rest
+}) => {
+  const isAdmin = get(currentUser, 'user.admin', false)
+  return (
+    <Route
+      {...rest}
+      render={props => (
+        <AuthenticatedComponent>
+          {isAdmin ? <Component {...props} /> : <Redirect to="/" />}
+        </AuthenticatedComponent>
+      )}
+    />
+  )
+}
+
+export default compose(
+  withRouter,
+  connect(state => ({
+    currentUser: state.currentUser,
+  })),
+)(AdminRoute)
diff --git a/packages/component-user/app/components/AdminUserForm.js b/packages/component-user/app/components/AdminUserForm.js
new file mode 100644
index 000000000..6b29d7dc0
--- /dev/null
+++ b/packages/component-user/app/components/AdminUserForm.js
@@ -0,0 +1,204 @@
+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 { compose, setDisplayName, withHandlers, withProps } from 'recompose'
+import {
+  H2,
+  Button,
+  Spinner,
+  TextField,
+  ValidatedFieldFormik,
+} from '@pubsweet/ui'
+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>
+)
+
+const setInitialRole = a => {
+  if (get(a, 'admin', false)) {
+    return 'admin'
+  }
+  if (get(a, 'handlingEditor', false)) {
+    return 'handlingEditor'
+  }
+  if (get(a, 'editorInChief', false)) {
+    return 'editorInChief'
+  }
+  return 'author'
+}
+
+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('AdminUserForm'),
+)(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
diff --git a/packages/component-user/app/components/OpenStatusModal.js b/packages/component-user/app/components/OpenStatusModal.js
new file mode 100644
index 000000000..f97ea6a7e
--- /dev/null
+++ b/packages/component-user/app/components/OpenStatusModal.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import { Button } from '@pubsweet/ui'
+import styled from 'styled-components'
+import { th } from '@pubsweet/ui-toolkit'
+import { withHandlers, compose, setDisplayName } from 'recompose'
+import { OpenModal, withFetching } from 'pubsweet-component-faraday-ui'
+
+const OpenStatusModal = ({
+  onConfirm,
+  isFetching,
+  user: { id, firstName = '', lastName = '', isActive },
+}) => (
+  <OpenModal
+    isFetching={isFetching}
+    modalKey={`deactivate-${id}`}
+    onConfirm={onConfirm}
+    subtitle={`${firstName} ${lastName}`}
+    title={`Are you sure you want to ${
+      !isActive ? 'activate' : 'deactivate'
+    } user?`}
+  >
+    {showModal => (
+      <ActivateButton ml={1} mr={1} onClick={showModal} size="small">
+        {!isActive ? 'ACTIVATE' : 'DEACTIVATE'}
+      </ActivateButton>
+    )}
+  </OpenModal>
+)
+
+export default compose(
+  withFetching,
+  withHandlers({
+    onConfirm: ({ user, onConfirm, ...props }) => modalProps => {
+      if (typeof onConfirm === 'function') {
+        onConfirm(user, { ...props, ...modalProps })
+      }
+    },
+  }),
+  setDisplayName('AdminStatusModal'),
+)(OpenStatusModal)
+
+// #region styles
+const ActivateButton = styled(Button)`
+  width: calc(${th('gridUnit')} * 10);
+`
+// #endregion
diff --git a/packages/component-user/app/components/OpenUserForm.js b/packages/component-user/app/components/OpenUserForm.js
new file mode 100644
index 000000000..ae406374e
--- /dev/null
+++ b/packages/component-user/app/components/OpenUserForm.js
@@ -0,0 +1,31 @@
+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
diff --git a/packages/component-user/app/components/index.js b/packages/component-user/app/components/index.js
new file mode 100644
index 000000000..900efc4da
--- /dev/null
+++ b/packages/component-user/app/components/index.js
@@ -0,0 +1,3 @@
+export { default as AdminUserForm } from './AdminUserForm'
+export { default as OpenStatusModal } from './OpenStatusModal'
+export { default as OpenUserForm } from './OpenUserForm'
diff --git a/packages/component-user/app/graphql/fragments.js b/packages/component-user/app/graphql/fragments.js
new file mode 100644
index 000000000..362829827
--- /dev/null
+++ b/packages/component-user/app/graphql/fragments.js
@@ -0,0 +1,18 @@
+import gql from 'graphql-tag'
+
+export const userFragment = gql`
+  fragment userDetails on User {
+    id
+    admin
+    email
+    title
+    country
+    username
+    lastName
+    isActive
+    firstName
+    affiliation
+    editorInChief
+    handlingEditor
+  }
+`
diff --git a/packages/component-user/app/graphql/index.js b/packages/component-user/app/graphql/index.js
new file mode 100644
index 000000000..cf105b664
--- /dev/null
+++ b/packages/component-user/app/graphql/index.js
@@ -0,0 +1,6 @@
+import * as fragments from './fragments'
+import * as queries from './queries'
+import * as mutations from './mutations'
+
+export { fragments, queries, mutations }
+export { default } from './withUsersGQL'
diff --git a/packages/component-user/app/graphql/mutations.js b/packages/component-user/app/graphql/mutations.js
new file mode 100644
index 000000000..fed993f4c
--- /dev/null
+++ b/packages/component-user/app/graphql/mutations.js
@@ -0,0 +1,30 @@
+import gql from 'graphql-tag'
+
+import { userFragment } from './fragments'
+
+export const addUserAsAdmin = gql`
+  mutation addUserAsAdmin($input: UserInput!) {
+    addUserAsAdmin(input: $input) {
+      ...userDetails
+    }
+  }
+  ${userFragment}
+`
+
+export const editUserAsAdmin = gql`
+  mutation editUserAsAdmin($id: ID!, $input: UserInput!) {
+    editUserAsAdmin(id: $id, input: $input) {
+      ...userDetails
+    }
+  }
+  ${userFragment}
+`
+
+export const activateUserAsAdmin = gql`
+  mutation activateUserAsAdmin($id: ID!, $input: ActivateUserInput) {
+    activateUserAsAdmin(id: $id, input: $input) {
+      ...userDetails
+    }
+  }
+  ${userFragment}
+`
diff --git a/packages/component-user/app/graphql/queries.js b/packages/component-user/app/graphql/queries.js
new file mode 100644
index 000000000..9e65b0e68
--- /dev/null
+++ b/packages/component-user/app/graphql/queries.js
@@ -0,0 +1,12 @@
+import gql from 'graphql-tag'
+
+import { userFragment } from './fragments'
+
+export const getUsers = gql`
+  {
+    users {
+      ...userDetails
+    }
+  }
+  ${userFragment}
+`
diff --git a/packages/component-user/app/graphql/withUsersGQL.js b/packages/component-user/app/graphql/withUsersGQL.js
new file mode 100644
index 000000000..aceee93b7
--- /dev/null
+++ b/packages/component-user/app/graphql/withUsersGQL.js
@@ -0,0 +1,24 @@
+import { graphql } from 'react-apollo'
+import { compose, withProps } from 'recompose'
+
+import * as queries from './queries'
+import * as mutations from './mutations'
+
+export default compose(
+  graphql(queries.getUsers),
+  graphql(mutations.addUserAsAdmin, {
+    name: 'addUser',
+    options: {
+      refetchQueries: [{ query: queries.getUsers }],
+    },
+  }),
+  graphql(mutations.editUserAsAdmin, {
+    name: 'updateUser',
+  }),
+  graphql(mutations.activateUserAsAdmin, {
+    name: 'activateUser',
+  }),
+  withProps(({ data }) => ({
+    users: data.users,
+  })),
+)
diff --git a/packages/component-user/app/index.js b/packages/component-user/app/index.js
new file mode 100644
index 000000000..5cf2fee32
--- /dev/null
+++ b/packages/component-user/app/index.js
@@ -0,0 +1,3 @@
+export { default as AdminRoute } from './AdminRoute'
+export { default as AdminUsers } from './pages/AdminUsers'
+export { default as AdminDashboard } from './pages/AdminDashboard'
diff --git a/packages/component-user/app/pages/AdminDashboard.js b/packages/component-user/app/pages/AdminDashboard.js
new file mode 100644
index 000000000..f07b7c426
--- /dev/null
+++ b/packages/component-user/app/pages/AdminDashboard.js
@@ -0,0 +1,19 @@
+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
diff --git a/packages/component-user/app/pages/AdminUsers.js b/packages/component-user/app/pages/AdminUsers.js
new file mode 100644
index 000000000..568ffd9c1
--- /dev/null
+++ b/packages/component-user/app/pages/AdminUsers.js
@@ -0,0 +1,264 @@
+import React, { Fragment } from 'react'
+import { get } from 'lodash'
+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,
+  Pagination,
+  ActionLink,
+  withPagination,
+} from 'pubsweet-component-faraday-ui'
+
+import withUsersGQL from '../graphql'
+import { OpenUserForm, OpenStatusModal } from '../components'
+
+const Users = ({
+  page,
+  users,
+  theme,
+  history,
+  journal,
+  getUsers,
+  isFetching,
+  getUserName,
+  getUserRoles,
+  itemsPerPage,
+  deactivateUser,
+  getStatusLabel,
+  paginatedItems,
+  //
+  addUser,
+  updateUser,
+  toggleUserStatus,
+  ...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}
+                />
+
+                <OpenStatusModal onConfirm={toggleUserStatus} user={user} />
+              </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'
+    },
+    addUser: ({ addUser }) => (
+      {
+        __typename,
+        id,
+        admin,
+        isActive,
+        editorInChief,
+        handlingEditor,
+        ...input
+      },
+      { setFetching, hideModal, setError },
+    ) => {
+      setFetching(true)
+      addUser({
+        variables: {
+          input: {
+            ...input,
+            username: input.email,
+          },
+        },
+      })
+        .then(() => {
+          setFetching(false)
+          hideModal()
+        })
+        .catch(e => {
+          setFetching(false)
+          setError(e.message)
+        })
+    },
+    updateUser: ({ updateUser }) => (
+      {
+        __typename,
+        id,
+        admin,
+        isActive,
+        handlingEditor,
+        editorInChief,
+        ...input
+      },
+      { setFetching, hideModal, setError },
+    ) => {
+      setFetching(true)
+      updateUser({
+        variables: {
+          id,
+          input,
+        },
+      })
+        .then(() => {
+          setFetching(false)
+          hideModal()
+        })
+        .catch(e => {
+          setFetching(false)
+          setError(e.message)
+        })
+    },
+    toggleUserStatus: ({ activateUser }) => (
+      { id, email, username, isActive },
+      { setFetching, hideModal, setModalError },
+    ) => {
+      setFetching(true)
+      activateUser({
+        variables: {
+          id,
+          input: {
+            email,
+            username,
+            isActive,
+          },
+        },
+      })
+        .then(() => {
+          setFetching(false)
+          hideModal()
+        })
+        .catch(e => {
+          setFetching(false)
+          setModalError(e.message)
+        })
+    },
+    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;
+    }
+  }
+`
+// #endregion
+
+// withHandlers({
+
+// }),
diff --git a/packages/component-user/index.js b/packages/component-user/index.js
index d48a22145..c9b9c509b 100644
--- a/packages/component-user/index.js
+++ b/packages/component-user/index.js
@@ -1,8 +1,7 @@
-const resolvers = require('./resolvers')
-const typeDefs = require('./typeDefs')
+const resolvers = require('./server/resolvers')
+const typeDefs = require('./server/typeDefs')
 
 module.exports = {
-  resolvers,
   typeDefs,
-  // modelName: 'User',
+  resolvers,
 }
diff --git a/packages/component-user/package.json b/packages/component-user/package.json
index 86aff3023..169e9f921 100644
--- a/packages/component-user/package.json
+++ b/packages/component-user/package.json
@@ -1,7 +1,7 @@
 {
   "name": "pubsweet-component-user",
   "version": "0.0.1",
-  "description": "user component for faraday",
+  "description": "User component for Hindawi peer review app.",
   "license": "MIT",
   "author": "Collaborative Knowledge Foundation",
   "files": [
@@ -19,12 +19,26 @@
     "path": "component-user"
   },
   "dependencies": {
-    "chance": "^1.0.13"
+    "@pubsweet/ui": "^9.0.3",
+    "@pubsweet/ui-toolkit": "^2.0.3",
+    "chance": "^1.0.13",
+    "formik": "^1.4.0",
+    "graphql-tag": "^2.10.0",
+    "lodash": "^4.17.11",
+    "pubsweet-client": "^7.0.0",
+    "react": "^16.6.0",
+    "react-apollo": "^2.3.2",
+    "react-dom": "^16.6.0",
+    "react-redux": "^5.0.2",
+    "react-router-dom": "^4.2.2",
+    "recompose": "^0.30.0",
+    "styled-components": "^4.1.2",
+    "xpub-validators": "^0.0.6"
   },
   "peerDependencies": {
+    "@pubsweet/component-send-email": "0.2.4",
     "@pubsweet/logger": "^0.0.1",
     "pubsweet-component-helper-service": "0.0.1",
-    "@pubsweet/component-send-email": "0.2.4",
     "pubsweet-server": "^10.0.0"
   },
   "devDependencies": {
diff --git a/packages/component-user/notifications/emailCopy.js b/packages/component-user/server/notifications/emailCopy.js
similarity index 100%
rename from packages/component-user/notifications/emailCopy.js
rename to packages/component-user/server/notifications/emailCopy.js
diff --git a/packages/component-user/notifications/notification.js b/packages/component-user/server/notifications/notification.js
similarity index 100%
rename from packages/component-user/notifications/notification.js
rename to packages/component-user/server/notifications/notification.js
diff --git a/packages/component-user/resolvers.js b/packages/component-user/server/resolvers.js
similarity index 100%
rename from packages/component-user/resolvers.js
rename to packages/component-user/server/resolvers.js
diff --git a/packages/component-user/typeDefs.js b/packages/component-user/server/typeDefs.js
similarity index 93%
rename from packages/component-user/typeDefs.js
rename to packages/component-user/server/typeDefs.js
index 805d71773..b10f93aa6 100644
--- a/packages/component-user/typeDefs.js
+++ b/packages/component-user/server/typeDefs.js
@@ -27,7 +27,7 @@ module.exports = `
   }
 
   extend type Mutation {
-    addUserAsAdmin(id:ID!, input: UserInput!): User
+    addUserAsAdmin(input: UserInput!): User
     editUserAsAdmin(id: ID!, input: UserInput!): User
     activateUserAsAdmin(id: ID!, input: ActivateUserInput): User
   }
diff --git a/packages/component-user/user.js b/packages/component-user/server/user.js
similarity index 100%
rename from packages/component-user/user.js
rename to packages/component-user/server/user.js
diff --git a/packages/xpub-faraday/app/routes.js b/packages/xpub-faraday/app/routes.js
index 95a60cded..cbc1a4804 100644
--- a/packages/xpub-faraday/app/routes.js
+++ b/packages/xpub-faraday/app/routes.js
@@ -11,6 +11,12 @@ import {
   UserProfilePage,
   ChangePasswordPage,
 } from 'pubsweet-components-faraday/src/components'
+
+import {
+  AdminRoute,
+  AdminUsers,
+  AdminDashboard,
+} from 'pubsweet-component-user/app'
 import { ManuscriptPage } from 'pubsweet-component-manuscript/src/components'
 import DashboardPage from 'pubsweet-components-faraday/src/components/Dashboard'
 import LoginPage from 'pubsweet-components-faraday/src/components/Login/LoginPage'
@@ -22,11 +28,6 @@ import {
   EQSDecisionPage,
   EQADecisionPage,
 } from 'pubsweet-components-faraday/src/components/UIComponents/'
-import {
-  AdminUsers,
-  AdminRoute,
-  AdminDashboard,
-} from 'pubsweet-components-faraday/src/components/Admin'
 import {
   ConfirmAccount,
   ReviewerSignUp,
diff --git a/yarn.lock b/yarn.lock
index 0800a7fa6..5782fb361 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -278,6 +278,15 @@
     lodash "^4.17.4"
     styled-components "^4.1.1"
 
+"@pubsweet/ui-toolkit@^2.0.3":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@pubsweet/ui-toolkit/-/ui-toolkit-2.0.3.tgz#673be0f68bfe3fb8c0a43401a3a032a6cfde75f7"
+  integrity sha512-zINDKK+A6kXllcYr8+hBdBij8pxF3uOWFGmQjctLBVHzgE6DTV0mPj2iv2WKezO1nE1EbJD5EyXqF4/72gQqQg==
+  dependencies:
+    color "^3.0.0"
+    lodash "^4.17.4"
+    styled-components "^4.1.1"
+
 "@pubsweet/ui-toolkit@latest":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@pubsweet/ui-toolkit/-/ui-toolkit-1.0.0.tgz#df05b54e7bbfabcb10c7afc2991752e1087d2298"
@@ -337,6 +346,31 @@
     redux-form "^7.0.3"
     styled-components "^4.1.1"
 
+"@pubsweet/ui@^9.0.3":
+  version "9.0.3"
+  resolved "https://registry.yarnpkg.com/@pubsweet/ui/-/ui-9.0.3.tgz#aac4746d6febecd88935e9b2ab1c9d0581baab29"
+  integrity sha512-8mfvxzNwTZIM0lgzHniJEn/fiyW7W5LyJh2pAYpc+Su8AuCDAKOCpQjeJlgtYzpJuZMoYJrRxfTbuLIz3c7q1g==
+  dependencies:
+    "@pubsweet/ui-toolkit" "^2.0.3"
+    babel-jest "^21.2.0"
+    classnames "^2.2.5"
+    enzyme "^3.7.0"
+    enzyme-adapter-react-16 "^1.1.1"
+    invariant "^2.2.3"
+    lodash "^4.17.4"
+    moment "^2.22.1"
+    prop-types "^15.5.10"
+    react "^16.2.0"
+    react-dom "^16.2.0"
+    react-feather "^1.0.8"
+    react-redux "^5.0.2"
+    react-router-dom "^4.2.2"
+    react-tag-autocomplete "^5.5.0"
+    recompose "^0.26.0"
+    redux "^3.6.0"
+    redux-form "^7.0.3"
+    styled-components "^4.1.1"
+
 "@types/async@2.0.49":
   version "2.0.49"
   resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0"
@@ -5516,6 +5550,21 @@ formik@^1.3.2:
     tslib "^1.9.3"
     warning "^3.0.0"
 
+formik@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/formik/-/formik-1.4.0.tgz#4261769f765dd41b7e791958fde7a08516d5920a"
+  integrity sha512-HOlb4cEgjTZ+5VMCYDlXt1r5Bt9wLhIH6uvJCAhJaIvqehmIM1RdzhYel8tCFPXzCcCx8QeZh3UcWKye5rsJmw==
+  dependencies:
+    create-react-context "^0.2.2"
+    deepmerge "^2.1.1"
+    hoist-non-react-statics "^2.5.5"
+    lodash "^4.17.11"
+    lodash-es "^4.17.11"
+    prop-types "^15.6.1"
+    react-fast-compare "^2.0.1"
+    tslib "^1.9.3"
+    warning "^3.0.0"
+
 forwarded@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -8240,6 +8289,11 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
+lodash-es@^4.17.11:
+  version "4.17.11"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
+  integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==
+
 lodash-es@^4.17.3, lodash-es@^4.17.5, lodash-es@^4.2.1:
   version "4.17.5"
   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.5.tgz#9fc6e737b1c4d151d8f9cae2247305d552ce748f"
@@ -11246,6 +11300,11 @@ react-fast-compare@^1.0.0:
   resolved "http://registry.npmjs.org/react-fast-compare/-/react-fast-compare-1.0.0.tgz#813a039155e49b43ceffe99528fe5e9d97a6c938"
   integrity sha512-dcQpdWr62flXQJuM8/bVEY5/10ad2SYBUafp8H4q4WHR3fTA/MMlp8mpzX12I0CCoEJc1P6QdiMg7U+7lFS6Rw==
 
+react-fast-compare@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
+  integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+
 react-feather@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-1.0.8.tgz#69b13d5c729949f194d33201dee91bab67fa31a2"
@@ -14726,6 +14785,13 @@ xpub-validators@^0.0.5:
   dependencies:
     striptags "^3.1.0"
 
+xpub-validators@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/xpub-validators/-/xpub-validators-0.0.6.tgz#42ebc2d722b9ec6801cc44b7177eaa71029c1514"
+  integrity sha512-zkayEdxC3NQERycoTu8YUUPNlBuN153ESbIax9exmgrO0yt4LG3be+FirXcbkgIPYmgPAB0nnhPofOCTMTJx1g==
+  dependencies:
+    striptags "^3.1.0"
+
 xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
-- 
GitLab