commit 8c57f95879d326de7d1c65c32232a7d0ec869ca0
Author: Vaclav Honzik <vaclavhonzik98@gmail.com>
Date:   Sat May 7 15:27:54 2022 +0200

    refactor to use Redux instead of localstate
    
    re #9629

diff --git a/frontend/src/features/Auth/userSlice.ts b/frontend/src/features/Auth/userSlice.ts
index 8b6b395..bd1baee 100644
--- a/frontend/src/features/Auth/userSlice.ts
+++ b/frontend/src/features/Auth/userSlice.ts
@@ -28,9 +28,7 @@ const initialState: UserState = {
 
 export const userSlice = createSlice({
     name: 'user', // name to generate action types
-
     initialState, // default state
-
     // Reducers that update the state
     reducers: {
         logout: () => initialState, // Reset to the inital state
@@ -57,7 +55,7 @@ export const userSlice = createSlice({
                 return { ...state, lastErr: action.error.message }
             }
         })
-        builder.addCase(logIn.pending, (state, action) => {
+        builder.addCase(logIn.pending, (state) => {
             return { ...state, isLoggingIn: true }
         })
     },
@@ -65,6 +63,7 @@ export const userSlice = createSlice({
 
 const userReducer = persistReducer(persistConfig, userSlice.reducer)
 
-export const { logout, refreshTokens, setErr, setUserState, resetLoggingIn } = userSlice.actions
+export const { logout, refreshTokens, setErr, setUserState, resetLoggingIn } =
+    userSlice.actions
 
 export default userReducer
diff --git a/frontend/src/features/TrackingTool/FileUpload.tsx b/frontend/src/features/TrackingTool/FileUpload.tsx
index 6f9d081..4e3ef2c 100644
--- a/frontend/src/features/TrackingTool/FileUpload.tsx
+++ b/frontend/src/features/TrackingTool/FileUpload.tsx
@@ -22,26 +22,13 @@ const FileUpload = () => {
         file: yup.mixed().required('File is required'),
     })
 
-    const [submitButtonEnabled, setSubmitButtonEnabled] = useState(true)
-
     const formik = useFormik({
         initialValues: {
             file: undefined,
         },
         validationSchema,
         onSubmit: async (values) => {
-            // TODO actually send the file somewhere
-            // TODO implement me
-
-            const formData = new FormData()
-            // @ts-ignore for now
-            formData.append('file', values.file as File)
-
-            const { data } = await axiosInstance.post('/path', formData, {
-                headers: {
-                    'Content-Type': 'multipart/form-data',
-                },
-            })
+            
         },
     })
 
diff --git a/frontend/src/features/TrackingTool/MapPath.tsx b/frontend/src/features/TrackingTool/MapPath.tsx
index 5b511d9..3985945 100644
--- a/frontend/src/features/TrackingTool/MapPath.tsx
+++ b/frontend/src/features/TrackingTool/MapPath.tsx
@@ -1,4 +1,4 @@
-import { Fragment, FunctionComponent, useState } from 'react'
+import { Fragment, FunctionComponent, useEffect, useState } from 'react'
 import { CatalogItemDto } from '../../swagger/data-contracts'
 import { PathVariant } from './buildPathVariants'
 import TextPath from 'react-leaflet-textpath'
@@ -6,6 +6,8 @@ import { Marker, Popup } from 'react-leaflet'
 import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material'
 import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils'
 import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../Catalog/CatalogItemDetail'
+import { useDispatch, useSelector } from 'react-redux'
+import { RootState } from '../redux/store'
 
 // CatalogItemDto wrapper to keep track whether the item is active or not
 class DisplayableMapPoint {
@@ -17,10 +19,7 @@ class DisplayableMapPoint {
 
 export interface MapPathProps {
     pathVariant: PathVariant // aka CatalogItemDto[]
-    active: boolean // whether this component is active
     idx: number // index of path in the list
-    primaryIdx: number // reference to index of path which has primary color
-    setPrimary: (idx: number) => void // callback to set the primary path
 }
 
 // Blue
@@ -29,24 +28,34 @@ export const primaryPathColor = '#346eeb'
 // Grey
 export const secondaryPathColor = '#878e9c'
 
-// Map path component 
-const MapPath: FunctionComponent<MapPathProps> = ({
-    pathVariant,
-    active,
-    primaryIdx,
-    idx,
-    setPrimary
-}) => {
+// Map path component
+const MapPath: FunctionComponent<MapPathProps> = ({ idx, pathVariant }) => {
     // List of all map points that belong to the path
     const [mapPoints, setMapPoints] = useState<DisplayableMapPoint[]>(
         pathVariant
             .filter((item) => item.latitude && item.longitude)
             .map((item) => new DisplayableMapPoint(item))
     )
-    
+
+    // Set of all active paths
+    const activePaths = useSelector(
+        (state: RootState) => state.trackingTool.activePaths
+    )
+    // Index of the primary path
+    const primaryPathIdx = useSelector(
+        (state: RootState) => state.trackingTool.primaryPathIdx
+    )
+
+    // Whether the path is active or not
+    const [active, setActive] = useState(false)
+    useEffect(() => {
+        setActive(activePaths.has(idx))
+    }, [activePaths, idx])
 
     const getActiveMapPoints = () => mapPoints.filter((item) => item.active)
 
+    const dispatch = useDispatch()
+
     // Builds all edges of the path
     const buildEdges = () => {
         const activeMapPoints = getActiveMapPoints()
@@ -73,13 +82,14 @@ const MapPath: FunctionComponent<MapPathProps> = ({
                     attributes={{
                         'font-size': 25,
                         // Set to primaryPathColor if primary index in the tracking tool is equal to this index
-                        fill: primaryIdx === idx ? primaryPathColor : secondaryPathColor,
+                        fill:
+                            primaryPathIdx === idx
+                                ? primaryPathColor
+                                : secondaryPathColor,
                     }}
                     onClick={() => {
-                        setPrimary(idx)
-                        console.log('hehehe')
-                    }
-                    }
+                        dispatch(setPrimaryIdx(idx))
+                    }}
                     repeat
                     center
                     weight={9}
@@ -111,7 +121,9 @@ const MapPath: FunctionComponent<MapPathProps> = ({
                                 fontWeight="bold"
                                 fontSize={16}
                             >
-                                {formatHtmlStringToReactDom(mapPoint.catalogItem.name as string)}
+                                {formatHtmlStringToReactDom(
+                                    mapPoint.catalogItem.name as string
+                                )}
                             </Typography>
                             <FormControlLabel
                                 control={
@@ -126,7 +138,9 @@ const MapPath: FunctionComponent<MapPathProps> = ({
                                 labelPlacement="end"
                                 label="Active"
                             />
-                            <CatalogItemDetailDialog itemId={mapPoint.catalogItem.id ?? ''} />
+                            <CatalogItemDetailDialog
+                                itemId={mapPoint.catalogItem.id ?? ''}
+                            />
                         </Stack>
                     </Fragment>
                 </Popup>
@@ -136,12 +150,17 @@ const MapPath: FunctionComponent<MapPathProps> = ({
 
     return (
         <Fragment>
-            {active && <Fragment>
-                {buildVertices()}
-                {buildEdges()}
-                </Fragment>}
+            {active && (
+                <Fragment>
+                    {buildVertices()}
+                    {buildEdges()}
+                </Fragment>
+            )}
         </Fragment>
     )
 }
 
 export default MapPath
+function setPrimaryIdx(idx: number): any {
+    throw new Error('Function not implemented.')
+}
diff --git a/frontend/src/features/TrackingTool/PlaintextUpload.tsx b/frontend/src/features/TrackingTool/PlaintextUpload.tsx
index 210d0e3..1e05016 100644
--- a/frontend/src/features/TrackingTool/PlaintextUpload.tsx
+++ b/frontend/src/features/TrackingTool/PlaintextUpload.tsx
@@ -7,65 +7,43 @@ import {
     TextField,
 } from '@mui/material'
 import { useFormik } from 'formik'
-import { Fragment, FunctionComponent, useState } from 'react'
+import { Fragment, FunctionComponent, useEffect, useState } from 'react'
 import SendIcon from '@mui/icons-material/Send'
 import ClearIcon from '@mui/icons-material/Clear'
 import { PathDto } from '../../swagger/data-contracts'
 import axiosInstance from '../../api/api'
-import { useDispatch } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
 import { showNotification } from '../Notification/notificationSlice'
+import { RootState } from '../redux/store'
+import { sendTextForProcessing } from './trackingToolThunks'
+import * as yup from 'yup'
+import { resetDialogApiCallSuccess } from './trackingToolSlice'
 
-export interface PlaintextUploadProps {
-    setPaths: React.Dispatch<React.SetStateAction<PathDto | undefined>>
-}
-
-const PlaintextUpload: FunctionComponent<PlaintextUploadProps> = ({
-    setPaths,
-}) => {
-    const [submitButtonDisabled, setSubmitButtonDisabled] = useState(false)
+const PlaintextUpload = () => {
+    const loading = useSelector(
+        (state: RootState) => state.trackingTool.isLoading
+    )
     const [open, setOpen] = useState(false) // controls whether the dialog is open
 
+    // This controls whether to keep dialog open after sending the request to the API
+    const dialogApiCallSuccess = useSelector(
+        (state: RootState) => state.trackingTool.dialogApiCallSuccess
+    )
+
     const dispatch = useDispatch()
 
+    const validationSchema = yup.object().shape({
+        text: yup.mixed().required('Text is required'),
+    })
+
     const formik = useFormik({
         initialValues: {
             text: '',
         },
+        validationSchema,
         onSubmit: async () => {
-            let closeDialog = false
-            setSubmitButtonDisabled(true)
-            try {
-                const { data, status } = await axiosInstance.post('path', {
-                    text: formik.values.text,
-                })
-
-                if (status !== 200) {
-                    dispatch(
-                        showNotification({
-                            message:
-                                'Error while fetching map, please try again later.',
-                            severity: 'error',
-                        })
-                    )
-                } else {
-                    dispatch(
-                        showNotification({
-                            message: 'Map fetched successfully.',
-                            severity: 'success',
-                        })
-                    )
-                    setPaths(data)
-                    closeDialog = true
-                }
-            } catch (err: any) {
-                dispatch(showNotification)
-                closeDialog = true
-            }
-            setSubmitButtonDisabled(false)
-
-            if (closeDialog) {
-                onCloseDialog()
-            }
+            // Dispatch the thunk
+            dispatch(sendTextForProcessing(formik.values.text))
         },
     })
 
@@ -78,6 +56,14 @@ const PlaintextUpload: FunctionComponent<PlaintextUploadProps> = ({
         formik.resetForm()
     }
 
+    useEffect(() => {
+        if (!dialogApiCallSuccess) {
+            return
+        }
+        dispatch(resetDialogApiCallSuccess())
+        setOpen(false)
+    }, [dialogApiCallSuccess, dispatch])
+
     return (
         <Fragment>
             <Button variant="contained" onClick={() => setOpen(true)}>
@@ -119,8 +105,8 @@ const PlaintextUpload: FunctionComponent<PlaintextUploadProps> = ({
                             <Button
                                 type="submit"
                                 variant="contained"
-                                disabled={submitButtonDisabled}
                                 startIcon={<SendIcon />}
+                                disabled={loading}
                             >
                                 Submit
                             </Button>
diff --git a/frontend/src/features/TrackingTool/ProcessedTextCard.tsx b/frontend/src/features/TrackingTool/ProcessedTextCard.tsx
new file mode 100644
index 0000000..ab7827d
--- /dev/null
+++ b/frontend/src/features/TrackingTool/ProcessedTextCard.tsx
@@ -0,0 +1,12 @@
+import { PathDto } from "../../swagger/data-contracts"
+
+export interface ProcessedtextCardProps {
+    pathDto?: PathDto
+    
+}
+
+const ProcessedTextCard = () => {
+
+}
+
+export default ProcessedTextCard
diff --git a/frontend/src/features/TrackingTool/TrackingTool.tsx b/frontend/src/features/TrackingTool/TrackingTool.tsx
index 2d1e293..f1c3e51 100644
--- a/frontend/src/features/TrackingTool/TrackingTool.tsx
+++ b/frontend/src/features/TrackingTool/TrackingTool.tsx
@@ -1,6 +1,13 @@
-import { Card, CardContent, Grid, Stack, Typography } from '@mui/material'
+import {
+    Button,
+    Card,
+    CardContent,
+    Grid,
+    Stack,
+    Typography,
+} from '@mui/material'
 import { Fragment, useEffect, useState } from 'react'
-import { MapContainer, TileLayer } from 'react-leaflet'
+import { MapContainer, TileLayer, useMap } from 'react-leaflet'
 import mapConfig from '../../config/mapConfig'
 import TextPath from 'react-leaflet-textpath'
 import PlaintextUpload from './PlaintextUpload'
@@ -9,64 +16,25 @@ import L from 'leaflet'
 import DeleteIcon from '@mui/icons-material/Delete'
 import { PathDto } from '../../swagger/data-contracts'
 import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils'
-import buildPathVariants, { PathVariant } from './buildPathVariants'
 import MapPath from './MapPath'
+import EditIcon from '@mui/icons-material/Edit'
+import { useSelector } from 'react-redux'
+import { RootState } from '../redux/store'
 
 // Page with tracking tool
 const TrackingTool = () => {
     // Path response from the API
-    const [pathDto, setPathDto] = useState<PathDto | undefined>(undefined)
+    const pathDto = useSelector((state: RootState) => state.trackingTool.pathDto)
+    const pathVariants = useSelector((state: RootState) => state.trackingTool.pathVariants)
+    const mapCenter = useSelector((state: RootState) => state.trackingTool.mapCenter)
 
-    // Whether the path variants are building
-    const [buildingPathVariants, setBuildingPathVariants] =
-        useState<boolean>(false)
+    // const map = useMap()
 
-    // List of all computed path variants
-    const [pathVariants, setPathVariants] = useState<PathVariant[] | undefined>(
-        undefined
-    )
-
-    // latitude longitude pair of where the map is to be centered
-    const [mapCenter, setMapCenter] = useState<number[]>([
-        mapConfig.defaultCoordinates[0],
-        mapConfig.defaultCoordinates[1],
-    ])
-
-    const [primaryPathIdx, setActivePathIdx] = useState(0)
-
-    // TODO make some sort of selection list to select active paths?
-    const [activePaths, setActivePaths] = useState<Set<number>>(new Set())
+    // // Set the map center
+    // useEffect(() => {
+    //     map.flyTo(mapCenter, mapConfig.defaultZoom)
+    // }, [map, mapCenter])
 
-    // Returns tuple of average latitude and longitude
-    const calculateMapCenter = (pathVariant: PathVariant) => [
-        pathVariant
-            .map((item) => item.latitude ?? 0)
-            .reduce((a, b) => a + b, 0) / pathVariant.length,
-        pathVariant
-            .map((item) => item.longitude ?? 0)
-            .reduce((a, b) => a + b, 0) / pathVariant.length,
-    ]
-
-    useEffect(() => {
-        if (!pathVariants || pathVariants.length === 0) {
-            return
-        }
-        // TODO calculate only for path that has some non-null coordinates
-        setMapCenter(calculateMapCenter(pathVariants[0]))
-    }, [pathVariants])
-
-    useEffect(() => {
-        if (!pathDto) {
-            return
-        }
-
-        setBuildingPathVariants(true)
-        buildPathVariants(pathDto).then((pathVariants) => {
-            setActivePaths(new Set(pathVariants.map((_, idx) => idx)))
-            setPathVariants(pathVariants)
-            setBuildingPathVariants(false)
-        })
-    }, [pathDto])
 
     return (
         <Fragment>
@@ -92,6 +60,9 @@ const TrackingTool = () => {
                                     )}
                                 </Typography>
                             </Stack>
+                            <Stack justifyItems="flex-end" alignSelf="flex-end" alignItems="flex-end">
+                                <Button size="small" variant="outlined" startIcon={<EditIcon />}>Edit</Button>
+                            </Stack>
                         </CardContent>
                     </Card>
                 </Fragment>
@@ -123,7 +94,7 @@ const TrackingTool = () => {
                         >
                             {pathDto ? 'Update path' : 'Show Path'}
                         </Typography>
-                        <PlaintextUpload setPaths={setPathDto} />
+                        <PlaintextUpload />
                         <FileUpload />
                     </Stack>
                 </Grid>
@@ -153,9 +124,6 @@ const TrackingTool = () => {
                                     key={idx}
                                     pathVariant={pathVariant}
                                     idx={idx}
-                                    setPrimary={setActivePathIdx}
-                                    primaryIdx={primaryPathIdx}
-                                    active={activePaths.has(idx)}
                                 />
                             ))}
                     </MapContainer>
diff --git a/frontend/src/features/TrackingTool/buildPathVariants.ts b/frontend/src/features/TrackingTool/buildPathVariants.ts
index 09a8111..939c485 100644
--- a/frontend/src/features/TrackingTool/buildPathVariants.ts
+++ b/frontend/src/features/TrackingTool/buildPathVariants.ts
@@ -14,7 +14,7 @@ const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] =>
         [[]]
     )
 
-export const buildPathVariants = async (pathDto: PathDto): Promise<PathVariant[]> => {
+export const buildPathVariants = (pathDto: PathDto): PathVariant[] => {
     if (!pathDto.foundCatalogItems) {
         return []
     }
diff --git a/frontend/src/features/TrackingTool/trackingToolSlice.ts b/frontend/src/features/TrackingTool/trackingToolSlice.ts
new file mode 100644
index 0000000..1e2c837
--- /dev/null
+++ b/frontend/src/features/TrackingTool/trackingToolSlice.ts
@@ -0,0 +1,89 @@
+import { createSlice } from '@reduxjs/toolkit'
+import { LatLngTuple } from 'leaflet'
+import mapConfig from '../../config/mapConfig'
+import { PathDto } from '../../swagger/data-contracts'
+import buildPathVariants, { PathVariant } from './buildPathVariants'
+import { sendTextForProcessing } from './trackingToolThunks'
+
+export interface TrackingToolState {
+    isLoading: boolean // whether the data is being loaded
+    pathDto?: PathDto // the data
+    pathVariants?: PathVariant[] // undefined signals that no path variants were yet fetched from the API
+    lastErr?: string // consumable for errors during thunks
+    mapCenter: LatLngTuple // pair of latitude and longitude
+    primaryPathIdx: number // index of the primary path
+    activePaths: Set<number> // indices of the active paths
+    // trigger to close the dialog when API call is finished
+    dialogApiCallSuccess: boolean
+}
+
+const initialState: TrackingToolState = {
+    isLoading: false,
+    mapCenter: [
+        mapConfig.defaultCoordinates[0],
+        mapConfig.defaultCoordinates[1],
+    ],
+    primaryPathIdx: 0,
+    activePaths: new Set(),
+    dialogApiCallSuccess: true,
+}
+
+// Returns tuple of average latitude and longitude
+const calculateMapCenter = (pathVariant: PathVariant): LatLngTuple => [
+    pathVariant.map((item) => item.latitude ?? 0).reduce((a, b) => a + b, 0) /
+        pathVariant.length,
+    pathVariant.map((item) => item.longitude ?? 0).reduce((a, b) => a + b, 0) /
+        pathVariant.length,
+]
+
+export const trackingToolSlice = createSlice({
+    name: 'trackingTool',
+    initialState,
+    reducers: {
+        consumeErr: (state) => ({ ...state, lastErr: undefined }),
+        setPrimaryIdx: (state, action) => ({
+            ...state,
+            primaryPathIdx: action.payload,
+        }),
+        resetDialogApiCallSuccess: (state) => ({
+            ...state,
+            dialogApiCallSuccess: false,
+        }),
+    },
+    extraReducers: (builder) => {
+        builder.addCase(sendTextForProcessing.fulfilled, (state, action) => {
+            const dto: PathDto = action.payload
+            const pathVariants = buildPathVariants(dto)
+            return {
+                ...state,
+                pathVariants,
+                // TODO map this correctly
+                activePaths: new Set(pathVariants.map((_, idx) => idx)),
+                // TODO calculate correctly
+                mapCenter:
+                    pathVariants.length > 0
+                        ? calculateMapCenter(pathVariants[0])
+                        : (state.mapCenter as LatLngTuple),
+                isLoading: false,
+                dialogApiCallSuccess: true,
+            }
+        })
+        builder.addCase(sendTextForProcessing.rejected, (state, action) => ({
+            ...initialState,
+            lastErr: action.error.message,
+            isLoading: false,
+            dialogApiCallSuccess: false,
+        }))
+        builder.addCase(sendTextForProcessing.pending, (state) => {
+            return {
+                ...state,
+                isLoading: true,
+                dialogApiCallSuccess: false,
+            }
+        })
+    },
+})
+
+export const { consumeErr, setPrimaryIdx, resetDialogApiCallSuccess } = trackingToolSlice.actions
+const trackingToolReducer = trackingToolSlice.reducer
+export default trackingToolReducer
diff --git a/frontend/src/features/TrackingTool/trackingToolThunks.ts b/frontend/src/features/TrackingTool/trackingToolThunks.ts
new file mode 100644
index 0000000..7d76e50
--- /dev/null
+++ b/frontend/src/features/TrackingTool/trackingToolThunks.ts
@@ -0,0 +1,19 @@
+import { createAsyncThunk } from '@reduxjs/toolkit'
+import axiosInstance from '../../api/api'
+
+export const sendTextForProcessing = createAsyncThunk(
+    'trackingTool/sendTextForProcessing',
+    async (text: string) => {
+        try {
+            const { data, status } = await axiosInstance.post('path', { text })
+            if (status !== 200) {
+                return Promise.reject(
+                    'Error while fetching map, please try again later'
+                )
+            }
+            return data
+        } catch (err: any) {
+            return Promise.reject('Error, server is currently unavailable')
+        }
+    }
+)
diff --git a/frontend/src/features/redux/store.ts b/frontend/src/features/redux/store.ts
index e3297b6..ea11fb7 100644
--- a/frontend/src/features/redux/store.ts
+++ b/frontend/src/features/redux/store.ts
@@ -6,6 +6,10 @@ import themeReducer from '../Theme/themeSlice'
 import catalogReducer from '../Catalog/catalogSlice'
 import { composeWithDevTools } from 'redux-devtools-extension'
 import notificationReducer from '../Notification/notificationSlice'
+import trackingToolReducer from '../TrackingTool/trackingToolSlice'
+import { enableMapSet } from 'immer'
+
+enableMapSet()
 
 const composeEnhancers = composeWithDevTools({})
 
@@ -15,7 +19,8 @@ const store = createStore(
         user: userReducer,
         theme: themeReducer,
         catalog: catalogReducer,
-        notification: notificationReducer
+        notification: notificationReducer,
+        trackingTool: trackingToolReducer
     }),
     process.env.REACT_APP_DEV_ENV === 'true'
         ? composeEnhancers( // ComposeEnhancers will inject redux-devtools-extension
