Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 8754af5c

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

Table scrolling

re #9534

Zobrazit rozdíly:

frontend/package.json
14 14
    "dotenv": "^16.0.0",
15 15
    "formik": "^2.2.9",
16 16
    "jwt-decode": "^3.1.2",
17
    "leaflet": "^1.8.0",
17 18
    "react": "^17.0.2",
18 19
    "react-dom": "^17.0.2",
20
    "react-leaflet": "3.2.5",
19 21
    "react-redux": "^7.2.6",
20 22
    "react-router-dom": "^6.2.2",
21 23
    "react-scripts": "5.0.0",
......
58 60
    "@testing-library/react": "^12.0.0",
59 61
    "@testing-library/user-event": "^13.2.1",
60 62
    "@types/jest": "^27.0.1",
63
    "@types/leaflet": "^1.7.9",
61 64
    "@types/node": "^16.7.13",
62 65
    "@types/react": "^17.0.43",
63 66
    "@types/react-dom": "^17.0.9",
frontend/src/config/conf.ts
1
// Configuration object for the application
1 2
const conf = {
2 3
    baseUrl:
3 4
        process.env.REACT_APP_DEV_ENV === 'true'
frontend/src/config/mapConfig.ts
1

  
2
// Map configuration interface
3
export interface MapConfig {
4
    attribution: string,
5
    url: string
6
    defaultCoordinates: number[] // pair of numbers
7
    defaultZoom: number
8
}
9

  
10
const mapConfig: MapConfig = {
11
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
12
    url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
13
    defaultCoordinates: [33.5138, 36.2765], // Damascus, Syria
14
    defaultZoom: 8
15
}
16

  
17
export default mapConfig
frontend/src/features/Catalog/Catalog.tsx
1
import {
2
    Container,
3
    Paper,
4
    Typography,
5
} from '@mui/material'
1
import { Container, Paper, Typography } from '@mui/material'
6 2
import CatalogTable from './CatalogTable'
7 3
import { Fragment } from 'react'
8 4
import CatalogFilter from './CatalogFilter'
9 5

  
10
const Catalog = () => {
11
    
12

  
13
    return (
14
        <Fragment>
15
            <Paper
16
                sx={{ py: 2, mt: 2 }}
17
                variant="outlined"
18
                style={{ minHeight: '50vh' }}
19
            >
20
                <Container sx={{ mt: 4 }}>
21
                    <Typography variant="h3" sx={{mb: 2}} fontWeight="bold" >Catalog</Typography>
22
                        <CatalogFilter />
23
                        <CatalogTable />
24
                </Container>
25
            </Paper>
26
        </Fragment>
27
    )
28
}
6
// Catalog page
7
const Catalog = () => (
8
    <Fragment>
9
        <Typography variant="h3" sx={{ mb: 2 }} fontWeight="bold">
10
            Catalog
11
        </Typography>
12
        <Paper
13
            sx={{ py: 2, mt: 2 }}
14
            variant="outlined"
15
            style={{ minHeight: '50vh' }}
16
        >
17
            <Container sx={{ mt: 4 }}>
18
                <CatalogFilter />
19
                <CatalogTable />
20
            </Container>
21
        </Paper>
22
    </Fragment>
23
)
29 24

  
30 25
export default Catalog
frontend/src/features/Catalog/CatalogFilter.tsx
1
import { Button, Collapse, Grid, Stack, TextField } from '@mui/material'
2
import { Fragment, useState } from 'react'
3
import { useDispatch } from 'react-redux'
4
import { setFilter, CatalogFilter as Filter } from './catalogSlice'
1
import {
2
    Button,
3
    Collapse,
4
    Divider,
5
    Grid,
6
    Stack,
7
    TextField,
8
} from '@mui/material'
9
import { Fragment } from 'react'
10
import { useDispatch, useSelector } from 'react-redux'
11
import { setFilter, setFilterOpen } from './catalogSlice'
5 12
import { fetchItems } from './catalogThunks'
13
import FilterListIcon from '@mui/icons-material/FilterList'
14
import FilterListOffIcon from '@mui/icons-material/FilterListOff'
15
import ManageSearchIcon from '@mui/icons-material/ManageSearch'
16
import { RootState } from '../redux/store'
6 17

  
7 18
const CatalogFilter = () => {
8 19
    const dispatch = useDispatch()
9 20

  
10
    const [filterOpen, setFilterOpen] = useState(false)
21
    const filterOpen = useSelector((state: RootState) => state.catalog.filterOpen)
11 22
    const toggleFilter = () => {
12
        setFilterOpen(!filterOpen)
23
        dispatch(setFilterOpen(!filterOpen))
13 24
    }
14 25

  
15 26
    // current filter object
16
    const filter: Filter = {}
27
    const filter = useSelector((state: RootState) => state.catalog.filter)
17 28
    const applyFilter = () => {
18 29
        dispatch(fetchItems())
19 30
    }
20 31

  
21

  
22 32
    return (
23 33
        <Fragment>
24
            <Button variant="outlined" color="primary" onClick={toggleFilter}>
34
            <Button
35
                startIcon={
36
                    filterOpen ? <FilterListOffIcon /> : <FilterListIcon />
37
                }
38
                // variant="outlined"
39
                color="primary"
40
                onClick={toggleFilter}
41
            >
25 42
                Filter
26 43
            </Button>
27 44
            <Collapse in={filterOpen} timeout="auto" unmountOnExit>
......
33 50
                                    size="small"
34 51
                                    id="name"
35 52
                                    label="Name"
36
                                    onChange={(e: any) => { 
37
                                        filter.name = e.target.value 
53
                                    onChange={(e: any) => {
54
                                        filter.name = e.target.value
38 55
                                        dispatch(setFilter(filter))
39 56
                                    }}
57
                                    value={filter.name}
40 58
                                />
41 59
                                <TextField
42 60
                                    size="small"
......
45 63
                                    onChange={(e: any) => {
46 64
                                        filter.type = e.target.value
47 65
                                        dispatch(setFilter(filter))
48
                                     }}
66
                                    }}
67
                                    value={filter.type}
49 68
                                />
50 69
                            </Stack>
51 70
                            <Stack direction="row" spacing={2}>
......
58 77
                                    size="small"
59 78
                                    id="stateOrTerritory"
60 79
                                    label="State or territory"
61
                                    onChange={(e: any) => { 
62
                                        filter.country = e.target.value 
80
                                    onChange={(e: any) => {
81
                                        filter.country = e.target.value
63 82
                                        dispatch(setFilter(filter))
64 83
                                    }}
84
                                    value={filter.country}
65 85
                                />
66 86
                                <TextField
67 87
                                    size="small"
......
77 97
                            justifyContent="flex-start"
78 98
                            alignItems="flex-end"
79 99
                        >
80
                            <Button variant="outlined" onClick={applyFilter}>Search</Button>
100
                            <Button startIcon={<ManageSearchIcon/>} variant="contained" onClick={applyFilter}>
101
                                Search
102
                            </Button>
81 103
                        </Stack>
82 104
                    </Grid>
83 105
                </Grid>
84 106
            </Collapse>
107
            {filterOpen ? <Divider sx={{ mb: 5, mt: 1 }} /> : null}
85 108
        </Fragment>
86 109
    )
87 110
}
frontend/src/features/Catalog/CatalogItemDetail.tsx
1 1
import {
2
    Box,
2
    Button,
3 3
    Divider,
4 4
    Grid,
5 5
    Paper,
6
    Skeleton,
7 6
    Typography,
8 7
} from '@mui/material'
9 8
import { Fragment, useEffect, useState } from 'react'
......
12 11
import { CatalogItemDto } from '../../swagger/data-contracts'
13 12
import ShowErrorIfPresent from '../Reusables/ShowErrorIfPresent'
14 13
import ContentLoading from '../Reusables/ContentLoading'
14
import CatalogItemMap from './CatalogItemMap'
15
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'
16
import { Link as RouterLink } from 'react-router-dom'
15 17

  
16 18
const apiError =
17 19
    'Error while fetching data from the server, please try again later.'
......
45 47
        }
46 48

  
47 49
        fetchItem()
48
    }, [])
50
    }, [itemId])
49 51

  
50 52
    // Maps catalogItem property to corresponding table row
51 53
    const mapToRow = (rowName: string, items: string[]) => (
......
56 58
                </Grid>
57 59
                <Grid item xs={4} sx={{ ml: 'auto' }}>
58 60
                    {items.map((item) => (
59
                        <Typography>{item}</Typography>
61
                        <Typography key={item}>{item}</Typography>
60 62
                    ))}
61 63
                </Grid>
62 64
            </Grid>
......
101 103

  
102 104
    return (
103 105
        // TODO remove min height
104
        <Paper style={{ minHeight: '100vh', borderRadius: 0 }} elevation={2}>
106
        <Fragment>
107
            <Button
108
                startIcon={<ArrowBackIosIcon />}
109
                variant="contained"
110
                component={RouterLink}
111
                to="/catalog"
112
                color="primary"
113
                sx={{ mb: 2 }}
114
            >
115
                Return To Catalog
116
            </Button>
105 117
            <ShowErrorIfPresent err={err} />
106 118

  
107
            {isItemLoading && !err ? <ContentLoading /> : null}
108
            {!isItemLoading && item ? (
109
                <Grid container justifyContent="space-around">
110
                    <Grid item xs={6} sx={{ px: 2 }}>
111
                        {rows.map((row, idx) => {
112
                            const maxIdx = rows.length - 1
113
                            return (
114
                                <Fragment>
115
                                    {mapToRow(
116
                                        row.rowName as string,
117
                                        row.items as string[]
118
                                    )}
119
                                    {idx === maxIdx ? null : <Divider />}
120
                                </Fragment>
121
                            )
122
                        })}
123
                    </Grid>
119
            <Paper style={{ minHeight: '100vh' }} variant="outlined">
124 120

  
125
                    <Grid item xs={6}>
126
                        <Box sx={{ px: 2, py: 4 }}>
127
                            <Typography
128
                                variant="h4"
129
                                sx={{ mb: 4 }}
130
                                fontWeight="bold"
131
                            >
132
                                Map
133
                            </Typography>
121
                {isItemLoading && !err ? <ContentLoading /> : null}
122
                {!isItemLoading && item ? (
123
                    <Grid container justifyContent="space-around">
124
                        <Grid item xs={12} md={6} sx={{ px: 2 }}>
125
                            {rows.map((row, idx) => {
126
                                const maxIdx = rows.length - 1
127
                                return (
128
                                    <Fragment>
129
                                        {mapToRow(
130
                                            row.rowName as string,
131
                                            row.items as string[]
132
                                        )}
133
                                        {idx === maxIdx ? null : <Divider />}
134
                                    </Fragment>
135
                                )
136
                            })}
137
                        </Grid>
134 138

  
135
                            <Skeleton
136
                                animation="pulse"
137
                                variant="rectangular"
138
                                width="100%"
139
                                height={400}
140
                            />
141
                        </Box>
139
                        <Grid item md={6} xs={12}>
140
                            <CatalogItemMap item={item} />
141
                        </Grid>
142 142
                    </Grid>
143
                </Grid>
144
            ) : null}
145
        </Paper>
143
                ) : null}
144
            </Paper>
145
        </Fragment>
146 146
    )
147 147
}
148 148

  
frontend/src/features/Catalog/CatalogItemMap.tsx
1
import { Box, Typography } from '@mui/material'
2
import { Fragment, FunctionComponent } from 'react'
3
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
4
import mapConfig from '../../config/mapConfig'
5
import { CatalogItemDto } from '../../swagger/data-contracts'
6

  
7
const CatalogItemMap: FunctionComponent<{ item: CatalogItemDto }> = ({
8
    item,
9
}) => {
10
    const [lat, long] = [
11
        item.longitude ?? mapConfig.defaultCoordinates[0],
12
        item.latitude ?? mapConfig.defaultCoordinates[1],
13
    ]
14

  
15
    return (
16
        <Fragment>
17
            <Box sx={{ px: 2, py: 4 }} style={{ height: '100%', minHeight: '450px', maxHeight: '70vh'}}>
18
                <MapContainer center={[long, lat]} zoom={7} style={{ height: '100%', minHeight: '100%'}}>
19
                    <TileLayer attribution={mapConfig.attribution} url={mapConfig.url} />
20
                    {!item.longitude || !item.latitude ? null : (
21
                        <Marker position={[long, lat]}>
22
                            <Popup>{item.name}</Popup>
23
                        </Marker>
24
                    )}
25
                </MapContainer>
26
                {!item.longitude || !item.latitude ? (
27
                    <Typography color="error" align="center" fontWeight="bold">Location Unavailable</Typography>
28
                ) : null}
29
            </Box>
30
        </Fragment>
31
    )
32
}
33

  
34
export default CatalogItemMap
frontend/src/features/Catalog/CatalogTable.tsx
7 7
    TableHead,
8 8
    TablePagination,
9 9
    TableRow,
10
    Typography,
10 11
} from '@mui/material'
11 12
import { Fragment, useEffect, useState } from 'react'
12 13
import { Link as RouterLink } from 'react-router-dom'
......
15 16
import ContentLoading from '../Reusables/ContentLoading'
16 17
import { RootState } from '../redux/store'
17 18
import { useDispatch, useSelector } from 'react-redux'
18
import { consumeError, setLoading } from './catalogSlice'
19
import {
20
    consumeError,
21
    setLoading,
22
    setRowsPerPage,
23
    ShowAllItemsOption,
24
} from './catalogSlice'
19 25
import { fetchItems } from './catalogThunks'
20 26

  
21 27
// Catalog table component
22 28
const CatalogTable = () => {
23 29
    const [page, setPage] = useState(0) // currently shown page
24
    const rowsPerPage = [5, 10, 15, 20] // number of rows per page
25 30

  
26
    // Selected rows per page
27
    const [selectedRowsPerPage, setSelectedRowsPerPage] = useState(
28
        rowsPerPage[0]
31
    const dispatch = useDispatch()
32

  
33
    // Rows per page
34
    const rowsPerPageOptions = useSelector(
35
        (state: RootState) => state.catalog.rowsPerPageOptions
36
    )
37
    const rowsPerPage = useSelector(
38
        (state: RootState) => state.catalog.rowsPerPage
29 39
    )
30 40

  
31
    // Subscribe to the store
41
    // Items, loading and error from api
32 42
    const items = useSelector((state: RootState) => state.catalog.items)
33 43
    const loading = useSelector((state: RootState) => state.catalog.loading)
34 44
    const apiError = useSelector((state: RootState) => state.catalog.error)
35 45

  
36
    const [displayError, setDisplayError] = useState<string | undefined>(undefined)
46
    // Local state to display any error relevant error
47
    const [displayError, setDisplayError] = useState<string | undefined>(
48
        undefined
49
    )
50
    const [unload, setUnload] = useState(true)
37 51

  
38 52
    // When changing rows per page set the selected number and reset to the first page
39 53
    const onRowsPerPageChange = (
40 54
        event: React.ChangeEvent<HTMLInputElement>
41 55
    ) => {
42
        setSelectedRowsPerPage(Number(event.target.value))
56
        dispatch(setRowsPerPage(Number(event.target.value)))
43 57
        setPage(0)
44 58
    }
45 59

  
46
    const dispatch = useDispatch()
47

  
48 60
    useEffect(() => {
49 61
        // Fetch items when the component is mounted
50 62
        // This will automatically search whenever the filter changes
......
64 76
        }
65 77
    }, [apiError, dispatch])
66 78

  
67

  
68 79
    // Name of columns in the header
69 80
    const columns = [
70 81
        'Name',
......
76 87
        'Certainty',
77 88
    ]
78 89

  
79
    const mapValueOrDefault = (value?: string) => (
80
        <TableCell align="center">{value || 'N/A'}</TableCell>
90
    const mapValueOrDefault = (value?: string, textStyle?: any) => (
91
        <TableCell align="center">
92
            <Typography
93
                sx={{
94
                    ...textStyle,
95
                }}
96
            >
97
                {value || 'N/A'}
98
            </Typography>
99
        </TableCell>
81 100
    )
82 101

  
83 102
    // Maps catalogItem to corresponding table row
......
88 107
                <Link
89 108
                    component={RouterLink}
90 109
                    to={`/catalog/${item.id as string}`}
110
                    onClick={() => setUnload(false)}
91 111
                >
92 112
                    {item.name}
93 113
                </Link>
94 114
            </TableCell>
95
            {mapValueOrDefault(item.alternativeNames?.join(', '))}
115
            {mapValueOrDefault(item.alternativeNames?.join(', '), {
116
                display: '-webkit-box',
117
                overflow: 'hidden',
118
                WebkitBoxOrient: 'vertical',
119
                wordBreak: 'break-all',
120
                WebkitLineClamp: 2,
121
            })}
96 122
            {mapValueOrDefault(item.writtenForms?.join(', '))}
97 123
            {mapValueOrDefault(item.types?.join(', '))}
98 124
            {mapValueOrDefault(item.countries?.join(', '))}
99 125
            {mapValueOrDefault(
100 126
                item.latitude && item.longitude
101
                    ? `${item.latitude}, ${item.longitude}`
127
                    ? `${item.latitude.toFixed(2)}, ${item.longitude.toFixed(
128
                          2
129
                      )}`
102 130
                    : undefined
103 131
            )}
104 132
            {mapValueOrDefault(
......
112 140
            <ShowErrorIfPresent err={displayError} />
113 141
            {loading && !displayError ? <ContentLoading /> : null}
114 142
            {!loading && !displayError ? (
115
                 <Fragment>
116
                    <TableContainer>
143
                <Fragment>
144
                    <TableContainer sx={{ minHeight: '50vh', maxHeight: '50vh' }}>
117 145
                        <Table
118 146
                            stickyHeader
119 147
                            sx={{ minWidth: 400 }}
......
131 159
                            <TableBody>
132 160
                                {items
133 161
                                    .slice(
134
                                        page * rowsPerPage[0],
135
                                        page * rowsPerPage[0] + rowsPerPage[0]
162
                                        page * rowsPerPage,
163
                                        page * rowsPerPage + rowsPerPage
136 164
                                    )
137 165
                                    .map((row, idx) => (
138 166
                                        <TableRow hover tabIndex={-1} key={idx}>
......
143 171
                        </Table>
144 172
                    </TableContainer>
145 173
                    <TablePagination
146
                        rowsPerPageOptions={rowsPerPage}
174
                        rowsPerPageOptions={rowsPerPageOptions.map((item) => ({
175
                            value:
176
                                item === ShowAllItemsOption
177
                                    ? items.length
178
                                    : item,
179
                            label: item as string,
180
                        }))}
147 181
                        component="div"
148 182
                        count={items.length}
149
                        rowsPerPage={selectedRowsPerPage}
183
                        rowsPerPage={rowsPerPage}
150 184
                        page={page}
151 185
                        onPageChange={(_, newPage) => setPage(newPage)}
152 186
                        onRowsPerPageChange={onRowsPerPageChange}
frontend/src/features/Catalog/catalogSlice.tsx
11 11
export interface CatalogState {
12 12
    items: CatalogItemDto[] // list of all fetched items
13 13
    filter: CatalogFilter // filter object
14
    filterOpen: boolean
14 15
    loading: boolean // whether the catalog is loading
15 16
    error?: string
17
    rowsPerPage: number
18
    rowsPerPageOptions: any[]
16 19
}
17 20

  
21
export const ShowAllItemsOption = 'All'
22

  
18 23
const initialState: CatalogState = {
19 24
    items: [],
20 25
    filter: {},
26
    filterOpen: false,
21 27
    loading: true,
22 28
    error: undefined,
29
    rowsPerPage: 20,
30
    rowsPerPageOptions: [5, 10, 20, 50, 100, 1000, ShowAllItemsOption],
23 31
}
24 32

  
25 33
const catalogSlice = createSlice({
......
28 36
    reducers: {
29 37
        setFilter: (state, action) => ({
30 38
            ...state,
31
            filter: {...action.payload},
39
            filter: { ...action.payload },
32 40
        }),
33 41
        clearFilter: (state, action) => ({
34 42
            ...state,
35 43
            loading: true,
36 44
            filter: {},
37 45
        }),
46
        setFilterOpen: (state, action) => ({
47
            ...state,
48
            filterOpen: action.payload,
49
        }),
38 50
        clear: (state) => ({ ...initialState }),
39 51
        setLoading: (state) => ({ ...state, loading: true }),
40 52
        consumeError: (state) => ({ ...state, error: undefined }),
53
        setRowsPerPage: (state, action) => ({
54
            ...state,
55
            rowsPerPage: action.payload,
56
        }),
41 57
    },
42 58
    extraReducers: (builder) => {
43 59
        builder.addCase(fetchItems.pending, (state) => ({
......
57 73
    },
58 74
})
59 75

  
60
export const { setFilter, clearFilter, clear, setLoading, consumeError } = catalogSlice.actions
76
export const {
77
    setFilter,
78
    clearFilter,
79
    setFilterOpen,
80
    clear,
81
    setLoading,
82
    consumeError,
83
    setRowsPerPage,
84
} = catalogSlice.actions
61 85
const reducer = catalogSlice.reducer
62 86
export default reducer
frontend/src/features/Navigation/Navigation.tsx
17 17
import ListItemText from '@mui/material/ListItemText'
18 18
import InboxIcon from '@mui/icons-material/MoveToInbox'
19 19
import MailIcon from '@mui/icons-material/Mail'
20
import { FunctionComponent } from 'react'
20
import { Fragment, FunctionComponent } from 'react'
21 21
import NavigationMenu from './NavigationMenu'
22 22
import { Paper } from '@mui/material'
23 23

  
......
84 84
    }
85 85

  
86 86
    return (
87
        <Box sx={{ display: 'flex' }}>
88
            <CssBaseline />
89
            <AppBar position="fixed" open={open}>
90
                <Toolbar>
91
                    <IconButton
92
                        color="inherit"
93
                        aria-label="open drawer"
94
                        onClick={onOpenDrawer}
95
                        edge="start"
96
                        sx={{ mr: 2, ...(open && { display: 'none' }) }}
97
                    >
98
                        <MenuIcon />
99
                    </IconButton>
100
                    <Typography variant="h6" noWrap component="div">
101
                        Assyrian Toponyms App Prototype
102
                    </Typography>
103
                </Toolbar>
104
            </AppBar>
105
            <NavigationMenu
106
                open={open}
107
                drawerWidth={drawerWidth}
108
                setOpen={setOpen}
109
            />
110
            <Main open={open} sx={{ mt: 2 }}>
111
                <Paper style={{ minHeight: '100vh', borderRadius: 0 }}>
112
                    <DrawerHeader />
113
                    {children}
114
                </Paper>
115
            </Main>
116
        </Box>
87
        <Fragment>
88
            <Paper style={{ minHeight: '100vh', borderRadius: 0 }}>
89
                <Box sx={{ display: 'flex' }}>
90
                    {/* <CssBaseline /> */}
91
                    <AppBar position="fixed" open={open}>
92
                        <Toolbar>
93
                            <IconButton
94
                                color="inherit"
95
                                aria-label="open drawer"
96
                                onClick={onOpenDrawer}
97
                                edge="start"
98
                                sx={{ ...(open && { display: 'none' }) }}
99
                            >
100
                                <MenuIcon />
101
                            </IconButton>
102
                            <Typography variant="h6" noWrap component="div">
103
                                Assyrian Toponyms App Prototype
104
                            </Typography>
105
                        </Toolbar>
106
                    </AppBar>
107
                    <NavigationMenu
108
                        open={open}
109
                        drawerWidth={drawerWidth}
110
                        setOpen={setOpen}
111
                    />
112
                    <Main open={open} sx={{ mt: 2 }}>
113
                        <DrawerHeader />
114
                        {children}
115
                    </Main>
116
                </Box>
117
            </Paper>
118
        </Fragment>
117 119
    )
118 120
}
119 121

  
frontend/src/features/Reusables/ShowErrorIfPresent.tsx
9 9
const ShowErrorIfPresent: FunctionComponent<ShowErrorProps> = ({ err }) => (
10 10
    <Fragment>
11 11
        {err ? (
12
            <Typography align="center" variant="h6" fontWeight="400">
12
            <Typography sx={{mb: 1}} align="center" variant="h6" color="error" fontWeight="bold">
13 13
                {err}
14 14
            </Typography>
15 15
        ) : null}

Také k dispozici: Unified diff