소스 검색

several changes

Tomi Cvetic 5 년 전
부모
커밋
41da436c26

+ 5 - 1
backend/index.js

@@ -15,7 +15,11 @@ const user = require('./src/user')
 
 const prismaResolvers = require('./src/resolvers')
 
-const resolvers = merge(prismaResolvers.resolvers, user.resolvers)
+const resolvers = merge(
+  prismaResolvers.resolvers,
+  user.resolvers
+)
+
 const typeDefs = ['./schema.graphql']
 
 const server = new GraphQLServer({

+ 48 - 7
backend/schema.graphql

@@ -1,21 +1,62 @@
 # import * from './database/generated/prisma.graphql'
 
 type Query {
-  users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
+  users(
+    where: UserWhereInput
+    orderBy: UserOrderByInput
+    skip: Int
+    after: String
+    before: String
+    first: Int
+    last: Int
+  ): [User]!
   training(where: TrainingWhereUniqueInput!): Training
-  trainings(where: TrainingWhereInput, orderBy: TrainingOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Training]!
-  trainingTypes(where: TrainingTypeWhereInput, orderBy: TrainingTypeOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [TrainingType]!
-  blocks(where: BlockWhereInput, orderBy: BlockOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Block]!
+  trainings(
+    where: TrainingWhereInput
+    orderBy: TrainingOrderByInput
+    skip: Int
+    after: String
+    before: String
+    first: Int
+    last: Int
+  ): [Training]!
+  trainingTypes(
+    where: TrainingTypeWhereInput
+    orderBy: TrainingTypeOrderByInput
+    skip: Int
+    after: String
+    before: String
+    first: Int
+    last: Int
+  ): [TrainingType]!
+  blocks(
+    where: BlockWhereInput
+    orderBy: BlockOrderByInput
+    skip: Int
+    after: String
+    before: String
+    first: Int
+    last: Int
+  ): [Block]!
   currentUser: User!
 }
 
 type Mutation {
   createUser(data: UserCreateInput!): User!
-  updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
-  deleteUser(where: UserWhereUniqueInput!): User
+  updateUser(email: String!, data: UserUpdateInput!): User
+  deleteUser(email: String!): User
   createTraining(title: String!): Training!
   createTrainingType(name: String!, description: String!): TrainingType!
-  createBlock(sequence: Int!, title: String!, duration: Int!, variation: String, format: ID, tracks: [ID]!, exercises: [ID]!, description: String!): Block!
+  createBlock(
+    sequence: Int!
+    title: String!
+    duration: Int!
+    variation: String
+    format: ID
+    tracks: [ID]!
+    exercises: [ID]!
+    description: String!
+  ): Block!
   login(email: String!, password: String!): User!
   logout: String!
   signup(name: String!, email: String!, password: String!): User!

+ 4 - 4
backend/src/user/resolvers.js

@@ -35,7 +35,7 @@ const Mutation = {
       }
     }, info)
   },
-  updateUser: async (parent, { data, where }, context, info) => {
+  updateUser: async (parent, { email, data }, context, info) => {
     if (!context.request.userId) throw LoginError
     if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
 
@@ -44,14 +44,14 @@ const Mutation = {
     if (data.password) updateData.password = await bcrypt.hash(data.password, 10)
     return context.db.mutation.updateUser({
       data: updateData,
-      where
+      where: { email }
     }, info)
   },
-  deleteUser: (parent, { where }, context, info) => {
+  deleteUser: (parent, { email }, context, info) => {
     if (!context.request.userId) throw LoginError
     if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
 
-    return context.db.mutation.deleteUser({ where })
+    return context.db.mutation.deleteUser({ where: { email } })
   },
   signup: async (parent, args, ctx, info) => {
     const email = args.email.toLowerCase()

+ 5 - 1
frontend/Dockerfile

@@ -6,8 +6,12 @@ ENV PATH /app/node_modules/.bin:$PATH
 
 COPY package.json /app/package.json
 
-RUN npm install --silent
+RUN rm -rf node_modules/*
+RUN rm -rf node_modules/.bin
+RUN rm -rf node_modules/.cache
+RUN rm -f package-lock.json
 RUN npm install typescript @types/react --silent
 RUN npm install react-scripts -g --silent
+RUN npm install --silent
 
 CMD ["npm", "run", "dev"]

+ 3 - 3
frontend/components/training/trainingType.js

@@ -2,9 +2,9 @@ import { Query, Mutation } from 'react-apollo'
 import { adopt } from 'react-adopt'
 import { Formik, Form } from 'formik'
 
-import { TextInput } from '../lib/forms'
-import { CREATE_TRAINING_TYPE, TRAINING_TYPES } from '../lib/graphql'
-import Overlay from './overlay'
+import { TextInput } from '../../lib/forms'
+import { CREATE_TRAINING_TYPE, TRAINING_TYPES } from '../../lib/graphql'
+import Overlay from '../overlay'
 
 const TrainingTypeInput = props => {
   const [displayForm, setDisplayForm] = React.useState(false)

+ 0 - 3
frontend/global.d.ts

@@ -1,4 +1 @@
 type Dict = { [name: string]: any }
-type Permissions =
-  | 'ADMIN'
-  | 'INSTRUCTOR'

+ 0 - 12
frontend/package-lock.json

@@ -7425,18 +7425,6 @@
         }
       }
     },
-    "next-apollo": {
-      "version": "3.1.10",
-      "resolved": "https://registry.npmjs.org/next-apollo/-/next-apollo-3.1.10.tgz",
-      "integrity": "sha512-VX64IW7h6Rdg+Ma0y8ryAIz7IfyLaz6Nd4VITuhfXFZ3nj51blcjRR6kc4E945SPk2WdrIMlLRZubgPRAgh9Ug==",
-      "requires": {
-        "@babel/runtime": "^7.4.5",
-        "isomorphic-unfetch": "^3.0.0",
-        "prop-types": "15.7.2",
-        "prop-types-exact": "1.2.0",
-        "url": "0.11.0"
-      }
-    },
     "next-link": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/next-link/-/next-link-2.0.0.tgz",

+ 0 - 2
frontend/package.json

@@ -12,7 +12,6 @@
   },
   "dependencies": {
     "@apollo/client": "^3.0.0-beta.16",
-    "@apollo/react-hooks": "^3.1.3",
     "@apollo/react-ssr": "^3.1.3",
     "@types/jest": "^24.0.25",
     "@types/lodash": "^4.14.149",
@@ -26,7 +25,6 @@
     "isomorphic-unfetch": "^3.0.0",
     "lodash": "^4.17.15",
     "next": "9.1.2",
-    "next-apollo": "^3.1.10",
     "next-link": "^2.0.0",
     "normalize.css": "^8.0.1",
     "nprogress": "^0.2.0",

+ 3 - 3
frontend/pages/signup.js

@@ -1,5 +1,5 @@
-import SignupForm from '../components/user/signup'
+import SignupForm from '../components/user/SignupForm'
 
-const Signup = props => <SignupForm />
+const SignupPage = props => <SignupForm />
 
-export default Signup
+export default SignupPage

+ 20 - 22
frontend/pages/user.tsx

@@ -1,36 +1,34 @@
-import { useQuery } from '@apollo/client'
 import { withRouter } from 'next/router'
 
-import SignupForm from '../components/user/SignupForm'
-import LoginForm from '../components/user/LoginForm'
-import LogoutButton from '../components/user/LogoutButton'
-import RequestPassword from '../components/user/RequestPassword'
-import ResetPassword from '../components/user/ResetPassword'
-import UserEditForm from '../components/user/UserEditForm'
-import UserDetails from '../components/user/UserDetails'
-import DeleteUserButton from '../components/user/DeleteUserButton'
-import UserAdmin from '../components/user/UserAdmin'
-
-import { CURRENT_USER } from '../components/user/graphql'
+import { useCurrentUserQuery } from '../src/gql'
 
+import {
+  SignupForm,
+  LoginForm,
+  LogoutButton,
+  RequestPassword,
+  ResetPassword,
+  UserDetails,
+  DeleteUserButton
+} from '../src/user'
 
 const UserPage = () => {
-  const { data, loading, error } = useQuery(CURRENT_USER)
+  const { data, loading, error } = useCurrentUserQuery()
   console.log('UserPage', data, loading, error && error.message)
+  const user = data && data.me
+
+  if (loading) return <p>Loading user data...</p>
+  if (error) return <p>Error loading user data.</p>
 
   return (
     <>
-      {data && <p>data.me.name</p>}
-      {error && <p>error.message</p>}
-      {loading && <p>Loading...</p>}
-      {error && <LoginForm />}
-      {data && <LogoutButton />}
+      {loading && <p>'Loading'</p>}
+      {user ? <LogoutButton /> : <LoginForm />}
+      <SignupForm />
       <RequestPassword />
       <ResetPassword />
-      {data && <UserEditForm user={data.me} />}
-      {data && <UserDetails user={data.me} />}
-      {data && <DeleteUserButton user={data.me} />}
-      <UserAdmin />
+      {user && <UserDetails user={user} />}
+      {user && <DeleteUserButton user={user} />}
     </>
   )
 }

+ 7 - 2
frontend/src/form/components/TextInput.tsx

@@ -1,9 +1,14 @@
+import { useEffect } from 'react'
 import { useField } from 'formik'
 import { InputProps } from '../types'
 
-const TextInput = ({ label, ...props }: any) => {
+const TextInput = ({ label, setState, ...props }: any) => {
   const [field, meta] = useField(props)
-  console.log(field, meta)
+
+  useEffect(() => {
+  }, [meta])
+
+  console.log('TextInput', meta)
   return (
     <>
       {label && <label htmlFor={props.id || props.name}>{label}</label>}

+ 2 - 12
frontend/src/form/types.ts

@@ -1,14 +1,4 @@
-export interface InputProps {
-  id: string,
-  name: string,
-  value: any,
-  onChange: (event: React.ChangeEvent) => void,
-  onBlur: (event: React.SyntheticEvent) => void,
-  placeholder: string,
+export interface InputProps extends React.HTMLProps<HTMLInputElement> {
   label?: string
-}
-
-export interface InputPropsOptions {
-  label?: string
-  subtree?: string
+  setState?: (args: any) => void
 }

+ 2 - 2
frontend/src/form/useFormHandler.ts

@@ -1,6 +1,6 @@
 import { useState, FormEvent, ChangeEvent, SyntheticEvent } from 'react'
 
-import { InputProps, InputPropsOptions } from './types'
+import { InputProps } from './types'
 import { getValue, setValue } from '../lib/nestedValues'
 
 interface FormHandler<ValueObject> {
@@ -60,7 +60,7 @@ export function useFormHandler<ValueObject>(
   }
 
   // users[3].name
-  function inputProps(name: string, options?: InputPropsOptions): InputProps {
+  function inputProps(name: string, options?: any): any {
     const path = (options && options.subtree) ? `${options.subtree}.${name}` : name
 
     return {

+ 103 - 12
frontend/src/gql/index.tsx

@@ -785,13 +785,13 @@ export type MutationCreateUserArgs = {
 
 
 export type MutationUpdateUserArgs = {
-  data: UserUpdateInput,
-  where: UserWhereUniqueInput
+  email: Scalars['String'],
+  data: UserUpdateInput
 };
 
 
 export type MutationDeleteUserArgs = {
-  where: UserWhereUniqueInput
+  email: Scalars['String']
 };
 
 
@@ -859,7 +859,7 @@ export type Query = {
   trainings: Array<Maybe<Training>>,
   trainingTypes: Array<Maybe<TrainingType>>,
   blocks: Array<Maybe<Block>>,
-  me: User,
+  currentUser: User,
 };
 
 
@@ -1923,11 +1923,6 @@ export type UserWhereInput = {
   ratings_none?: Maybe<RatingWhereInput>,
 };
 
-export type UserWhereUniqueInput = {
-  id?: Maybe<Scalars['ID']>,
-  email?: Maybe<Scalars['String']>,
-};
-
 export type UsersQueryVariables = {};
 
 
@@ -1981,7 +1976,7 @@ export type CurrentUserQueryVariables = {};
 
 export type CurrentUserQuery = (
   { __typename?: 'Query' }
-  & { me: (
+  & { currentUser: (
     { __typename?: 'User' }
     & Pick<User, 'id' | 'email' | 'name' | 'permissions' | 'interests'>
   ) }
@@ -2011,6 +2006,33 @@ export type ResetPasswordMutation = (
   ) }
 );
 
+export type UserDeleteMutationVariables = {
+  email: Scalars['String']
+};
+
+
+export type UserDeleteMutation = (
+  { __typename?: 'Mutation' }
+  & { deleteUser: Maybe<(
+    { __typename?: 'User' }
+    & Pick<User, 'id'>
+  )> }
+);
+
+export type UserUpdateMutationVariables = {
+  email: Scalars['String'],
+  data: UserUpdateInput
+};
+
+
+export type UserUpdateMutation = (
+  { __typename?: 'Mutation' }
+  & { updateUser: Maybe<(
+    { __typename?: 'User' }
+    & Pick<User, 'id' | 'name' | 'email' | 'permissions' | 'interests'>
+  )> }
+);
+
 
 export const UsersDocument = gql`
     query Users {
@@ -2150,7 +2172,7 @@ export type UserLogoutMutationResult = ApolloReactCommon.MutationResult<UserLogo
 export type UserLogoutMutationOptions = ApolloReactCommon.BaseMutationOptions<UserLogoutMutation, UserLogoutMutationVariables>;
 export const CurrentUserDocument = gql`
     query CurrentUser {
-  me {
+  currentUser {
     id
     email
     name
@@ -2247,4 +2269,73 @@ export function useResetPasswordMutation(baseOptions?: ApolloReactHooks.Mutation
       }
 export type ResetPasswordMutationHookResult = ReturnType<typeof useResetPasswordMutation>;
 export type ResetPasswordMutationResult = ApolloReactCommon.MutationResult<ResetPasswordMutation>;
-export type ResetPasswordMutationOptions = ApolloReactCommon.BaseMutationOptions<ResetPasswordMutation, ResetPasswordMutationVariables>;
+export type ResetPasswordMutationOptions = ApolloReactCommon.BaseMutationOptions<ResetPasswordMutation, ResetPasswordMutationVariables>;
+export const UserDeleteDocument = gql`
+    mutation UserDelete($email: String!) {
+  deleteUser(email: $email) {
+    id
+  }
+}
+    `;
+export type UserDeleteMutationFn = ApolloReactCommon.MutationFunction<UserDeleteMutation, UserDeleteMutationVariables>;
+
+/**
+ * __useUserDeleteMutation__
+ *
+ * To run a mutation, you first call `useUserDeleteMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useUserDeleteMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [userDeleteMutation, { data, loading, error }] = useUserDeleteMutation({
+ *   variables: {
+ *      email: // value for 'email'
+ *   },
+ * });
+ */
+export function useUserDeleteMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UserDeleteMutation, UserDeleteMutationVariables>) {
+        return ApolloReactHooks.useMutation<UserDeleteMutation, UserDeleteMutationVariables>(UserDeleteDocument, baseOptions);
+      }
+export type UserDeleteMutationHookResult = ReturnType<typeof useUserDeleteMutation>;
+export type UserDeleteMutationResult = ApolloReactCommon.MutationResult<UserDeleteMutation>;
+export type UserDeleteMutationOptions = ApolloReactCommon.BaseMutationOptions<UserDeleteMutation, UserDeleteMutationVariables>;
+export const UserUpdateDocument = gql`
+    mutation UserUpdate($email: String!, $data: UserUpdateInput!) {
+  updateUser(email: $email, data: $data) {
+    id
+    name
+    email
+    permissions
+    interests
+  }
+}
+    `;
+export type UserUpdateMutationFn = ApolloReactCommon.MutationFunction<UserUpdateMutation, UserUpdateMutationVariables>;
+
+/**
+ * __useUserUpdateMutation__
+ *
+ * To run a mutation, you first call `useUserUpdateMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useUserUpdateMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [userUpdateMutation, { data, loading, error }] = useUserUpdateMutation({
+ *   variables: {
+ *      email: // value for 'email'
+ *      data: // value for 'data'
+ *   },
+ * });
+ */
+export function useUserUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UserUpdateMutation, UserUpdateMutationVariables>) {
+        return ApolloReactHooks.useMutation<UserUpdateMutation, UserUpdateMutationVariables>(UserUpdateDocument, baseOptions);
+      }
+export type UserUpdateMutationHookResult = ReturnType<typeof useUserUpdateMutation>;
+export type UserUpdateMutationResult = ApolloReactCommon.MutationResult<UserUpdateMutation>;
+export type UserUpdateMutationOptions = ApolloReactCommon.BaseMutationOptions<UserUpdateMutation, UserUpdateMutationVariables>;

+ 1 - 1
frontend/src/user/__tests__/signup.js.bak

@@ -2,7 +2,7 @@ import React from 'react'
 import renderer from 'react-test-renderer'
 import { MockedProvider } from '@apollo/react-testing'
 
-import SignupForm from '../signup'
+import SignupForm from '../SignupForm'
 import { USER_SIGNUP } from '../graphql'
 
 const { act } = renderer

+ 6 - 9
frontend/src/user/components/DeleteUserButton.tsx

@@ -1,18 +1,15 @@
-import { SyntheticEvent } from "react"
-import { } from ''
+import { useUserDeleteMutation, User } from '../../gql'
 
 interface DeleteUserProps {
-  user: {
-    id: string
-  }
+  user: User
   title?: string
 }
 
-const DeleteUserButton = ({ title, user: { id } }: DeleteUserProps) => {
-  const [deleteUser, { loading, error }] = useMutation(USER_DELETE)
+const DeleteUserButton = ({ title, user: { email } }: DeleteUserProps) => {
+  const [deleteUser, { loading, error }] = useUserDeleteMutation()
   return (
-    <button onClick={(event: SyntheticEvent) => {
-      deleteUser({ variables: { id } })
+    <button onClick={(event: React.SyntheticEvent) => {
+      deleteUser({ variables: { email } })
     }}>{title || 'Delete user'}</button>
   )
 }

+ 19 - 19
frontend/src/user/components/LoginForm.tsx

@@ -1,5 +1,6 @@
+import { Formik, Form } from 'formik'
 import { useUserLoginMutation, CurrentUserDocument } from '../../gql'
-import { useFormHandler, TextInput } from '../../form'
+import { TextInput } from '../../form'
 
 const initialValues = {
   email: 'tomislav.cvetic@u-blox.com',
@@ -8,26 +9,25 @@ const initialValues = {
 
 const LoginForm = () => {
 
-  const [login, { loading, error }] = useUserLoginMutation()
-  const { inputProps, values } = useFormHandler(initialValues)
+  const [login, { loading, error, data }] = useUserLoginMutation({ refetchQueries: CurrentUserDocument })
+  console.log('LoginForm', loading, error, data)
 
   return (
-    <form onSubmit={async (event: React.FormEvent) => {
-      event.preventDefault()
-      try {
-        const data = await login({
-          variables: values,
-          refetchQueries: [{ query: CurrentUserDocument }]
-        })
-        console.log('LoginForm', data)
-      } catch (error) {
-        console.log('LoginForm', error)
-      }
-    }}>
-      <TextInput label='Email' {...inputProps('email')} />
-      <TextInput label='Password' {...inputProps('password')} />
-      <button type='submit' disabled={loading}>Login!</button>
-    </form>
+    <Formik
+      initialValues={initialValues}
+      onSubmit={values => {
+        login({ variables: values })
+      }}
+    >
+      {({ values, setValues }) => (
+        <Form>
+          <TextInput label='Email' name='email' type='text' placeholder='Email' />
+          <TextInput label='Password' name='password' type='password' />
+          <button type='submit' disabled={loading}>Login!</button>
+          {error && <div className='error'>{error.message}</div>}
+        </Form>
+      )}
+    </Formik>
   )
 }
 

+ 3 - 1
frontend/src/user/components/ResetPassword.tsx

@@ -2,6 +2,7 @@ import { useResetPasswordMutation } from "../../gql"
 import { useFormHandler, TextInput } from "../../form"
 
 const initialValues = {
+  token: '',
   password: '',
   passwordAgain: ''
 }
@@ -9,11 +10,12 @@ const initialValues = {
 const ResetPassword = () => {
 
   const [resetPassword, { loading, error }] = useResetPasswordMutation()
-  const { inputProps } = useFormHandler(initialValues)
+  const { inputProps, values: { token, password } } = useFormHandler(initialValues)
 
   return (
     <form onSubmit={async (event: React.FormEvent) => {
       event.preventDefault()
+      resetPassword({ variables: { token, password } })
     }}>
       <TextInput label='Password' {...inputProps('password')} />
       <TextInput label='Repeat password' {...inputProps('passwordAgain')} />

+ 0 - 1
frontend/src/user/components/SignupForm.tsx

@@ -2,7 +2,6 @@ import { useFormHandler, TextInput } from '../../form'
 import { useUserSignupMutation } from '../../gql'
 import { userValidation } from '../validation'
 
-
 const initialValues = {
   name: 'Tomi Cvetic',
   email: 'tomislav.cvetic@u-blox.com',

+ 59 - 23
frontend/src/user/components/UserAdmin.tsx

@@ -1,10 +1,8 @@
 import { useEffect, useState } from 'react'
-import { Formik, Form } from 'formik'
-import { useUsersQuery, UsersQuery, Permission } from '../../gql'
+import { Formik, Form, useField } from 'formik'
+import { useUsersQuery, useUserDeleteMutation, useUserUpdateMutation, UsersQuery, Permission, User, UsersQueryResult } from '../../gql'
 import { TextInput, Checkbox } from '../../form'
 
-const Permissions = Object.getOwnPropertyNames(Permission)
-console.log('Permissions', Permissions)
 
 const emptyUser = {
   name: '',
@@ -16,24 +14,66 @@ const emptyUser = {
 }
 
 interface UserInputProps {
-  inputProps: (name: string, options?: any) => any
-  subtree: string
+  user: UsersQueryResult
+  path: string
+  touched: boolean[]
 }
 
-const UserInput = ({ path, ...props }: any) => {
-  const [touched, touch] = useState(false)
+function dbToForm(user: UsersQueryResult): any {
+  if (!user) return null
+  const { __typename, id, permissions, ...returnValue } = user
+  for (let key of Object.keys(Permission)) {
+    (returnValue as any)[key] = permissions.includes(Permission[key as keyof object])
+  }
+  return returnValue
+}
+
+function formToDb() { }
+
+const UserInput = ({ user, path, touched }: any) => {
+  const [updateUser, updateData] = useUserUpdateMutation()
+  const [deleteUser, deleteData] = useUserDeleteMutation()
+
+  function composeData(formValues: any) {
+    if (!touched) return {}
+    const { admin, instructor, ...otherTouched } = touched
+    const permissions = (admin || instructor) ? Object.entries(Permission).filter(([key, value]) => user[key]) : {}
+    return {
+      ...Object.keys(otherTouched).map(key => formValues[key]),
+      ...permissions
+    }
+  }
+
+  console.log('UserInput', user, composeData(user))
 
   return (
     <tr>
       <td><input type='checkbox' /></td>
       <td><TextInput name={`${path}.name`} type='text' placeholder='Name' /></td>
       <td><TextInput name={`${path}.email`} type='email' placeholder='eMail' /></td>
-      <td><Checkbox name={`${path}.admin`} /></td>
-      <td><Checkbox name={`${path}.instructor`} /></td>
+      {Object.keys(Permission).map(permission => (
+        <td><Checkbox name={`${path}.${permission}`} /></td>
+      ))}
       <td></td>
-      <td><button>Delete</button><button>Save</button></td>
+      <td>
+        <button type='button' disabled={deleteData.loading} onClick={async event => {
+          try {
+            const deletedUser = await deleteUser({ variables: { email: user.email } })
+            console.log(deletedUser)
+          } catch (error) {
+            console.log(error.message)
+          }
+        }}>Delete</button>
+        <button type='button' disabled={!touched || updateData.loading} onClick={async event => {
+          try {
+            const updatedUser = await updateUser({ variables: { email: user.email, data: user } })
+            console.log(updatedUser)
+          } catch (error) {
+            console.log(error.message)
+          }
+        }}>Save</button>
+      </td>
     </tr>
-
   )
 }
 
@@ -46,22 +86,18 @@ const UserAdmin = () => {
   if (error) return <p>Error: {error.message}</p>
   if (loading) return <p>Loading...</p>
   if (data) {
+
     const formData = data.users.map(user => {
-      if (!user) return null
-      const { permissions, ...rest } = user
-      return {
-        ...rest,
-        admin: permissions.includes(Permission.Admin),
-        instructor: permissions.includes(Permission.Instructor)
-      }
     })
 
     return (
       <Formik
         initialValues={formData}
-        onSubmit={values => console.log(values)}
+        onSubmit={values => {
+          console.log(values)
+        }}
       >
-        {({ values, setValues }) => (
+        {({ values, setValues, touched }) => (
           <>
             <Form>
               <table>
@@ -70,7 +106,7 @@ const UserAdmin = () => {
                     <th></th>
                     <th>Name</th>
                     <th>Email</th>
-                    {Permissions.map(permission => (
+                    {Object.keys(Permission).map(permission => (
                       <th key={permission}>{permission}</th>
                     ))}
                     <th>Interests</th>
@@ -79,7 +115,7 @@ const UserAdmin = () => {
                 </thead>
                 <tbody>
                   {values.map((user, index) => user && (
-                    <UserInput path={`[${index}]`} key={user.id} />
+                    <UserInput path={`[${index}]`} user={user} touched={touched[index]} key={user.id} />
                   ))}
                 </tbody>
               </table>

+ 0 - 1
frontend/src/user/components/UserDetails.tsx

@@ -11,5 +11,4 @@ const UserDetails = ({ user }: UserProps) => {
   )
 }
 
-
 export default UserDetails

+ 2 - 2
frontend/src/user/components/UserEditForm.tsx

@@ -11,13 +11,13 @@ const initialData = {
 }
 
 const UserEditForm = ({ user }: UserProps) => {
-  const [updateUser, userEdit] = useMutation(USER_EDIT)
+  const [updateUser, userEdit] = useUserUpdateMutation()
   const { inputProps, values } = useFormHandler({ ...initialData, ...user })
 
   return (
     <form onSubmit={async (event: React.SyntheticEvent) => {
       event.preventDefault()
-      await updateUser({ variables: values, refetchQueries: [{ query: CURRENT_USER }] })
+      await updateUser({ variables: { id: user.id, data: '12' } })
     }}>
       <TextInput label='Name' {...inputProps('name')} />
       <TextInput label='Email' {...inputProps('email')} />

+ 31 - 0
frontend/src/user/components/__tests__/DeleteUserButton.test.tsx

@@ -0,0 +1,31 @@
+import { shallow } from 'enzyme'
+import { MockedProvider } from '@apollo/client/testing'
+
+import DeleteUserButton from '../DeleteUserButton'
+import { UserDeleteDocument } from '../../../gql'
+
+const mocks = [
+  {
+    request: {
+      query: UserDeleteDocument,
+      variables: {
+        id: '12'
+      }
+    },
+    result: {
+      data: {
+        user: { id: '12' }
+      }
+    }
+  }
+]
+
+describe('testing delete user button', () => {
+  it('renders properly', () => {
+    const component = shallow(
+      <MockedProvider mocks={mocks} addTypename={false}>
+        <DeleteUserButton title='Delete' user={{ id: '12' }} />
+      </MockedProvider>
+    )
+  })
+})

+ 3 - 1
frontend/src/user/index.ts

@@ -5,6 +5,7 @@ import ResetPassword from './components/ResetPassword'
 import SignupForm from './components/SignupForm'
 import UserAdmin from './components/UserAdmin'
 import UserDetails from './components/UserDetails'
+import DeleteUserButton from './components/DeleteUserButton'
 
 export {
   LoginForm,
@@ -13,5 +14,6 @@ export {
   ResetPassword,
   SignupForm,
   UserAdmin,
-  UserDetails
+  UserDetails,
+  DeleteUserButton
 }

+ 17 - 2
frontend/src/user/user.graphql

@@ -1,4 +1,3 @@
-
 query Users {
   users {
     id
@@ -30,7 +29,7 @@ mutation UserLogout {
 }
 
 query CurrentUser {
-  me {
+  currentUser {
     id
     email
     name
@@ -49,3 +48,19 @@ mutation ResetPassword($token: String!, $password: String!) {
     name
   }
 }
+
+mutation UserDelete($email: String!) {
+  deleteUser(email: $email) {
+    id
+  }
+}
+
+mutation UserUpdate($email: String!, $data: UserUpdateInput!) {
+  updateUser(email: $email, data: $data) {
+    id
+    name
+    email
+    permissions
+    interests
+  }
+}

+ 31 - 33
frontend/src/user/user.js

@@ -1,5 +1,9 @@
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
 import Link from 'next/link'
+import { useQuery } from '@apollo/client'
+import { CURRENT_USER } from './graphql'
+import LogoutButton from './components/LogoutButton'
+import LoginForm from './components/LoginForm'
 
 const UserNav = props => {
   const [menu, setMenu] = useState(false)
@@ -20,41 +24,35 @@ const UserNav = props => {
   )
 }
 
-const myStyle = (
-  <style jsx>
-    {`
-section.usermenu {
-position: absolute;
-background: rgba(127,0,0,0.5);
-}
-`}
-  </style>
-)
-
 const UserNavMenu = props => {
-  const logout = async (ev, logout) => {
-    ev.preventDefault()
-    try {
-      const id = await logout()
-    } catch (error) {
-      console.log(error)
-    }
-  }
+  const { data, loading, error } = useQuery(CURRENT_USER, { fetchPolicy: 'cache-and-network' })
+  console.log('UserNav', data, loading, error && error.message)
+  const user = data && data.me
+
+  if (loading) return <p>Loading user data...</p>
+  if (error) return <p>Error loading user data.</p>
 
   return (
-    <>
-      <section className='usermenu'>
-        <h2>Welcome, {name}</h2>
-        <Link href={{ pathname: 'user' }}><a>Edit user data</a></Link>
-        <a
-          href='' onClick={ev => {
-            ev.preventDefault()
-            this.logout(ev, logout)
-          }}
-        >Logout
-        </a>
-      </section>
-    </>
+    <section className='usermenu'>
+      {user ? (
+        <>
+          <h2>Welcome, {user.name}</h2>
+          <Link href={{ pathname: 'user' }}><a>Edit user data</a></Link>
+          <LogoutButton />
+        </>
+      ) : (
+          <LoginForm />
+        )}
+
+      <style jsx>
+        {`
+          section.usermenu {
+          position: absolute;
+          background: rgba(127,0,0,0.5);
+          }
+        `}
+      </style>
+    </section>
   )
 }