Projekt

Obecné

Profil

« Předchozí | Další » 

Revize f41a4cd3

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

import menu start

re #9741

Zobrazit rozdíly:

frontend/.vscode/launch.json
1
{
2
    // Use IntelliSense to learn about possible attributes.
3
    // Hover to view descriptions of existing attributes.
4
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
    "version": "0.2.0",
6
    "configurations": [
7
        {
8
            "type": "pwa-chrome",
9
            "request": "launch",
10
            "name": "Launch Chrome against localhost",
11
            "url": "http://localhost:3000",
12
            "webRoot": "${workspaceFolder}"
13
        }
14
    ]
15
}
frontend/src/features/Reusables/ButtonOpenableDialog.tsx
12 12
    maxWidth?: 'xs' | 'sm' | 'md' | 'lg' // the max width of the dialog
13 13
    open: boolean,
14 14
    setOpen: (open: boolean) => void
15
    size?: 'small' | 'medium' | 'large'
15 16
}
16 17

  
17 18

  
......
25 26
    children,
26 27
    maxWidth,
27 28
    open,
28
    setOpen
29
    setOpen,
30
    size
29 31
}) => {
30 32

  
31 33
    // Change maxWidth to large if its undefined
......
52 54
                onClick={onOpen}
53 55
                color={buttonColor}
54 56
                variant={buttonVariant}
57
                size={size ?? 'medium'}
55 58
            >
56 59
                {buttonText}
57 60
            </Button>
frontend/src/features/TrackingTool/DraggableList/DraggableList.tsx
12 12
    onDragEnd: OnDragEndResponder
13 13
}
14 14

  
15
const DraggableList = memo(({ items, onDragEnd }: DraggableListProps) => {
16
    window.addEventListener('error', (e) => {
17
        if (
18
            e.message ===
19
                'ResizeObserver loop completed with undelivered notifications.' ||
20
            e.message === 'ResizeObserver loop limit exceeded'
21
        ) {
22
            e.stopImmediatePropagation()
23
        }
24
    })
15
window.addEventListener('error', (e) => {
16
    if (
17
        e.message ===
18
            'ResizeObserver loop completed with undelivered notifications.' ||
19
        e.message === 'ResizeObserver loop limit exceeded'
20
    ) {
21
        e.stopImmediatePropagation()
22
    }
23
})
25 24

  
25
const DraggableList = memo(({ items, onDragEnd }: DraggableListProps) => {
26 26
    return (
27 27
        <DragDropContext onDragEnd={onDragEnd}>
28 28
            <Droppable droppableId="droppable-list">
frontend/src/features/TrackingTool/DraggableList/DraggableListItem.tsx
1 1
import {
2
    Avatar,
3 2
    Checkbox,
3
    FormControlLabel,
4
    IconButton,
4 5
    ListItem,
5 6
    ListItemAvatar,
6 7
    ListItemText,
7 8
} from '@mui/material'
8 9
import { Draggable } from 'react-beautiful-dnd'
9 10
import { MapPoint, PathVariant } from '../Map/pathUtils'
10
import LocationOnIcon from '@mui/icons-material/LocationOn'
11 11
import { CatalogItemDto } from '../../../swagger/data-contracts'
12 12
import { useDispatch } from 'react-redux'
13
import { updateMapMarker, updateMapMarkerWithId } from '../trackingToolSlice'
13
import { updateMapMarkerWithId } from '../trackingToolSlice'
14 14
import { useMemo } from 'react'
15
import DragHandleIcon from '@mui/icons-material/DragHandle'
16
import VisibilityIcon from '@mui/icons-material/Visibility'
17
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
18
import AddRoadIcon from '@mui/icons-material/AddRoad'
19
import RemoveRoadIcon from '@mui/icons-material/RemoveRoad'
15 20

  
16 21
export type DraggableListItemProps = {
17 22
    list: PathVariant
......
20 25

  
21 26
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => {
22 27
    if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) {
23
        return 'Location Unknown'
28
        return 'Location unavailable'
24 29
    }
25 30

  
26
    return `${catalogItem.latitude}°, ${catalogItem.longitude}°`
31
    return `${catalogItem.latitude.toFixed(
32
        3
33
    )}°, ${catalogItem.longitude.toFixed(3)}°`
27 34
}
28 35

  
29 36
const DraggableListItem = ({ list, idx }: DraggableListItemProps) => {
......
32 39

  
33 40
    // useMemo to prevent unnecessary re-renders which will make the list jumpy
34 41
    return useMemo(() => {
42
        const toggleAddToPath = () => {
43
            dispatch(
44
                updateMapMarkerWithId({
45
                    item: {
46
                        ...item,
47
                        addToPath: !item?.addToPath,
48
                    } as MapPoint,
49
                    id: item.id,
50
                })
51
            )
52
        }
53

  
35 54
        const toggleHidden = () => {
36 55
            dispatch(
37 56
                updateMapMarkerWithId({
38 57
                    item: {
39 58
                        ...item,
40
                        active: !item?.active,
59
                        hidden: !item?.hidden,
41 60
                    } as MapPoint,
42 61
                    id: item.id,
43 62
                })
......
55 74
                        ref={provided.innerRef}
56 75
                        {...provided.draggableProps}
57 76
                        {...provided.dragHandleProps}
58
                        sx={{
59
                            background: snapshot.isDragging
60
                                ? 'rgb(235,235,235)'
61
                                : 'inherit',
62
                        }}
63 77
                    >
64 78
                        <ListItemAvatar>
65
                            <Avatar>
66
                                <LocationOnIcon />
67
                            </Avatar>
79
                            <DragHandleIcon />
68 80
                        </ListItemAvatar>
69 81
                        <ListItemText
70 82
                            primary={item.catalogItem.name ?? 'Unknown name'}
......
72 84
                                item.catalogItem
73 85
                            )}
74 86
                        />
75
                        <Checkbox
76
                            checked={item.active}
77
                            onChange={toggleHidden}
78
                        />
87
                        <IconButton sx={{ mr: 1 }} onClick={toggleHidden}>
88
                            {item.hidden ? (
89
                                <VisibilityOffIcon />
90
                            ) : (
91
                                <VisibilityIcon />
92
                            )}
93
                        </IconButton>
94
                        <IconButton sx={{ mr: 1 }} onClick={toggleAddToPath}>
95
                            {item.addToPath ? (
96
                                <AddRoadIcon />
97
                            ) : (
98
                                <RemoveRoadIcon />
99
                            )}
100
                        </IconButton>
101
                        {/* <FormControlLabel
102
                            control={
103
                                <Checkbox
104
                                    checked={item.addToPath}
105
                                    onChange={toggleAddToPath}
106
                                />
107
                            }
108
                            label="Add to path"
109
                        /> */}
79 110
                    </ListItem>
80 111
                )}
81 112
            </Draggable>
frontend/src/features/TrackingTool/Import/AddFromCoordinatesDialog.tsx
1
import { DialogContent, DialogTitle, Grid, TextField } from '@mui/material'
2
import { FunctionComponent, useState } from 'react'
3
import ButtonOpenableDialog from '../../Reusables/ButtonOpenableDialog'
4
import * as yup from 'yup'
5
import { useDispatch } from 'react-redux'
6
import { mergeWithCurrentPath } from '../trackingToolSlice'
7
import { MapPointType, PathVariant } from '../Map/pathUtils'
8
import generateUuid from '../../../utils/id/uuidGenerator'
9
import { useFormik } from 'formik'
10
import ContextMenuDialogProps from './contextMenuDialogProps'
11

  
12
interface AddCatalogItemFromCoords {
13
    latitude: number
14
    longitude: number
15
    name: string
16
}
17

  
18
const AddFromCoordinatesDialog: FunctionComponent<ContextMenuDialogProps> = ({
19
    latLng,
20
    closeContextMenu,
21
}) => {
22
    const [open, setOpen] = useState(false)
23
    const dispatch = useDispatch()
24

  
25
    const formik = useFormik({
26
        initialValues: {
27
            latitude: latLng[0],
28
            longitude: latLng[1],
29
            name: '',
30
        } as AddCatalogItemFromCoords,
31
        validationSchema: yup.object().shape({
32
            latitude: yup.number().required('Latitude is required'),
33
            longitude: yup.number().required('Longitude is required'),
34
            name: yup.string().required('Name is required'),
35
        }),
36
        onSubmit: (values: AddCatalogItemFromCoords) => {
37
            dispatch(
38
                mergeWithCurrentPath([
39
                    {
40
                        id: generateUuid(),
41
                        idx: -1,
42
                        addToPath: false,
43
                        type: MapPointType.FromCoordinates,
44
                        catalogItem: {
45
                            name: values.name,
46
                            latitude: values.latitude,
47
                            longitude: values.longitude,
48
                        },
49
                    },
50
                ] as PathVariant)
51
            )
52
        },
53
    })
54

  
55
    return (
56
        <ButtonOpenableDialog
57
            buttonText="Create Location"
58
            buttonColor="primary"
59
            buttonVariant="text"
60
            onCloseCallback={closeContextMenu}
61
            maxWidth="xs"
62
            open={open}
63
            setOpen={setOpen}
64
            size="small"
65
        >
66
            <DialogTitle>Add New Location From Coordinates</DialogTitle>
67
            <DialogContent>
68
                <form onSubmit={formik.handleSubmit}>
69
                    <Grid container spacing={1} sx={{ mt: 1, mb: 1 }}>
70
                        <Grid item xs={12} md={6}>
71
                            <TextField
72
                                fullWidth
73
                                label="Latitude"
74
                                name="latitude"
75
                                type="number"
76
                                variant="outlined"
77
                                value={formik.values.latitude}
78
                                onChange={formik.handleChange}
79
                                error={
80
                                    Boolean(formik.errors.latitude) &&
81
                                    formik.touched.latitude
82
                                }
83
                                helperText={
84
                                    formik.errors.latitude &&
85
                                    formik.touched.latitude
86
                                }
87
                            />
88
                        </Grid>
89
                        <Grid item xs={12} md={6}>
90
                            <TextField
91
                                fullWidth
92
                                label="Longitude"
93
                                name="longitude"
94
                                type="number"
95
                                variant="outlined"
96
                                value={formik.values.longitude}
97
                                onChange={formik.handleChange}
98
                                error={
99
                                    Boolean(formik.errors.longitude) &&
100
                                    formik.touched.longitude
101
                                }
102
                                helperText={
103
                                    formik.errors.longitude &&
104
                                    formik.touched.longitude
105
                                }
106
                            />
107
                        </Grid>
108
                    </Grid>
109
                    <TextField
110
                        fullWidth
111
                        label="Name"
112
                        name="name"
113
                        variant="outlined"
114
                        value={formik.values.name}
115
                        onChange={formik.handleChange}
116
                        error={
117
                            Boolean(formik.errors.name) && formik.touched.name
118
                        }
119
                        helperText={formik.errors.name && formik.touched.name}
120
                    />
121
                </form>
122
            </DialogContent>
123
        </ButtonOpenableDialog>
124
    )
125
}
126

  
127
export default AddFromCoordinatesDialog
frontend/src/features/TrackingTool/Import/ImportContextMenu.tsx
1
import { LeafletMouseEvent } from 'leaflet'
2
import { Fragment, useCallback, useState } from 'react'
3
import { Popup, useMap, useMapEvents } from 'react-leaflet'
4

  
5
import * as React from 'react'
6
import Divider from '@mui/material/Divider'
7
import Paper from '@mui/material/Paper'
8
import MenuList from '@mui/material/MenuList'
9
import MenuItem from '@mui/material/MenuItem'
10
import ListItemText from '@mui/material/ListItemText'
11
import ListItemIcon from '@mui/material/ListItemIcon'
12
import Typography from '@mui/material/Typography'
13
import ContentCut from '@mui/icons-material/ContentCut'
14
import ContentCopy from '@mui/icons-material/ContentCopy'
15
import ContentPaste from '@mui/icons-material/ContentPaste'
16
import Cloud from '@mui/icons-material/Cloud'
17
import { Button, Stack, ThemeProvider } from '@mui/material'
18
import AddFromCoordinatesDialog from './AddFromCoordinatesDialog'
19
import { useSelector } from 'react-redux'
20
import { RootState } from '../../redux/store'
21
import { buildTheme } from '../../Theme/ThemeWrapper'
22
import ImportLocationDialog from './ImportLocationDialog'
23

  
24
const RightClickPopupMenu = () => {
25
    const [open, setOpen] = useState(false)
26
    const [latLng, setLatLng] = useState<[number, number]>([0, 0])
27

  
28
    const paletteMode = useSelector(
29
        (state: RootState) => state.theme.paletteMode
30
    )
31

  
32
    useMapEvents({
33
        contextmenu: (e: LeafletMouseEvent) => {
34
            setLatLng([e.latlng.lat, e.latlng.lng])
35
            setOpen(true)
36
        },
37
    })
38

  
39
    const closeContextMenu = useCallback(() => {
40
        setOpen(false)
41
    }, [setOpen])
42

  
43
    return (
44
        <Fragment>
45
            {open && (
46
                <Popup onClose={() => setOpen(false)} position={latLng}>
47
                    <Stack
48
                        sx={{ p: 0, mt: 0 }}
49
                        direction="column"
50
                        justifyItems="center"
51
                        justifyContent="center"
52
                    >
53
                        <Typography style={{margin: 0}} sx={{ mb: 0.5 }} align="center">
54
                            {latLng[0].toFixed(5)}°{latLng[1].toFixed(5)}°
55
                        </Typography>
56
                        <AddFromCoordinatesDialog
57
                            latLng={latLng}
58
                            closeContextMenu={closeContextMenu}
59
                        />
60
                        <ImportLocationDialog
61
                            latLng={latLng}
62
                            closeContextMenu={closeContextMenu}
63
                        />
64
                    </Stack>
65
                </Popup>
66
            )}
67
        </Fragment>
68
    )
69
}
70

  
71
export default RightClickPopupMenu
frontend/src/features/TrackingTool/Import/ImportLocationDialog.tsx
1
import {
2
    Button,
3
    DialogContent,
4
    DialogTitle,
5
    FormControl,
6
    Grid,
7
    InputLabel,
8
    MenuItem,
9
    Select,
10
} from '@mui/material'
11
import { FunctionComponent, useState } from 'react'
12
import ButtonOpenableDialog from '../../Reusables/ButtonOpenableDialog'
13
import ContextMenuDialogProps from './contextMenuDialogProps'
14

  
15
const importTypes = {
16
    localCatalog: 'Local Catalog',
17
    websiteCatalogs: 'Website Catalogs',
18
}
19

  
20
const ImportLocationDialog: FunctionComponent<ContextMenuDialogProps> = ({
21
    latLng,
22
    closeContextMenu,
23
}) => {
24
    const [open, setOpen] = useState(false)
25
    const [importType, setImportType] = useState('localCatalog')
26
    const [submitFunction, setSubmitFunction] = useState<() => void>(() => {})
27

  
28
    return (
29
        <ButtonOpenableDialog
30
            buttonText="Import Location"
31
            buttonColor="primary"
32
            buttonVariant="text"
33
            onCloseCallback={closeContextMenu}
34
            maxWidth="xs"
35
            open={open}
36
            setOpen={setOpen}
37
            size="small"
38
            onOpenCallback={() => {}}
39
        >
40
            <DialogTitle>Import Locations</DialogTitle>
41
            <DialogContent>
42
                <Grid container sx={{mt: 0}} spacing={1}>
43
                    <Grid item xs={6} md={9}>
44
                        <FormControl fullWidth>
45
                            <InputLabel id="importType">Import</InputLabel>
46
                            <Select
47
                                labelId="importType"
48
                                id="importTypeSelect"
49
                                value={importType}
50
                                label="Import"
51
                                size="small"
52
                                onChange={(e) => setImportType(e.target.value)}
53
                            >
54
                                {Object.keys(importTypes).map((key) => (
55
                                    <MenuItem key={key} value={key}>
56
                                        {
57
                                            //@ts-ignore
58
                                            importTypes[key]
59
                                        }
60
                                    </MenuItem>
61
                                ))}
62
                            </Select>
63
                        </FormControl>
64
                    </Grid>
65
                    <Grid item xs={6} md={3} alignContent="flex-end" justifyContent="flex-end">
66
                        <Button variant="contained" color="primary" onClick={submitFunction}>Import</Button>
67
                    </Grid>
68
                </Grid>
69
            </DialogContent>
70
        </ButtonOpenableDialog>
71
    )
72
}
73

  
74
export default ImportLocationDialog
frontend/src/features/TrackingTool/Import/contextMenuDialogProps.ts
1
export default interface ContextMenuDialogProps {
2
    latLng: [number, number]
3
    closeContextMenu: () => void
4
}
frontend/src/features/TrackingTool/Map/MapPath.tsx
62 62
    useEffect(() => {
63 63
        // Get all active map points
64 64
        const activeMapPoints = displayableMapPoints.filter(
65
            (item) => item.active
65
            (item) => item.addToPath && !item.hidden
66 66
        )
67 67
        if (activeMapPoints.length < 2) {
68 68
            setEdges([])
......
155 155
                                    <FormControlLabel
156 156
                                        control={
157 157
                                            <Checkbox
158
                                                checked={item.active}
158
                                                checked={item.addToPath}
159 159
                                                onChange={() => {
160 160
                                                    dispatch(
161 161
                                                        updateMapMarker({
162 162
                                                            ...item,
163
                                                            active: !item.active,
163
                                                            addToPath: !item.addToPath,
164 164
                                                        })
165 165
                                                    )
166 166
                                                }}
frontend/src/features/TrackingTool/Map/RightClickPopupMenu.tsx
1
import { LeafletMouseEvent } from 'leaflet'
2
import { Fragment, useState } from 'react'
3
import { Popup, useMap, useMapEvents } from 'react-leaflet'
4

  
5
import * as React from 'react'
6
import Divider from '@mui/material/Divider'
7
import Paper from '@mui/material/Paper'
8
import MenuList from '@mui/material/MenuList'
9
import MenuItem from '@mui/material/MenuItem'
10
import ListItemText from '@mui/material/ListItemText'
11
import ListItemIcon from '@mui/material/ListItemIcon'
12
import Typography from '@mui/material/Typography'
13
import ContentCut from '@mui/icons-material/ContentCut'
14
import ContentCopy from '@mui/icons-material/ContentCopy'
15
import ContentPaste from '@mui/icons-material/ContentPaste'
16
import Cloud from '@mui/icons-material/Cloud'
17

  
18
export function IconMenu() {
19
    return (
20
        // <Paper sx={{ width: 320, maxWidth: '100%' }}>
21
            <MenuList>
22
                <MenuItem>
23
                    <ListItemIcon>
24
                        <ContentCut fontSize="small" />
25
                    </ListItemIcon>
26
                    <ListItemText>Cut</ListItemText>
27
                    <Typography variant="body2" color="text.secondary">
28
                        ⌘X
29
                    </Typography>
30
                </MenuItem>
31
                <MenuItem>
32
                    <ListItemIcon>
33
                        <ContentCopy fontSize="small" />
34
                    </ListItemIcon>
35
                    <ListItemText>Copy</ListItemText>
36
                    <Typography variant="body2" color="text.secondary">
37
                        ⌘C
38
                    </Typography>
39
                </MenuItem>
40
                <MenuItem>
41
                    <ListItemIcon>
42
                        <ContentPaste fontSize="small" />
43
                    </ListItemIcon>
44
                    <ListItemText>Paste</ListItemText>
45
                    <Typography variant="body2" color="text.secondary">
46
                        ⌘V
47
                    </Typography>
48
                </MenuItem>
49
                <Divider />
50
                <MenuItem>
51
                    <ListItemIcon>
52
                        <Cloud fontSize="small" />
53
                    </ListItemIcon>
54
                    <ListItemText>Web Clipboard</ListItemText>
55
                </MenuItem>
56
            </MenuList>
57
        // </Paper>
58
    )
59
}
60

  
61
const RightClickPopupMenu = () => {
62
    const [open, setOpen] = useState(false)
63
    const [latLng, setLatLng] = useState<[number, number]>([0, 0])
64
    const mapEvents = useMapEvents({
65
        contextmenu: (e: LeafletMouseEvent) => {
66
            setLatLng([e.latlng.lat, e.latlng.lng])
67
            setOpen(true)
68
        },
69
    })
70

  
71
    return (
72
        <Fragment>
73
            {open && <Popup onClose={() => setOpen(false)} position={latLng}>
74
                <IconMenu />
75
                </Popup>}
76
        </Fragment>
77
    )
78
}
79

  
80
export default RightClickPopupMenu
frontend/src/features/TrackingTool/Map/pathUtils.ts
17 17
export interface MapPoint {
18 18
    id: string // unique id for react
19 19
    idx: number,
20
    active: boolean,
20
    addToPath: boolean, // whether to add the point to the path
21 21
    catalogItem: CatalogItemDto,
22 22
    type: MapPointType
23
    hidden?: boolean // if true the point will not be displayed on the map
23 24
}
24 25

  
25 26
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean =>
26
    !!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude
27
    !!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude && !mapPoint.hidden
27 28

  
28 29
/**
29 30
 * Cartesian product of two arrays
......
59 60
                {
60 61
                    id: generateUuid(),
61 62
                    idx,
62
                    active: !!catalogItem.latitude && !!catalogItem.longitude,
63
                    addToPath: !!catalogItem.latitude && !!catalogItem.longitude,
63 64
                    catalogItem,
64 65
                    type: mapPointType,
65
                })
66
                } as MapPoint)
66 67
        )
67 68
    )
68 69
}
frontend/src/features/TrackingTool/TrackingTool.tsx
24 24
import GeoJsonImportDialog from './Upload/GeoJsonImportDialog'
25 25
import ProcessedTextDisplay from './ProcessedText/ProcessedTextDisplay'
26 26
import DraggableMarkerList from './DraggableList/DraggableMarkerList'
27
import RightClickPopupMenu from './Map/RightClickPopupMenu'
27
import RightClickPopupMenu from './Import/ImportContextMenu'
28 28
import { buildTheme, getPalette } from '../Theme/ThemeWrapper'
29 29

  
30 30
const mapTheme = buildTheme('light')
frontend/src/features/TrackingTool/Upload/GeoJsonExportButton.tsx
32 32
        }
33 33

  
34 34
        const exportPath = path.filter(
35
            (vertex) => isMapPointDisplayable(vertex) && vertex.active
35
            (vertex) => isMapPointDisplayable(vertex) && vertex.addToPath
36 36
        )
37 37
        const exportPathString = exportAsGeoJsonString(exportPath)
38 38
        const blob = new Blob([exportPathString], { type: 'application/json' })
frontend/src/features/TrackingTool/Upload/GeoJsonIo.ts
4 4

  
5 5
export const exportAsGeoJsonString = (path: PathVariant) => JSON.stringify({
6 6
    type: 'FeatureCollection',
7
    features: path.filter(item => item.active && isMapPointDisplayable(item)).map((item) => {
7
    features: path.filter(item => item.addToPath && isMapPointDisplayable(item)).map((item) => {
8 8
        const catalogItem = item.catalogItem
9 9
        return {
10 10
            type: 'Feature',
......
63 63
        return {
64 64
            id: generateUuid(),
65 65
            idx: feature.properties.idx,
66
            active: true,
66
            addToPath: true,
67 67
            catalogItem: {
68 68
                id: catalogItem.id,
69 69
                name: catalogItem.name,
frontend/src/features/TrackingTool/trackingToolSlice.ts
172 172

  
173 173
            // Add items to the end
174 174
            itemsToAdd.forEach((item) => {
175
                item.active = !state.pathVariants || state.pathVariants.length === 0
175
                item.addToPath = !state.pathVariants || state.pathVariants.length === 0
176 176
                item.idx = newPath.length
177 177
                newPath.push(item)
178 178
            })

Také k dispozici: Unified diff