commit 5b1882cbce53253b3ce318ef3da7338eb4b9c3af
Author: Schwobik <michal.schwob@seznam.cz>
Date:   Mon May 9 23:28:26 2022 +0200

    Refactor of administration page and user registration
    re #9627

diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml
index ae0f936..1a5197d 100644
--- a/backend/docker-compose.yml
+++ b/backend/docker-compose.yml
@@ -19,7 +19,7 @@ services:
   app: # Spring boot application
     build: .
     container_name: app-backend  # name of the container
-    image: schwobik/backend-app:1.5
+    image: schwobik/backend-app:1.7
     ports:
       - "8080:8080"                 # expose port 8080 out of the docker container do the local machine
     depends_on:
@@ -31,7 +31,7 @@ services:
 #     Since our Dockerfile for web-server is located in react-app folder, our build context is ./react-app
     build: ../frontend
     container_name: frontend
-    image: schwobik/frontend-app:1.5
+    image: schwobik/frontend-app:1.7
     ports:
       - "80:80"
 
diff --git a/frontend/src/features/Administration/Administration.tsx b/frontend/src/features/Administration/Administration.tsx
index b27a2c7..8e66d61 100644
--- a/frontend/src/features/Administration/Administration.tsx
+++ b/frontend/src/features/Administration/Administration.tsx
@@ -1,94 +1,113 @@
 import {
     Button,
-    Container, Grid,
-    Link,
+    Grid,
     List,
     ListItem,
-    ListItemIcon,
     ListItemText,
-    NativeSelect,
     Paper,
-    Select, SelectChangeEvent
 } from '@mui/material'
-import {Fragment, ReactNode, useEffect, useState} from 'react'
+import {Fragment, useEffect, useState} from 'react'
 import { useDispatch, useSelector } from 'react-redux'
 import { RootState } from '../redux/store'
-import axiosInstance from "../../api/api"
 import 'react-quill/dist/quill.snow.css'
 import ContentLoading from "../Reusables/ContentLoading"
 import {UserDto} from "../../swagger/data-contracts"
 import UserDetail from "./UserDetail"
-import EditIcon from "@mui/icons-material/Edit"
 import {Link as RouterLink} from "react-router-dom"
 import AddIcon from '@mui/icons-material/Add';
-
-
+import {fetchUsers} from "./userDetailThunks"
+import {
+    clear,
+    consumeError,
+    setLoading,
+    setSelectedUser
+} from "./userDetailSlice"
+import ShowErrorIfPresent from "../Reusables/ShowErrorIfPresent"
+import {number} from "yup"
 
 const apiError =
     'Error while fetching data from the server, please try again later.'
 
 const Administration = () => {
-    const [users, setUsers] = useState<UserDto[] | undefined>(undefined)
-    const [selectedUser, setSelectedUser] = useState<UserDto | undefined>(undefined)
-    const [selectedUserID, setSelectedUserID] = useState<number | undefined>(undefined)
-    const [isContentLoading, setIsContentLoading] = useState(true)
-    const [err, setErr] = useState<string | undefined>(undefined)
+    // Items, loading and error from api
+    const users = useSelector((state: RootState) => state.usersDetail.users)
+    const loading = useSelector((state: RootState) => state.usersDetail.loading)
+    const apiError = useSelector((state: RootState) => state.usersDetail.error)
+    const selectedUser = useSelector((state: RootState) => state.usersDetail.selectedUser)
+    const [selectedUserId, setSelectedUserId] = useState<number | undefined>(undefined)
+
+    // Local state to display any error relevant error
+    const [displayError, setDisplayError] = useState<string | undefined>(
+        undefined
+    )
+
+    const dispatch = useDispatch()
 
     const isAdmin = useSelector(
         (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
     )
 
     const handleClick = (userID: number) => {
-        console.log("clicked " + userID)
+        console.log("clicked "+ userID)
         if (users) {
-            setSelectedUser(users.at(userID))
-            setSelectedUserID(userID)
+            setSelectedUserId(userID)
+            dispatch(setSelectedUser(users.at(userID)))
+            console.log("after " + selectedUser)
         }
     }
 
-    // Function to fetch the item from the api
-    const fetchItem = async () => {
-        try {
-            const { data, status } = await axiosInstance.get(
-                `/users`
-            )
-            if (status !== 200) {
-                setErr(apiError)
-                return
-            }
+    // Use effect to read the error and consume it
+    useEffect(() => {
+        if (users.length > 0) {
+            setSelectedUserId(0)
+            dispatch(setSelectedUser(users.at(0)))
+            console.log("selected id: " + selectedUser)
+        }
+    }, [users])
+
 
-            setUsers(data)
-            setIsContentLoading(false)
-        } catch (err: any) {
-            setErr(apiError)
+    // Use effect to read the error and consume it
+    useEffect(() => {
+        if (apiError) {
+            setDisplayError(apiError)
+            dispatch(consumeError())
         }
-    }
+    }, [apiError, dispatch])
 
     // Fetch the item from the api after mounting the component
     useEffect(() => {
-        fetchItem()
-    }, [])
+        console.log("before fetch")
+        dispatch(fetchUsers())
+        console.log("after fetch: " + loading)
+
+        return () => {
+            // Invalidate the state when unmounting so that the old list is not rerendered when the user returns to the page
+            dispatch(setLoading())
+            console.log("set loading: " + loading)
+        }
+    }, [dispatch])
 
     const childClosed = () => {
-        setSelectedUser(undefined)
-        setSelectedUserID(undefined)
-        fetchItem()
+        dispatch(clear())
+        dispatch(fetchUsers())
+        setSelectedUserId(0)
+        dispatch(setSelectedUser(users.at(0)))
     }
 
     return (
         <Fragment>
-
-            {isContentLoading && !err ? <ContentLoading /> : null}
-            {!isContentLoading && users && isAdmin ? (
+            <ShowErrorIfPresent err={displayError} />
+            {loading && !apiError ? <ContentLoading /> : null}
+            {!loading && users && isAdmin ? (
                 <Grid container justifyContent="space-around">
-                    <Grid item xs={3} md={2} sx={{ px: 2 }}>
+                    <Grid item xs={6} md={3} sx={{ px: 2 }}>
                         <Paper style={{ minHeight: '80vh', display:'flex', justifyContent: 'space-between', flexDirection:'column'}} variant="outlined">
                             <List>
                                 {users.map((user, id) => (
                                     <ListItem
                                         key={id}
                                         className={
-                                            selectedUserID === id
+                                            selectedUserId === id
                                                 ? 'clicked-user'
                                                 : ''
                                         }
@@ -108,9 +127,9 @@ const Administration = () => {
                             </Button>
                         </Paper>
                     </Grid>
-                    <Grid item md={10} xs={9}>
-                        {selectedUser ?
-                            <UserDetail user={selectedUser} onClose={childClosed} />
+                    <Grid item md={9} xs={6}>
+                        {selectedUserId !== undefined  ?
+                            <UserDetail user={selectedUser as UserDto} onClose={childClosed} />
                         : null }
                     </Grid>
                 </Grid>
diff --git a/frontend/src/features/Administration/UserDetail.tsx b/frontend/src/features/Administration/UserDetail.tsx
index 3731047..ab4fc49 100644
--- a/frontend/src/features/Administration/UserDetail.tsx
+++ b/frontend/src/features/Administration/UserDetail.tsx
@@ -1,28 +1,22 @@
 import {
     Button,
-    Container, Divider, Grid,
+    Grid,
     Checkbox,
-    Link,
-    List,
-    ListItem,
-    ListItemIcon,
-    ListItemText,
-    NativeSelect,
     Paper,
-    Select, SelectChangeEvent, Typography, FormGroup, FormControlLabel
+    Typography,
+    FormGroup,
+    FormControlLabel, Container
 } from '@mui/material'
-import React, {ChangeEvent, Fragment, ReactNode, useEffect, useState} from 'react'
-import {useDispatch, useSelector} from 'react-redux'
-import {RootState} from '../redux/store'
+import React, {Fragment, useEffect, useState} from 'react'
 import axiosInstance from "../../api/api"
 import 'react-quill/dist/quill.snow.css'
-import ContentLoading from "../Reusables/ContentLoading"
-import {Link as RouterLink, useNavigate} from "react-router-dom"
-import EditIcon from '@mui/icons-material/Edit'
-import {formatHtmlStringToReactDom} from "../../utils/formatting/HtmlUtils"
 import {PermissionDto, UserDto} from "../../swagger/data-contracts"
-import {Label} from "@mui/icons-material"
 import ClearIcon from "@mui/icons-material/Clear"
+import CheckIcon from '@mui/icons-material/Check';
+import {useDispatch, useSelector} from "react-redux"
+import {fetchUsers, savePermissions} from "./userDetailThunks"
+import {RootState} from "../redux/store"
+import {setSelectedUserPermissions} from "./userDetailSlice"
 
 export interface UserDetailProps {
     user: UserDto,
@@ -30,20 +24,28 @@ export interface UserDetailProps {
 }
 
 const UserDetail = (props: UserDetailProps) => {
-    const [user] = useState(props.user)
-    const [canRead, setCanRead] = useState(user.permissions?.canRead)
-    const [canWrite, setCanWrite] = useState(user.permissions?.canWrite)
-    const [canDelete, setCanDelete] = useState(user.permissions?.canDelete)
+    const selectedUser = useSelector((state: RootState) => state.usersDetail.selectedUser)
+    const [canRead, setCanRead] = useState(selectedUser?.permissions?.canRead)
+    const [canWrite, setCanWrite] = useState(selectedUser?.permissions?.canWrite)
+    const [canDelete, setCanDelete] = useState(selectedUser?.permissions?.canDelete)
 
+    const dispatch = useDispatch()
+
+    useEffect(()=> {
+        console.log("user updated:")
+        setCanRead(selectedUser?.permissions?.canRead)
+        setCanWrite(selectedUser?.permissions?.canWrite)
+        setCanDelete(selectedUser?.permissions?.canDelete)
+    }, [selectedUser]);
 
     // Maps user property to corresponding table row
     const mapToRow = (rowName: string, item: string) => (
         <Fragment>
             <Grid sx={{my: 2}} container justifyContent="space-around">
-                <Grid item xs={4} sx={{px: 1}}>
+                <Grid item xs={6} sx={{px: 1}}>
                     <Typography fontWeight={500}>{rowName}</Typography>
                 </Grid>
-                <Grid item xs={8} sx={{ml: 'auto'}}>
+                <Grid item xs={6} sx={{ml: 'auto'}}>
                     <Typography key={item}>{item}</Typography>
                 </Grid>
             </Grid>
@@ -57,7 +59,7 @@ const UserDetail = (props: UserDetailProps) => {
         },
         {
             rowName: 'E-mail:',
-            item: user?.email,
+            item: selectedUser?.email,
         }
     ]
     const rights = {
@@ -69,7 +71,7 @@ const UserDetail = (props: UserDetailProps) => {
 
     const deleteUser = async () => {
         const { data, status } = await axiosInstance.delete(
-            `/users/${user.email}`
+            `/users/${selectedUser?.email}`
         )
         if (status !== 200) {
             // TODO dodělat zpracování erroru
@@ -78,11 +80,17 @@ const UserDetail = (props: UserDetailProps) => {
         props.onClose();
     }
 
+    const prepareSavePermissions = () => {
+        dispatch(setSelectedUserPermissions({canRead: canRead, canWrite: canWrite, canDelete: canDelete}))
+        dispatch(savePermissions())
+        dispatch(fetchUsers())
+    }
+
     return (
         <Fragment>
             <Paper style={{minHeight: '80vh'}} variant="outlined">
                 <Grid container justifyContent="space-around">
-                    <Grid item xs={6} md={6} sx={{p: 2}}>
+                    <Grid item xs={12} md={6} sx={{p: 2}}>
                         <Typography fontWeight={800}>Information</Typography>
                         {rows.map((row) => (
 
@@ -94,33 +102,40 @@ const UserDetail = (props: UserDetailProps) => {
                             </Fragment>
                         ))}
                     </Grid>
-                    <Grid item xs={6} md={6} sx={{p: 2}}>
-                        <b>Rights</b>
-                            <FormGroup>
-                                <FormControlLabel
-                                    control={
-                                        <Checkbox
-                                            checked={canWrite}
-                                            onChange={e => setCanWrite(e.target.checked)}
-                                        />}
-                                    label={rights.canWrite}/>
-                                <FormControlLabel
-                                    control={
-                                        <Checkbox
-                                            checked={canRead}
-                                            onChange={e => setCanRead(e.target.checked)}
-                                        />}
-                                    label={rights.canRead}/>
-                                <FormControlLabel
-                                    control={
-                                        <Checkbox
-                                            checked={canDelete}
-                                            onChange={e => setCanDelete(e.target.checked)}
-                                        />}
-                                    label={rights.canDelete}/>
-                            </FormGroup>
+                    <Grid item xs={12} md={6} sx={{p: 2}}>
+                        <Typography fontWeight={800}>Rights</Typography>
+                        <FormGroup>
+                            <FormControlLabel
+                                control={
+                                    <Checkbox
+                                        checked={canWrite}
+                                        onChange={e => setCanWrite(e.target.checked)}
+                                    />}
+                                label={rights.canWrite}/>
+                            <FormControlLabel
+                                control={
+                                    <Checkbox
+                                        checked={canRead}
+                                        onChange={e => setCanRead(e.target.checked)}
+                                    />}
+                                label={rights.canRead}/>
+                            <FormControlLabel
+                                control={
+                                    <Checkbox
+                                        checked={canDelete}
+                                        onChange={e => setCanDelete(e.target.checked)}
+                                    />}
+                                label={rights.canDelete}/>
+                        </FormGroup>
                     </Grid>
                 </Grid>
+                <Button startIcon={<CheckIcon />}
+                        variant="contained"
+                        color="primary"
+                        onClick={prepareSavePermissions}
+                        sx={{ m: 2 }} >
+                    Save
+                </Button>
                 <Button startIcon={<ClearIcon />}
                         variant="contained"
                         color="primary"
diff --git a/frontend/src/features/Administration/userDetailSlice.tsx b/frontend/src/features/Administration/userDetailSlice.tsx
new file mode 100644
index 0000000..247df15
--- /dev/null
+++ b/frontend/src/features/Administration/userDetailSlice.tsx
@@ -0,0 +1,64 @@
+import { createSlice } from '@reduxjs/toolkit'
+import {PermissionDto, UserDto} from '../../swagger/data-contracts'
+import {fetchUsers} from './userDetailThunks'
+import {number} from "yup"
+
+export interface UsersDetailState {
+    users: UserDto[] // list of all fetched items
+    loading: boolean // whether the users are loading
+    error?: string,
+    selectedUser?: UserDto,
+    permissions?: PermissionDto
+}
+
+const initialState: UsersDetailState = {
+    users: [],
+    loading: true,
+    error: undefined,
+    selectedUser: undefined,
+    permissions: undefined
+}
+
+const usersDetailSlice = createSlice({
+    name: 'usersDetail',
+    initialState,
+    reducers: {
+        clear: (state) => ({ ...initialState }),
+        setLoading: (state) => ({ ...state, loading: true }),
+        consumeError: (state) => ({ ...state, error: undefined }),
+        setSelectedUser: (state, action) => ({
+            ...state,
+            selectedUser: action.payload
+        }),
+        setSelectedUserPermissions: (state, action) => ({
+            ...state,
+            permissions: action.payload
+        }),
+    },
+    extraReducers: (builder) => {
+        builder.addCase(fetchUsers.pending, (state) => ({
+            ...state,
+            loading: true,
+        }))
+        builder.addCase(fetchUsers.fulfilled, (state, action) => ({
+            ...state,
+            users: action.payload,
+            loading: false,
+        }))
+        builder.addCase(fetchUsers.rejected, (state, action) => ({
+            ...state,
+            loading: false,
+            error: action.error.message as string,
+        }))
+    },
+})
+
+export const {
+    clear,
+    setLoading,
+    consumeError,
+    setSelectedUser,
+    setSelectedUserPermissions,
+} = usersDetailSlice.actions
+const usersDetailReducer = usersDetailSlice.reducer
+export default usersDetailReducer
diff --git a/frontend/src/features/Administration/userDetailThunks.tsx b/frontend/src/features/Administration/userDetailThunks.tsx
new file mode 100644
index 0000000..02d4c73
--- /dev/null
+++ b/frontend/src/features/Administration/userDetailThunks.tsx
@@ -0,0 +1,61 @@
+import { createAsyncThunk } from '@reduxjs/toolkit'
+import axiosInstance from '../../api/api'
+import {UsersDetailState} from "./userDetailSlice"
+
+
+const apiError = 'Error, server is currently unavailable.'
+
+export const fetchUsers = createAsyncThunk(
+    'usersDetail/fetchUsers',
+    async (dispatch, { getState }) => {
+        try {
+            console.log("fetch users called")
+            const {data, status} = await axiosInstance.get(
+                `/users`
+            )
+            if (status === 200) {
+                console.log(data)
+                return data
+            }
+            console.log("not returning data: " + data)
+            return Promise.reject(apiError)
+        } catch (err: any) {
+            return Promise.reject(apiError)
+        }
+    }
+)
+
+export const savePermissions = createAsyncThunk(
+    'usersDetail/savePermissions',
+    async (dispatch, { getState }) => {
+        try {
+            const { usersDetail } = getState() as { usersDetail: UsersDetailState }
+            console.log("savePermissions called")
+
+            const selectedUser = usersDetail.selectedUser
+            if (selectedUser === undefined) {
+                return Promise.reject("User is undefined")
+            }
+
+            const selectedUserPermissions = usersDetail.permissions
+            if (selectedUserPermissions === undefined) {
+                return Promise.reject("Permissions is undefined")
+            }
+
+            const selectedUserIdentifier = selectedUser.email
+
+            const {data, status} = await axiosInstance.patch(
+                `/users/${selectedUserIdentifier}/permissions`,
+                selectedUserPermissions
+            )
+            if (status === 200) {
+                console.log("returning data: " + data)
+                return data
+            }
+            console.log("not returning data: " + data)
+            return Promise.reject(apiError)
+        } catch (err: any) {
+            return Promise.reject(apiError)
+        }
+    }
+)
diff --git a/frontend/src/features/redux/store.ts b/frontend/src/features/redux/store.ts
index ea11fb7..d2e194d 100644
--- a/frontend/src/features/redux/store.ts
+++ b/frontend/src/features/redux/store.ts
@@ -7,6 +7,7 @@ import catalogReducer from '../Catalog/catalogSlice'
 import { composeWithDevTools } from 'redux-devtools-extension'
 import notificationReducer from '../Notification/notificationSlice'
 import trackingToolReducer from '../TrackingTool/trackingToolSlice'
+import usersDetailReducer from '../Administration/userDetailSlice'
 import { enableMapSet } from 'immer'
 
 enableMapSet()
@@ -20,7 +21,8 @@ const store = createStore(
         theme: themeReducer,
         catalog: catalogReducer,
         notification: notificationReducer,
-        trackingTool: trackingToolReducer
+        trackingTool: trackingToolReducer,
+        usersDetail: usersDetailReducer,
     }),
     process.env.REACT_APP_DEV_ENV === 'true'
         ? composeEnhancers( // ComposeEnhancers will inject redux-devtools-extension
