Skip to content
Snippets Groups Projects
Commit 15142bcd authored by Carl Schönfelder's avatar Carl Schönfelder Committed by Victor Löfgren
Browse files

Resolve "Crud users"

parent 9f804405
No related branches found
No related tags found
1 merge request!47Resolve "Crud users"
Pipeline #39119 passed
Showing
with 707 additions and 24 deletions
import axios from 'axios'
import { AppDispatch } from './../store'
import Types from './types'
export const getRoles = () => async (dispatch: AppDispatch) => {
await axios
.get('/misc/roles')
.then((res) => {
dispatch({
type: Types.SET_ROLES,
payload: res.data.items,
})
})
.catch((err) => console.log(err))
}
import axios from 'axios'
import { UserFilterParams } from '../interfaces/UserData'
import { AppDispatch, RootState } from './../store'
import Types from './types'
export const getSearchUsers = () => async (dispatch: AppDispatch, getState: () => RootState) => {
const currentParams: UserFilterParams = getState().searchUsers.filterParams
// Send params in snake-case for api
const params = {
page_size: currentParams.pageSize,
role_id: currentParams.roleId,
city_id: currentParams.cityId,
name: currentParams.name,
page: currentParams.page,
email: currentParams.email,
}
await axios
.get('/users/search', { params })
.then((res) => {
dispatch({
type: Types.SET_SEARCH_USERS,
payload: res.data.items,
})
dispatch({
type: Types.SET_SEARCH_USERS_TOTAL_COUNT,
payload: res.data.total_count,
})
dispatch({
type: Types.SET_SEARCH_USERS_COUNT,
payload: res.data.count,
})
})
.catch((err) => {
console.log(err)
})
}
export const setFilterParams = (params: UserFilterParams) => (dispatch: AppDispatch) => {
dispatch({ type: Types.SET_SEARCH_USERS_FILTER_PARAMS, payload: params })
}
export default {
LOADING_UI: 'LOADING_UI',
LOADING_USER: 'LOADING_USER',
SET_ROLES: 'SET_ROLES',
SET_USER: 'SET_USER',
SET_SEARCH_USERS: 'SET_SEARCH_USERS',
SET_SEARCH_USERS_FILTER_PARAMS: 'SET_SEARCH_USERS_FILTER_PARAMS',
SET_SEARCH_USERS_COUNT: 'SET_SEARCH_USERS_COUNT',
SET_SEARCH_USERS_TOTAL_COUNT: 'SET_SEARCH_USERS_TOTAL_COUNT',
SET_ERRORS: 'SET_ERRORS',
CLEAR_ERRORS: 'SET_ERRORS',
SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED',
......
import { City } from './City'
export interface Competition {
name: string
city: City
style_id: number
year: number
export interface Role {
id: number
name: string
}
export interface SearchUSerFilterParams {
name?: string
year?: number
cityId?: number
styleId?: number
page: number
pageSize: number
}
export interface UserData {
id: number
name?: string
email: string
role_id: number
city_id: number
}
export interface UserFilterParams {
name?: string
email?: string
cityId?: number
roleId?: number
page: number
pageSize: number
}
......@@ -12,3 +12,18 @@ export interface AddCompetitionModel {
export interface CompetitionLoginModel {
code: string
}
export interface AddUserModel {
email: string
password: string
role: string
city: string
name?: string
}
export interface EditUserModel {
email: string
role: string
city: string
name?: string
}
......@@ -19,6 +19,7 @@ import { logoutUser } from '../../actions/user'
import { useAppDispatch } from '../../hooks'
import CompetitionManager from './components/CompetitionManager'
import Regions from './components/Regions'
import UserManager from './components/UserManager'
import { LeftDrawer } from './styled'
const drawerWidth = 250
......@@ -112,9 +113,7 @@ const AdminView: React.FC = () => {
<Regions />
</Route>
<Route path={`${path}/användare`}>
<Typography variant="h1" noWrap>
Användare
</Typography>
<UserManager />
</Route>
<Route path={`${path}/tävlingshanterare`}>
<CompetitionManager />
......
import { Button, FormControl, InputLabel, MenuItem, Popover, TextField } from '@material-ui/core'
import { Alert, AlertTitle } from '@material-ui/lab'
import axios from 'axios'
import { Formik, FormikHelpers } from 'formik'
import React from 'react'
import * as Yup from 'yup'
import { getSearchUsers } from '../../../actions/searchUser'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { City } from '../../../interfaces/City'
import { AddUserModel } from '../../../interfaces/models'
import { Role } from '../../../interfaces/Role'
import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled'
interface ServerResponse {
code: number
message: string
}
interface AddUserFormModel {
model: AddUserModel
error?: string
}
const noRoleSelected = 'Välj roll'
const noCitySelected = 'Välj stad'
const userSchema: Yup.SchemaOf<AddUserFormModel> = Yup.object({
model: Yup.object()
.shape({
name: Yup.string(), //.required('Namn krävs'),
email: Yup.string().email().required('Email krävs'),
password: Yup.string()
.required('Lösenord krävs.')
.min(6, 'Lösenord måste vara minst 6 tecken.')
.matches(/[a-zA-Z]/, 'Lösenord får enbart innehålla a-z, A-Z.'),
role: Yup.string().required('Roll krävs').notOneOf([noCitySelected], 'Välj en roll'),
city: Yup.string().required('Stad krävs').notOneOf([noRoleSelected], 'Välj en stad'),
})
.required(),
error: Yup.string().optional(),
})
const AddUser: React.FC = (props: any) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
const [selectedRole, setSelectedRole] = React.useState<Role | undefined>()
const roles = useAppSelector((state) => state.roles.roles)
const [selectedCity, setSelectedCity] = React.useState<City | undefined>()
const cities = useAppSelector((state) => state.cities.cities)
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const open = Boolean(anchorEl)
const dispatch = useAppDispatch()
const id = open ? 'simple-popover' : undefined
const handleCompetitionSubmit = async (values: AddUserFormModel, actions: FormikHelpers<AddUserFormModel>) => {
const params = {
email: values.model.email,
password: values.model.password,
//name: values.model.name,
city_id: selectedCity?.id as number,
role_id: selectedRole?.id as number,
}
await axios
.post<ServerResponse>('/auth/signup', params)
.then(() => {
actions.resetForm()
setAnchorEl(null)
dispatch(getSearchUsers())
setSelectedCity(undefined)
setSelectedRole(undefined)
})
.catch(({ response }) => {
console.warn(response.data)
if (response.data && response.data.message)
actions.setFieldError('error', response.data && response.data.message)
else actions.setFieldError('error', 'Something went wrong, please try again')
})
.finally(() => {
actions.setSubmitting(false)
})
}
const userInitialValues: AddUserFormModel = {
model: { email: '', password: '', name: '', city: noCitySelected, role: noRoleSelected },
}
return (
<div>
<AddCompetitionButton color="secondary" variant="contained" onClick={handleClick}>
Ny Användare
</AddCompetitionButton>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<AddCompetitionContent>
<Formik initialValues={userInitialValues} validationSchema={userSchema} onSubmit={handleCompetitionSubmit}>
{(formik) => (
<AddCompetitionForm onSubmit={formik.handleSubmit}>
<TextField
label="Email"
name="model.email"
helperText={formik.touched.model?.email ? formik.errors.model?.email : ''}
error={Boolean(formik.touched.model?.email && formik.errors.model?.email)}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
margin="normal"
/>
<TextField
label="Lösenord"
name="model.password"
helperText={formik.touched.model?.password ? formik.errors.model?.password : ''}
error={Boolean(formik.touched.model?.password && formik.errors.model?.password)}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
margin="normal"
/>
<TextField
label="Namn"
name="model.name"
helperText={formik.touched.model?.name ? formik.errors.model?.name : ''}
error={Boolean(formik.touched.model?.name && formik.errors.model?.name)}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
margin="normal"
/>
<FormControl>
<InputLabel shrink id="demo-customized-select-native">
Region
</InputLabel>
<TextField
select
name="model.city"
id="standard-select-currency"
value={selectedCity ? selectedCity.name : noCitySelected}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={Boolean(formik.errors.model?.city && formik.touched.model?.city)}
helperText={formik.touched.model?.city && formik.errors.model?.city}
margin="normal"
>
<MenuItem value={noCitySelected} onClick={() => setSelectedCity(undefined)}>
{noCitySelected}
</MenuItem>
{cities &&
cities.map((city) => (
<MenuItem key={city.name} value={city.name} onClick={() => setSelectedCity(city)}>
{city.name}
</MenuItem>
))}
</TextField>
</FormControl>
<FormControl>
<InputLabel shrink id="demo-customized-select-native">
Roll
</InputLabel>
<TextField
select
name="model.role"
id="standard-select-currency"
value={selectedRole ? selectedRole.name : noRoleSelected}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={Boolean(formik.errors.model?.role && formik.touched.model?.role)}
helperText={formik.touched.model?.role && formik.errors.model?.role}
margin="normal"
>
<MenuItem value={noRoleSelected} onClick={() => setSelectedRole(undefined)}>
{noRoleSelected}
</MenuItem>
{roles &&
roles.map((role) => (
<MenuItem key={role.name} value={role.name} onClick={() => setSelectedRole(role)}>
{role.name}
</MenuItem>
))}
</TextField>
</FormControl>
<Button
type="submit"
fullWidth
variant="contained"
color="secondary"
disabled={!formik.isValid || !formik.values.model?.name || !formik.values.model?.city}
>
Lägg till
</Button>
{formik.errors.error && (
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
{formik.errors.error}
</Alert>
)}
</AddCompetitionForm>
)}
</Formik>
</AddCompetitionContent>
</Popover>
</div>
)
}
export default AddUser
import { render } from '@testing-library/react'
import React from 'react'
import Regions from './Regions'
it('renders regions', () => {
render(<Regions />)
})
import Typography from '@material-ui/core/Typography'
import React from 'react'
import { Button, Menu, TextField, Typography } from '@material-ui/core'
import FormControl from '@material-ui/core/FormControl'
import Paper from '@material-ui/core/Paper'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import Table from '@material-ui/core/Table'
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import axios from 'axios'
import React, { useEffect } from 'react'
import { getCities } from '../../../actions/cities'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { RemoveCompetition, TopBar } from './styled'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
table: {
width: '100%',
},
margin: {
margin: theme.spacing(1),
},
})
)
const UserManager: React.FC = (props: any) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
const [activeId, setActiveId] = React.useState<number | undefined>(undefined)
const citiesTotal = useAppSelector((state) => state.cities.total)
const cities = useAppSelector((state) => state.cities.cities)
const [newCity, setNewCity] = React.useState()
const classes = useStyles()
const dispatch = useAppDispatch()
const handleClose = () => {
setAnchorEl(null)
setActiveId(undefined)
}
useEffect(() => {
dispatch(getCities())
}, [])
const handleDeleteCity = async () => {
if (activeId) {
await axios
.delete(`/misc/cities/${activeId}`)
.then(() => {
setAnchorEl(null)
dispatch(getCities())
})
.catch(({ response }) => {
console.warn(response.data)
})
}
}
const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => {
setAnchorEl(event.currentTarget)
setActiveId(id)
}
const handleAddCity = async () => {
await axios
.post(`/misc/cities`, { name: newCity })
.then(() => {
setAnchorEl(null)
dispatch(getCities())
})
.catch(({ response }) => {
console.warn(response.data)
})
}
const handleChange = (event: any) => {
setNewCity(event.target.value)
}
const Regions: React.FC = () => {
return (
<Typography variant="h1" noWrap>
Regions
</Typography>
<div>
<TopBar>
<FormControl className={classes.margin}>
<TextField className={classes.margin} value={newCity} onChange={handleChange} label="Region"></TextField>
<Button color="primary" variant="contained" onClick={handleAddCity}>
Lägg till
</Button>
</FormControl>
</TopBar>
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Regioner</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{cities &&
cities.map((row) => (
<TableRow key={row.name}>
<TableCell scope="row">{row.name}</TableCell>
<TableCell align="right">
<Button onClick={(event) => handleClick(event, row.id)}>
<MoreHorizIcon />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{(!cities || cities.length === 0) && <Typography>Inga regioner hittades</Typography>}
</TableContainer>
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
<RemoveCompetition onClick={handleDeleteCity}>Ta bort</RemoveCompetition>
</Menu>
</div>
)
}
export default Regions
export default UserManager
import { Button, Menu, TablePagination, TextField, Typography } from '@material-ui/core'
import FormControl from '@material-ui/core/FormControl'
import InputLabel from '@material-ui/core/InputLabel'
import MenuItem from '@material-ui/core/MenuItem'
import Paper from '@material-ui/core/Paper'
import Select from '@material-ui/core/Select'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import Table from '@material-ui/core/Table'
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import axios from 'axios'
import React, { useEffect } from 'react'
import { getCities } from '../../../actions/cities'
import { getRoles } from '../../../actions/roles'
import { getSearchUsers, setFilterParams } from '../../../actions/searchUser'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { UserFilterParams } from '../../../interfaces/UserData'
import AddUser from './AddUser'
import { FilterContainer, RemoveCompetition, TopBar } from './styled'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
table: {
width: '100%',
},
margin: {
margin: theme.spacing(1),
},
})
)
const UserManager: React.FC = (props: any) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
const [activeId, setActiveId] = React.useState<number | undefined>(undefined)
const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined)
const users = useAppSelector((state) => state.searchUsers.users)
const filterParams = useAppSelector((state) => state.searchUsers.filterParams)
const usersTotal = useAppSelector((state) => state.searchUsers.total)
const cities = useAppSelector((state) => state.cities.cities)
const roles = useAppSelector((state) => state.roles.roles)
const classes = useStyles()
const noFilterText = 'Alla'
const dispatch = useAppDispatch()
const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => {
setAnchorEl(event.currentTarget)
setActiveId(id)
}
const handleClose = () => {
setAnchorEl(null)
setActiveId(undefined)
}
useEffect(() => {
dispatch(getCities())
dispatch(getRoles())
dispatch(getSearchUsers())
}, [])
const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
if (timerHandle) {
clearTimeout(timerHandle)
setTimerHandle(undefined)
}
//Only updates filter and api 100ms after last input was made
setTimerHandle(window.setTimeout(() => dispatch(getSearchUsers()), 100))
dispatch(setFilterParams({ ...filterParams, email: event.target.value }))
}
const handleDeleteUsers = async () => {
if (activeId) {
await axios
.delete(`/auth/delete/${activeId}`)
.then(() => {
setAnchorEl(null)
dispatch(getSearchUsers())
})
.catch(({ response }) => {
console.warn(response.data)
})
}
}
const handleFilterChange = (newParams: UserFilterParams) => {
dispatch(setFilterParams(newParams))
dispatch(getSearchUsers())
}
return (
<div>
<TopBar>
<FilterContainer>
<TextField
className={classes.margin}
value={filterParams.email || ''}
onChange={onSearchChange}
label="Sök"
></TextField>
<FormControl className={classes.margin}>
<InputLabel shrink id="demo-customized-select-native">
Region
</InputLabel>
<Select
labelId="demo-customized-select-label"
id="demo-customized-select"
value={filterParams.cityId ? cities.find((city) => filterParams.cityId === city.id)?.name : noFilterText}
>
<MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, cityId: undefined })}>
{noFilterText}
</MenuItem>
{cities &&
cities.map((city) => (
<MenuItem
key={city.name}
value={city.name}
onClick={() => handleFilterChange({ ...filterParams, cityId: city.id })}
>
{city.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl className={classes.margin}>
<InputLabel shrink id="demo-customized-select-native">
Roles
</InputLabel>
<Select
labelId="demo-customized-select-label"
id="demo-customized-select"
value={filterParams.roleId ? roles.find((role) => filterParams.roleId === role.id)?.name : noFilterText}
>
<MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, roleId: undefined })}>
{noFilterText}
</MenuItem>
{cities &&
cities.map((role) => (
<MenuItem
key={role.name}
value={role.name}
onClick={() => handleFilterChange({ ...filterParams, roleId: role.id })}
>
{role.name}
</MenuItem>
))}
</Select>
</FormControl>
</FilterContainer>
<AddUser />
</TopBar>
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Email</TableCell>
<TableCell>Namn</TableCell>
<TableCell>Region</TableCell>
<TableCell>Roll</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{users &&
users.map((row) => (
<TableRow key={row.email}>
<TableCell scope="row">{row.email}</TableCell>
<TableCell scope="row">{row.name}</TableCell>
<TableCell>{cities.find((city) => city.id === row.city_id)?.name || ''}</TableCell>
<TableCell>{roles.find((role) => role.id === row.role_id)?.name || ''}</TableCell>
<TableCell align="right">
<Button onClick={(event) => handleClick(event, row.id)}>
<MoreHorizIcon />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{(!users || users.length === 0) && <Typography>Inga tävlingar hittades med nuvarande filter</Typography>}
</TableContainer>
<TablePagination
component="div"
rowsPerPageOptions={[]}
rowsPerPage={filterParams.pageSize}
count={usersTotal}
page={filterParams.page}
onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })}
/>
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
<MenuItem>Redigera</MenuItem>
<MenuItem>Byt lösenord</MenuItem>
<RemoveCompetition onClick={handleDeleteUsers}>Ta bort</RemoveCompetition>
</Menu>
</div>
)
}
export default UserManager
......@@ -3,6 +3,8 @@
import { combineReducers } from 'redux'
import citiesReducer from './citiesReducer'
import competitionsReducer from './competitionsReducer'
import rolesReducer from './rolesReducer'
import searchUserReducer from './searchUserReducer'
import uiReducer from './uiReducer'
import userReducer from './userReducer'
......@@ -12,5 +14,7 @@ const allReducers = combineReducers({
UI: uiReducer,
competitions: competitionsReducer,
cities: citiesReducer,
roles: rolesReducer,
searchUsers: searchUserReducer,
})
export default allReducers
import { AnyAction } from 'redux'
import Types from '../actions/types'
import { Role } from '../interfaces/Role'
interface RoleState {
roles: Role[]
}
const initialState: RoleState = {
roles: [],
}
export default function (state = initialState, action: AnyAction) {
switch (action.type) {
case Types.SET_ROLES:
return { ...state, roles: action.payload as Role[] }
default:
return state
}
}
import { AnyAction } from 'redux'
import Types from '../actions/types'
import { UserData, UserFilterParams } from '../interfaces/UserData'
interface SearchUserState {
users: UserData[]
total: number
count: number
filterParams: UserFilterParams
}
const initialState: SearchUserState = {
users: [],
total: 0,
count: 0,
filterParams: { pageSize: 10, page: 0 },
}
export default function (state = initialState, action: AnyAction) {
switch (action.type) {
case Types.SET_SEARCH_USERS:
return {
...state,
users: action.payload as UserData[],
}
case Types.SET_SEARCH_USERS_FILTER_PARAMS:
return {
...state,
filterParams: action.payload as UserFilterParams,
}
case Types.SET_SEARCH_USERS_TOTAL_COUNT:
return {
...state,
total: action.payload as number,
}
case Types.SET_SEARCH_USERS_COUNT:
return {
...state,
count: action.payload as number,
}
default:
return state
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment