Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 8c57f958

Přidáno uživatelem Václav Honzík před asi 2 roky(ů)

refactor to use Redux instead of localstate

re #9629

Zobrazit rozdíly:

frontend/src/features/Auth/userSlice.ts
28 28

  
29 29
export const userSlice = createSlice({
30 30
    name: 'user', // name to generate action types
31

  
32 31
    initialState, // default state
33

  
34 32
    // Reducers that update the state
35 33
    reducers: {
36 34
        logout: () => initialState, // Reset to the inital state
......
57 55
                return { ...state, lastErr: action.error.message }
58 56
            }
59 57
        })
60
        builder.addCase(logIn.pending, (state, action) => {
58
        builder.addCase(logIn.pending, (state) => {
61 59
            return { ...state, isLoggingIn: true }
62 60
        })
63 61
    },
......
65 63

  
66 64
const userReducer = persistReducer(persistConfig, userSlice.reducer)
67 65

  
68
export const { logout, refreshTokens, setErr, setUserState, resetLoggingIn } = userSlice.actions
66
export const { logout, refreshTokens, setErr, setUserState, resetLoggingIn } =
67
    userSlice.actions
69 68

  
70 69
export default userReducer
frontend/src/features/TrackingTool/FileUpload.tsx
22 22
        file: yup.mixed().required('File is required'),
23 23
    })
24 24

  
25
    const [submitButtonEnabled, setSubmitButtonEnabled] = useState(true)
26

  
27 25
    const formik = useFormik({
28 26
        initialValues: {
29 27
            file: undefined,
30 28
        },
31 29
        validationSchema,
32 30
        onSubmit: async (values) => {
33
            // TODO actually send the file somewhere
34
            // TODO implement me
35

  
36
            const formData = new FormData()
37
            // @ts-ignore for now
38
            formData.append('file', values.file as File)
39

  
40
            const { data } = await axiosInstance.post('/path', formData, {
41
                headers: {
42
                    'Content-Type': 'multipart/form-data',
43
                },
44
            })
31
            
45 32
        },
46 33
    })
47 34

  
frontend/src/features/TrackingTool/MapPath.tsx
1
import { Fragment, FunctionComponent, useState } from 'react'
1
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
2 2
import { CatalogItemDto } from '../../swagger/data-contracts'
3 3
import { PathVariant } from './buildPathVariants'
4 4
import TextPath from 'react-leaflet-textpath'
......
6 6
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material'
7 7
import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils'
8 8
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../Catalog/CatalogItemDetail'
9
import { useDispatch, useSelector } from 'react-redux'
10
import { RootState } from '../redux/store'
9 11

  
10 12
// CatalogItemDto wrapper to keep track whether the item is active or not
11 13
class DisplayableMapPoint {
......
17 19

  
18 20
export interface MapPathProps {
19 21
    pathVariant: PathVariant // aka CatalogItemDto[]
20
    active: boolean // whether this component is active
21 22
    idx: number // index of path in the list
22
    primaryIdx: number // reference to index of path which has primary color
23
    setPrimary: (idx: number) => void // callback to set the primary path
24 23
}
25 24

  
26 25
// Blue
......
29 28
// Grey
30 29
export const secondaryPathColor = '#878e9c'
31 30

  
32
// Map path component 
33
const MapPath: FunctionComponent<MapPathProps> = ({
34
    pathVariant,
35
    active,
36
    primaryIdx,
37
    idx,
38
    setPrimary
39
}) => {
31
// Map path component
32
const MapPath: FunctionComponent<MapPathProps> = ({ idx, pathVariant }) => {
40 33
    // List of all map points that belong to the path
41 34
    const [mapPoints, setMapPoints] = useState<DisplayableMapPoint[]>(
42 35
        pathVariant
43 36
            .filter((item) => item.latitude && item.longitude)
44 37
            .map((item) => new DisplayableMapPoint(item))
45 38
    )
46
    
39

  
40
    // Set of all active paths
41
    const activePaths = useSelector(
42
        (state: RootState) => state.trackingTool.activePaths
43
    )
44
    // Index of the primary path
45
    const primaryPathIdx = useSelector(
46
        (state: RootState) => state.trackingTool.primaryPathIdx
47
    )
48

  
49
    // Whether the path is active or not
50
    const [active, setActive] = useState(false)
51
    useEffect(() => {
52
        setActive(activePaths.has(idx))
53
    }, [activePaths, idx])
47 54

  
48 55
    const getActiveMapPoints = () => mapPoints.filter((item) => item.active)
49 56

  
57
    const dispatch = useDispatch()
58

  
50 59
    // Builds all edges of the path
51 60
    const buildEdges = () => {
52 61
        const activeMapPoints = getActiveMapPoints()
......
73 82
                    attributes={{
74 83
                        'font-size': 25,
75 84
                        // Set to primaryPathColor if primary index in the tracking tool is equal to this index
76
                        fill: primaryIdx === idx ? primaryPathColor : secondaryPathColor,
85
                        fill:
86
                            primaryPathIdx === idx
87
                                ? primaryPathColor
88
                                : secondaryPathColor,
77 89
                    }}
78 90
                    onClick={() => {
79
                        setPrimary(idx)
80
                        console.log('hehehe')
81
                    }
82
                    }
91
                        dispatch(setPrimaryIdx(idx))
92
                    }}
83 93
                    repeat
84 94
                    center
85 95
                    weight={9}
......
111 121
                                fontWeight="bold"
112 122
                                fontSize={16}
113 123
                            >
114
                                {formatHtmlStringToReactDom(mapPoint.catalogItem.name as string)}
124
                                {formatHtmlStringToReactDom(
125
                                    mapPoint.catalogItem.name as string
126
                                )}
115 127
                            </Typography>
116 128
                            <FormControlLabel
117 129
                                control={
......
126 138
                                labelPlacement="end"
127 139
                                label="Active"
128 140
                            />
129
                            <CatalogItemDetailDialog itemId={mapPoint.catalogItem.id ?? ''} />
141
                            <CatalogItemDetailDialog
142
                                itemId={mapPoint.catalogItem.id ?? ''}
143
                            />
130 144
                        </Stack>
131 145
                    </Fragment>
132 146
                </Popup>
......
136 150

  
137 151
    return (
138 152
        <Fragment>
139
            {active && <Fragment>
140
                {buildVertices()}
141
                {buildEdges()}
142
                </Fragment>}
153
            {active && (
154
                <Fragment>
155
                    {buildVertices()}
156
                    {buildEdges()}
157
                </Fragment>
158
            )}
143 159
        </Fragment>
144 160
    )
145 161
}
146 162

  
147 163
export default MapPath
164
function setPrimaryIdx(idx: number): any {
165
    throw new Error('Function not implemented.')
166
}
frontend/src/features/TrackingTool/PlaintextUpload.tsx
7 7
    TextField,
8 8
} from '@mui/material'
9 9
import { useFormik } from 'formik'
10
import { Fragment, FunctionComponent, useState } from 'react'
10
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
11 11
import SendIcon from '@mui/icons-material/Send'
12 12
import ClearIcon from '@mui/icons-material/Clear'
13 13
import { PathDto } from '../../swagger/data-contracts'
14 14
import axiosInstance from '../../api/api'
15
import { useDispatch } from 'react-redux'
15
import { useDispatch, useSelector } from 'react-redux'
16 16
import { showNotification } from '../Notification/notificationSlice'
17
import { RootState } from '../redux/store'
18
import { sendTextForProcessing } from './trackingToolThunks'
19
import * as yup from 'yup'
20
import { resetDialogApiCallSuccess } from './trackingToolSlice'
17 21

  
18
export interface PlaintextUploadProps {
19
    setPaths: React.Dispatch<React.SetStateAction<PathDto | undefined>>
20
}
21

  
22
const PlaintextUpload: FunctionComponent<PlaintextUploadProps> = ({
23
    setPaths,
24
}) => {
25
    const [submitButtonDisabled, setSubmitButtonDisabled] = useState(false)
22
const PlaintextUpload = () => {
23
    const loading = useSelector(
24
        (state: RootState) => state.trackingTool.isLoading
25
    )
26 26
    const [open, setOpen] = useState(false) // controls whether the dialog is open
27 27

  
28
    // This controls whether to keep dialog open after sending the request to the API
29
    const dialogApiCallSuccess = useSelector(
30
        (state: RootState) => state.trackingTool.dialogApiCallSuccess
31
    )
32

  
28 33
    const dispatch = useDispatch()
29 34

  
35
    const validationSchema = yup.object().shape({
36
        text: yup.mixed().required('Text is required'),
37
    })
38

  
30 39
    const formik = useFormik({
31 40
        initialValues: {
32 41
            text: '',
33 42
        },
43
        validationSchema,
34 44
        onSubmit: async () => {
35
            let closeDialog = false
36
            setSubmitButtonDisabled(true)
37
            try {
38
                const { data, status } = await axiosInstance.post('path', {
39
                    text: formik.values.text,
40
                })
41

  
42
                if (status !== 200) {
43
                    dispatch(
44
                        showNotification({
45
                            message:
46
                                'Error while fetching map, please try again later.',
47
                            severity: 'error',
48
                        })
49
                    )
50
                } else {
51
                    dispatch(
52
                        showNotification({
53
                            message: 'Map fetched successfully.',
54
                            severity: 'success',
55
                        })
56
                    )
57
                    setPaths(data)
58
                    closeDialog = true
59
                }
60
            } catch (err: any) {
61
                dispatch(showNotification)
62
                closeDialog = true
63
            }
64
            setSubmitButtonDisabled(false)
65

  
66
            if (closeDialog) {
67
                onCloseDialog()
68
            }
45
            // Dispatch the thunk
46
            dispatch(sendTextForProcessing(formik.values.text))
69 47
        },
70 48
    })
71 49

  
......
78 56
        formik.resetForm()
79 57
    }
80 58

  
59
    useEffect(() => {
60
        if (!dialogApiCallSuccess) {
61
            return
62
        }
63
        dispatch(resetDialogApiCallSuccess())
64
        setOpen(false)
65
    }, [dialogApiCallSuccess, dispatch])
66

  
81 67
    return (
82 68
        <Fragment>
83 69
            <Button variant="contained" onClick={() => setOpen(true)}>
......
119 105
                            <Button
120 106
                                type="submit"
121 107
                                variant="contained"
122
                                disabled={submitButtonDisabled}
123 108
                                startIcon={<SendIcon />}
109
                                disabled={loading}
124 110
                            >
125 111
                                Submit
126 112
                            </Button>
frontend/src/features/TrackingTool/ProcessedTextCard.tsx
1
import { PathDto } from "../../swagger/data-contracts"
2

  
3
export interface ProcessedtextCardProps {
4
    pathDto?: PathDto
5
    
6
}
7

  
8
const ProcessedTextCard = () => {
9

  
10
}
11

  
12
export default ProcessedTextCard
frontend/src/features/TrackingTool/TrackingTool.tsx
1
import { Card, CardContent, Grid, Stack, Typography } from '@mui/material'
1
import {
2
    Button,
3
    Card,
4
    CardContent,
5
    Grid,
6
    Stack,
7
    Typography,
8
} from '@mui/material'
2 9
import { Fragment, useEffect, useState } from 'react'
3
import { MapContainer, TileLayer } from 'react-leaflet'
10
import { MapContainer, TileLayer, useMap } from 'react-leaflet'
4 11
import mapConfig from '../../config/mapConfig'
5 12
import TextPath from 'react-leaflet-textpath'
6 13
import PlaintextUpload from './PlaintextUpload'
......
9 16
import DeleteIcon from '@mui/icons-material/Delete'
10 17
import { PathDto } from '../../swagger/data-contracts'
11 18
import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils'
12
import buildPathVariants, { PathVariant } from './buildPathVariants'
13 19
import MapPath from './MapPath'
20
import EditIcon from '@mui/icons-material/Edit'
21
import { useSelector } from 'react-redux'
22
import { RootState } from '../redux/store'
14 23

  
15 24
// Page with tracking tool
16 25
const TrackingTool = () => {
17 26
    // Path response from the API
18
    const [pathDto, setPathDto] = useState<PathDto | undefined>(undefined)
27
    const pathDto = useSelector((state: RootState) => state.trackingTool.pathDto)
28
    const pathVariants = useSelector((state: RootState) => state.trackingTool.pathVariants)
29
    const mapCenter = useSelector((state: RootState) => state.trackingTool.mapCenter)
19 30

  
20
    // Whether the path variants are building
21
    const [buildingPathVariants, setBuildingPathVariants] =
22
        useState<boolean>(false)
31
    // const map = useMap()
23 32

  
24
    // List of all computed path variants
25
    const [pathVariants, setPathVariants] = useState<PathVariant[] | undefined>(
26
        undefined
27
    )
28

  
29
    // latitude longitude pair of where the map is to be centered
30
    const [mapCenter, setMapCenter] = useState<number[]>([
31
        mapConfig.defaultCoordinates[0],
32
        mapConfig.defaultCoordinates[1],
33
    ])
34

  
35
    const [primaryPathIdx, setActivePathIdx] = useState(0)
36

  
37
    // TODO make some sort of selection list to select active paths?
38
    const [activePaths, setActivePaths] = useState<Set<number>>(new Set())
33
    // // Set the map center
34
    // useEffect(() => {
35
    //     map.flyTo(mapCenter, mapConfig.defaultZoom)
36
    // }, [map, mapCenter])
39 37

  
40
    // Returns tuple of average latitude and longitude
41
    const calculateMapCenter = (pathVariant: PathVariant) => [
42
        pathVariant
43
            .map((item) => item.latitude ?? 0)
44
            .reduce((a, b) => a + b, 0) / pathVariant.length,
45
        pathVariant
46
            .map((item) => item.longitude ?? 0)
47
            .reduce((a, b) => a + b, 0) / pathVariant.length,
48
    ]
49

  
50
    useEffect(() => {
51
        if (!pathVariants || pathVariants.length === 0) {
52
            return
53
        }
54
        // TODO calculate only for path that has some non-null coordinates
55
        setMapCenter(calculateMapCenter(pathVariants[0]))
56
    }, [pathVariants])
57

  
58
    useEffect(() => {
59
        if (!pathDto) {
60
            return
61
        }
62

  
63
        setBuildingPathVariants(true)
64
        buildPathVariants(pathDto).then((pathVariants) => {
65
            setActivePaths(new Set(pathVariants.map((_, idx) => idx)))
66
            setPathVariants(pathVariants)
67
            setBuildingPathVariants(false)
68
        })
69
    }, [pathDto])
70 38

  
71 39
    return (
72 40
        <Fragment>
......
92 60
                                    )}
93 61
                                </Typography>
94 62
                            </Stack>
63
                            <Stack justifyItems="flex-end" alignSelf="flex-end" alignItems="flex-end">
64
                                <Button size="small" variant="outlined" startIcon={<EditIcon />}>Edit</Button>
65
                            </Stack>
95 66
                        </CardContent>
96 67
                    </Card>
97 68
                </Fragment>
......
123 94
                        >
124 95
                            {pathDto ? 'Update path' : 'Show Path'}
125 96
                        </Typography>
126
                        <PlaintextUpload setPaths={setPathDto} />
97
                        <PlaintextUpload />
127 98
                        <FileUpload />
128 99
                    </Stack>
129 100
                </Grid>
......
153 124
                                    key={idx}
154 125
                                    pathVariant={pathVariant}
155 126
                                    idx={idx}
156
                                    setPrimary={setActivePathIdx}
157
                                    primaryIdx={primaryPathIdx}
158
                                    active={activePaths.has(idx)}
159 127
                                />
160 128
                            ))}
161 129
                    </MapContainer>
frontend/src/features/TrackingTool/buildPathVariants.ts
14 14
        [[]]
15 15
    )
16 16

  
17
export const buildPathVariants = async (pathDto: PathDto): Promise<PathVariant[]> => {
17
export const buildPathVariants = (pathDto: PathDto): PathVariant[] => {
18 18
    if (!pathDto.foundCatalogItems) {
19 19
        return []
20 20
    }
frontend/src/features/TrackingTool/trackingToolSlice.ts
1
import { createSlice } from '@reduxjs/toolkit'
2
import { LatLngTuple } from 'leaflet'
3
import mapConfig from '../../config/mapConfig'
4
import { PathDto } from '../../swagger/data-contracts'
5
import buildPathVariants, { PathVariant } from './buildPathVariants'
6
import { sendTextForProcessing } from './trackingToolThunks'
7

  
8
export interface TrackingToolState {
9
    isLoading: boolean // whether the data is being loaded
10
    pathDto?: PathDto // the data
11
    pathVariants?: PathVariant[] // undefined signals that no path variants were yet fetched from the API
12
    lastErr?: string // consumable for errors during thunks
13
    mapCenter: LatLngTuple // pair of latitude and longitude
14
    primaryPathIdx: number // index of the primary path
15
    activePaths: Set<number> // indices of the active paths
16
    // trigger to close the dialog when API call is finished
17
    dialogApiCallSuccess: boolean
18
}
19

  
20
const initialState: TrackingToolState = {
21
    isLoading: false,
22
    mapCenter: [
23
        mapConfig.defaultCoordinates[0],
24
        mapConfig.defaultCoordinates[1],
25
    ],
26
    primaryPathIdx: 0,
27
    activePaths: new Set(),
28
    dialogApiCallSuccess: true,
29
}
30

  
31
// Returns tuple of average latitude and longitude
32
const calculateMapCenter = (pathVariant: PathVariant): LatLngTuple => [
33
    pathVariant.map((item) => item.latitude ?? 0).reduce((a, b) => a + b, 0) /
34
        pathVariant.length,
35
    pathVariant.map((item) => item.longitude ?? 0).reduce((a, b) => a + b, 0) /
36
        pathVariant.length,
37
]
38

  
39
export const trackingToolSlice = createSlice({
40
    name: 'trackingTool',
41
    initialState,
42
    reducers: {
43
        consumeErr: (state) => ({ ...state, lastErr: undefined }),
44
        setPrimaryIdx: (state, action) => ({
45
            ...state,
46
            primaryPathIdx: action.payload,
47
        }),
48
        resetDialogApiCallSuccess: (state) => ({
49
            ...state,
50
            dialogApiCallSuccess: false,
51
        }),
52
    },
53
    extraReducers: (builder) => {
54
        builder.addCase(sendTextForProcessing.fulfilled, (state, action) => {
55
            const dto: PathDto = action.payload
56
            const pathVariants = buildPathVariants(dto)
57
            return {
58
                ...state,
59
                pathVariants,
60
                // TODO map this correctly
61
                activePaths: new Set(pathVariants.map((_, idx) => idx)),
62
                // TODO calculate correctly
63
                mapCenter:
64
                    pathVariants.length > 0
65
                        ? calculateMapCenter(pathVariants[0])
66
                        : (state.mapCenter as LatLngTuple),
67
                isLoading: false,
68
                dialogApiCallSuccess: true,
69
            }
70
        })
71
        builder.addCase(sendTextForProcessing.rejected, (state, action) => ({
72
            ...initialState,
73
            lastErr: action.error.message,
74
            isLoading: false,
75
            dialogApiCallSuccess: false,
76
        }))
77
        builder.addCase(sendTextForProcessing.pending, (state) => {
78
            return {
79
                ...state,
80
                isLoading: true,
81
                dialogApiCallSuccess: false,
82
            }
83
        })
84
    },
85
})
86

  
87
export const { consumeErr, setPrimaryIdx, resetDialogApiCallSuccess } = trackingToolSlice.actions
88
const trackingToolReducer = trackingToolSlice.reducer
89
export default trackingToolReducer
frontend/src/features/TrackingTool/trackingToolThunks.ts
1
import { createAsyncThunk } from '@reduxjs/toolkit'
2
import axiosInstance from '../../api/api'
3

  
4
export const sendTextForProcessing = createAsyncThunk(
5
    'trackingTool/sendTextForProcessing',
6
    async (text: string) => {
7
        try {
8
            const { data, status } = await axiosInstance.post('path', { text })
9
            if (status !== 200) {
10
                return Promise.reject(
11
                    'Error while fetching map, please try again later'
12
                )
13
            }
14
            return data
15
        } catch (err: any) {
16
            return Promise.reject('Error, server is currently unavailable')
17
        }
18
    }
19
)
frontend/src/features/redux/store.ts
6 6
import catalogReducer from '../Catalog/catalogSlice'
7 7
import { composeWithDevTools } from 'redux-devtools-extension'
8 8
import notificationReducer from '../Notification/notificationSlice'
9
import trackingToolReducer from '../TrackingTool/trackingToolSlice'
10
import { enableMapSet } from 'immer'
11

  
12
enableMapSet()
9 13

  
10 14
const composeEnhancers = composeWithDevTools({})
11 15

  
......
15 19
        user: userReducer,
16 20
        theme: themeReducer,
17 21
        catalog: catalogReducer,
18
        notification: notificationReducer
22
        notification: notificationReducer,
23
        trackingTool: trackingToolReducer
19 24
    }),
20 25
    process.env.REACT_APP_DEV_ENV === 'true'
21 26
        ? composeEnhancers( // ComposeEnhancers will inject redux-devtools-extension

Také k dispozici: Unified diff