Projekt

Obecné

Profil

« Předchozí | Další » 

Revize f2db3e05

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

re #9369 - catalog detail fetching

Zobrazit rozdíly:

frontend/src/App.tsx
8 8
import { useSelector } from 'react-redux'
9 9
import { RootState } from './features/redux/store'
10 10
import Login from './features/Auth/Login'
11
import CatalogItemDetail from './features/Catalog/CatalogItemDetail'
11 12

  
12 13
const App = () => {
13 14
    const theme: Theme = useSelector((state: RootState) => state.theme.theme)
......
28 29
                    <Routes>
29 30
                        <Route path="/" element={<Home />} />
30 31
                        <Route path="/catalog" element={<Catalog />} />
32
                        <Route
33
                            path="/catalog/:itemId"
34
                            element={<CatalogItemDetail />}
35
                        />
31 36
                        <Route path="/login" element={<Login />} />
32 37
                        <Route path="*" element={<NotFound />} />
33 38
                    </Routes>
frontend/src/api/api.ts
39 39
    async (err) => {
40 40
        const originalConfig = err.config
41 41

  
42

  
42 43
        // Original URL might be login in which case we don't want to refresh the access token
43 44
        // Since the user just failed to log in and no token expired
44 45
        if (originalConfig.url === '/login' || !err.response) {
45 46
            return Promise.reject(err)
46 47
        }
47 48

  
49
        // If the error is not a 401 reject this
50
        if (err.response.status !== 401) {
51
            return Promise.reject(err)
52
        }
53

  
48 54
        // We need to set the refresh token in the auth header
49 55
        const oldRefreshToken = store.getState().user.refreshToken
50 56

  
frontend/src/config/conf.ts
1
export default {
1
const conf = {
2 2
    baseUrl: process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080'
3
}
3
}
4

  
5
export default conf;
frontend/src/features/Catalog/CatalogItemDetail.tsx
1
import { Box, Divider, Grid, Paper, Skeleton, Typography } from '@mui/material'
2
import { Fragment, ReactNode } from 'react'
3
import { CatalogItemDto } from '../../swagger/data-contracts'
1
import {
2
    Box,
3
    Divider,
4
    Grid,
5
    Paper,
6
    Skeleton,
7
    Stack,
8
    Typography,
9
} from '@mui/material'
10
import { Fragment, useEffect } from 'react'
11
import { useDispatch, useSelector } from 'react-redux'
12
import { useParams } from 'react-router-dom'
13
import { RootState } from '../redux/store'
14
import ConsumeLastError from '../Reusables/ConsumeLastError'
15
import ContentLoading from '../Reusables/ContentLoading'
16
import { consumeCatalogItemLastErr, setItemLoading } from './catalogSlice'
17
import { getCatalogItem } from './catalogThunks'
4 18

  
5 19
const CatalogItemDetail = () => {
6
    // TODO fetch from api
7
    const item: CatalogItemDto = {
8
        id: '1',
9
        name: 'Abaindi',
10
        alternativeNames: ['Abaindi', 'Tell muhamad', 'and his waifu'],
11
        certainty: 100,
12
        longitude: 27.22,
13
        latitude: 27.22,
14
        bibliography: ['A', 'V muhamad', '😭😂'],
15
        writtenForms: ['sadsadsa', 'sadTell muhamad', 'and his waifu'],
16
        types: ['City', 'Town', 'Village'],
17
        countries: ['Sidonu', 'Lebanon', 'Syria'],
18
    }
20
    // itemId from query params
21
    const { itemId } = useParams()
19 22

  
20
    const mapToRow = (rowName: string, items: string[]): ReactNode => (
23
    // Loaded item
24
    const item = useSelector((state: RootState) => state.catalog.catalogItem)
25

  
26
    // Whether the item is loading
27
    const isItemLoading = useSelector(
28
        (state: RootState) => state.catalog.isItemLoading
29
    )
30

  
31
    // Redux dispatch
32
    const dispatch = useDispatch()
33

  
34
    const lastErr = useSelector(
35
        (state: RootState) => state.catalog.catalogItemLastErr
36
    )
37

  
38
    // Fetch the item from the api after mounting the component
39
    useEffect(() => {
40
        dispatch(setItemLoading(true))
41
        dispatch(getCatalogItem(itemId as string))
42
    }, [dispatch, itemId])
43

  
44
    // Maps catalogItem property to corresponding table row
45
    const mapToRow = (rowName: string, items: string[]) => (
21 46
        <Fragment>
22 47
            <Grid sx={{ my: 2 }} container justifyContent="space-around">
23 48
                <Grid item xs={8} sx={{ px: 1 }}>
......
27 52
                    {items.map((item) => (
28 53
                        <Typography>{item}</Typography>
29 54
                    ))}
30
                    {items.map((itm, idx) => () => <Fragment></Fragment>)}
31 55
                </Grid>
32 56
            </Grid>
33 57
        </Fragment>
34 58
    )
35 59

  
60
    // Catalog item rows
36 61
    const rows = [
37 62
        {
38 63
            rowName: 'Name',
39
            items: [item.name],
64
            items: [item?.name],
40 65
        },
41 66
        {
42 67
            rowName: 'Alternative Names',
43
            items: item.alternativeNames,
68
            items: item?.alternativeNames,
44 69
        },
45 70
        {
46 71
            rowName: 'Written Forms',
47
            items: item.writtenForms,
72
            items: item?.writtenForms,
48 73
        },
49 74
        {
50 75
            rowName: 'Type',
51
            items: item.types,
76
            items: item?.types,
52 77
        },
53 78
        {
54 79
            rowName: 'State or Territory',
55
            items: item.countries,
80
            items: item?.countries,
56 81
        },
57 82
        {
58 83
            rowName: 'Coordinates',
59
            items: [`${item.longitude}°, ${item.latitude}°`],
84
            items: [`${item?.longitude}°, ${item?.latitude}°`],
60 85
        },
61

  
62 86
        {
63 87
            rowName: 'Certainty',
64
            // @ts-ignore
65
            items: [item.certainty],
88
            items: [item?.certainty],
66 89
        },
67 90
        {
68 91
            rowName: 'Bibliography',
69
            items: item.bibliography,
92
            items: item?.bibliography,
70 93
        },
71
        // TODO description
72 94
    ]
73 95

  
74 96
    return (
75 97
        // TODO remove min height
76 98
        <Paper style={{ minHeight: '100vh', borderRadius: 0 }} elevation={2}>
77
            <Grid container justifyContent="space-around">
78
                {/* <Paper style={{ minHeight: '100vh', borderRadius: 0 }}> */}
79
                <Grid item xs={6} sx={{ px: 2 }}>
80
                    {rows.map((row, idx) => {
81
                        return (
82
                            <Fragment>
83
                                {mapToRow(
84
                                    row.rowName as string,
85
                                    row.items as string[]
86
                                )}
87
                                {idx === rows.length - 1 ? null : <Divider />}
88
                            </Fragment>
89
                        )
90
                    })}
91
                </Grid>
99
            <ConsumeLastError
100
                consumeFn={consumeCatalogItemLastErr}
101
                err={lastErr}
102
            />
92 103

  
93
                <Grid item xs={6}>
94
                    <Box sx={{ px: 2, py: 4 }}>
95
                        <Typography variant="h4" sx={{mb: 4}} fontWeight="bold">Map</Typography>
104
            {isItemLoading ? <ContentLoading /> : null}
105
            {!isItemLoading && item ? (
106
                <Grid container justifyContent="space-around">
107
                    <Grid item xs={6} sx={{ px: 2 }}>
108
                        {rows.map((row, idx) => {
109
                            const maxIdx = rows.length - 1
110
                            return (
111
                                <Fragment>
112
                                    {mapToRow(
113
                                        row.rowName as string,
114
                                        row.items as string[]
115
                                    )}
116
                                    {idx === maxIdx ? null : <Divider />}
117
                                </Fragment>
118
                            )
119
                        })}
120
                    </Grid>
96 121

  
97
                        <Skeleton
98
                        animation="pulse"
99
                            variant="rectangular"
100
                            width={'100%'}
101
                            height={400}
102
                        />
103
                        {/* <Skeleton animation="wave" />
104
                        <Skeleton animation="wave" />
105
                        <Skeleton animation="wave" />
106
                        <Skeleton animation="wave" /> */}
107
                    </Box>
122
                    <Grid item xs={6}>
123
                        <Box sx={{ px: 2, py: 4 }}>
124
                            <Typography
125
                                variant="h4"
126
                                sx={{ mb: 4 }}
127
                                fontWeight="bold"
128
                            >
129
                                Map
130
                            </Typography>
131

  
132
                            <Skeleton
133
                                animation="pulse"
134
                                variant="rectangular"
135
                                width="100%"
136
                                height={400}
137
                            />
138
                        </Box>
139
                    </Grid>
108 140
                </Grid>
109
                {/* </Paper> */}
110
            </Grid>
141
            ) : null}
111 142
        </Paper>
112 143
    )
113 144
}
frontend/src/features/Catalog/CatalogTable.tsx
1 1
import {
2
    Link,
2 3
    Table,
3 4
    TableBody,
4 5
    TableCell,
......
9 10
} from '@mui/material'
10 11
import { Fragment, useEffect, useState } from 'react'
11 12
import { useDispatch, useSelector } from 'react-redux'
13
import { Link as RouterLink } from 'react-router-dom'
12 14
import { CatalogItemDto } from '../../swagger/data-contracts'
13 15
import { RootState } from '../redux/store'
16
import ConsumeLastError from '../Reusables/ConsumeLastError'
17
import ContentLoading from '../Reusables/ContentLoading'
18
import { consumeCatalogLastErr } from './catalogSlice'
14 19
import { getCatalogItems } from './catalogThunks'
15 20

  
16 21
// Catalog table component
......
19 24
    const rowsPerPage = [5, 10, 15, 20] // number of rows per page
20 25

  
21 26
    // Selected rows per page
22
    const [selectedRowsPerPage, setSelectedRowsPerPage] = useState(rowsPerPage[0])
27
    const [selectedRowsPerPage, setSelectedRowsPerPage] = useState(
28
        rowsPerPage[0]
29
    )
23 30

  
24 31
    // Fetched items from the store
25 32
    const items = useSelector((state: RootState) => state.catalog.catalogItems)
33
    const areItemsLoading = useSelector(
34
        (state: RootState) => state.catalog.isListLoading
35
    )
36
    const fetchError = useSelector(
37
        (state: RootState) => state.catalog.catalogLastErr
38
    )
26 39

  
27 40
    // Redux dispatch
28 41
    const dispatch = useDispatch()
29 42

  
30 43
    // When changing rows per page set the selected number and reset to the first page
31
    const onRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
44
    const onRowsPerPageChange = (
45
        event: React.ChangeEvent<HTMLInputElement>
46
    ) => {
32 47
        setSelectedRowsPerPage(Number(event.target.value))
33
        setPage(0);
48
        setPage(0)
34 49
    }
35 50

  
36 51
    // Use effect hook to fetch rows from the server
......
56 71
    // Maps catalogItem to corresponding table row
57 72
    const mapItemColumnValues = (item: CatalogItemDto) => (
58 73
        <Fragment>
59
            {mapValueOrDefault(item.name)}
74
            {/* {mapValueOrDefault(item.name)} */}
75
            <TableCell align="center">
76
                <Link component={RouterLink} to={`/catalog/${item.id as string}`}>{item.name}</Link>
77
            </TableCell>
60 78
            {mapValueOrDefault(item.alternativeNames?.join(', '))}
61 79
            {mapValueOrDefault(item.writtenForms?.join(', '))}
62 80
            {mapValueOrDefault(item.types?.join(', '))}
......
74 92

  
75 93
    return (
76 94
        <Fragment>
77
            <TableContainer>
78
                <Table
79
                    stickyHeader
80
                    sx={{ minWidth: 400 }}
81
                    aria-label="catalogTable"
82
                >
83
                    <TableHead>
84
                        <TableRow>
85
                            {columns.map((col, idx) => (
86
                                <TableCell key={idx} align="center">
87
                                    {col}
88
                                </TableCell>
89
                            ))}
90
                        </TableRow>
91
                    </TableHead>
92
                    <TableBody>
93
                        {items
94
                            .slice(
95
                                page * rowsPerPage[0],
96
                                page * rowsPerPage[0] + rowsPerPage[0]
97
                            )
98
                            .map((row, idx) => (
99
                                <TableRow hover tabIndex={-1} key={idx}>
100
                                    {mapItemColumnValues(row)}
101
                                </TableRow>
102
                            ))}
103
                    </TableBody>
104
                </Table>
105
            </TableContainer>
106
            <TablePagination
107
                rowsPerPageOptions={rowsPerPage}
108
                component="div"
109
                count={items.length}
110
                rowsPerPage={selectedRowsPerPage}
111
                page={page}
112
                onPageChange={(_, newPage) => setPage(newPage)}
113
                onRowsPerPageChange={onRowsPerPageChange}
95
            <ConsumeLastError
96
                err={fetchError}
97
                consumeFn={consumeCatalogLastErr}
114 98
            />
99
            {areItemsLoading && !fetchError ? <ContentLoading /> : null}
100
            {!areItemsLoading && !fetchError ? (
101
                <Fragment>
102
                    <TableContainer>
103
                        <Table
104
                            stickyHeader
105
                            sx={{ minWidth: 400 }}
106
                            aria-label="catalogTable"
107
                        >
108
                            <TableHead>
109
                                <TableRow>
110
                                    {columns.map((col, idx) => (
111
                                        <TableCell key={idx} align="center">
112
                                            {col}
113
                                        </TableCell>
114
                                    ))}
115
                                </TableRow>
116
                            </TableHead>
117
                            <TableBody>
118
                                {items
119
                                    .slice(
120
                                        page * rowsPerPage[0],
121
                                        page * rowsPerPage[0] + rowsPerPage[0]
122
                                    )
123
                                    .map((row, idx) => (
124
                                        <TableRow hover tabIndex={-1} key={idx}>
125
                                            {mapItemColumnValues(row)}
126
                                        </TableRow>
127
                                    ))}
128
                            </TableBody>
129
                        </Table>
130
                    </TableContainer>
131
                    <TablePagination
132
                        rowsPerPageOptions={rowsPerPage}
133
                        component="div"
134
                        count={items.length}
135
                        rowsPerPage={selectedRowsPerPage}
136
                        page={page}
137
                        onPageChange={(_, newPage) => setPage(newPage)}
138
                        onRowsPerPageChange={onRowsPerPageChange}
139
                    />
140
                </Fragment>
141
            ) : null}
115 142
        </Fragment>
116 143
    )
117 144
}
frontend/src/features/Catalog/catalogSlice.ts
5 5
export interface CatalogState {
6 6
    catalogItems: CatalogItemDto[] // Items shown in the table
7 7
    isListLoading: boolean
8
    lastErr?: string
8
    catalogLastErr?: string // Error while fetching all catalog items
9 9
    catalogItem?: CatalogItemDto // Item shown in detail
10 10
    isItemLoading: boolean
11
    catalogItemLastErr?: string // Error while fetching catalog item
11 12
}
12 13

  
13 14
const initialState: CatalogState = {
......
18 19

  
19 20
export const catalogSlice = createSlice({
20 21
    name: 'catalog',
21

  
22 22
    initialState,
23

  
24 23
    reducers: {
25 24
        setItemListLoading: (state, action) => ({
26 25
            ...state,
27 26
            isListLoading: action.payload,
28 27
        }),
28
        consumeCatalogLastErr: (state) => ({
29
            ...state,
30
            catalogLastErr: undefined,
31
        }),
29 32
        setItemLoading: (state, action) => ({
30 33
            ...state,
31 34
            isItemLoading: action.payload,
32 35
        }),
36
        consumeCatalogItemLastErr: (state) => ({
37
            ...state,
38
            catalogItemLastErr: undefined,
39
        }),
33 40
    },
34 41

  
35 42
    extraReducers: (builder) => {
......
51 58
            ...state,
52 59
            lastErr: action.payload as string,
53 60
        }))
54
        builder.addCase(getCatalogItem.fulfilled, (state, action) => ({
55
            ...state, catalogItem: action.payload, isItemLoading: false,
56
        }))
57
        builder.addCase(getCatalogItem.rejected, (state, action) => ({
58
            ...state, lastErr: action.payload as string
59
        }))
60 61
    },
61 62
})
62 63

  
64
export const {
65
    setItemListLoading,
66
    setItemLoading,
67
    consumeCatalogItemLastErr,
68
    consumeCatalogLastErr,
69
} = catalogSlice.actions
70

  
63 71
export const catalogReducer = catalogSlice.reducer
frontend/src/features/Catalog/catalogThunks.ts
21 21

  
22 22
export const getCatalogItem = createAsyncThunk(
23 23
    'catalog/getCatalogItem',
24
    async (id: number) => {
24
    async (id: string) => {
25 25
        try {
26 26
            const { data, status } = await axiosInstance.get(
27 27
                `/catalog-items/${id}`
frontend/src/features/Reusables/ConsumeLastError.tsx
1
import { Typography } from '@mui/material'
2
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
3
import { useDispatch } from 'react-redux'
4

  
5
export interface ConsumeLastErrorProps {
6
    // Observable error from the store
7
    err?: string
8
    // Consume function that clears the error in the store
9
    consumeFn: () => void
10
}
11

  
12
// Utility component that consumes the last error from the store and shows it
13
const ConsumeLastError: FunctionComponent<ConsumeLastErrorProps> = ({
14
    err: error,
15
    consumeFn,
16
}) => {
17
    const dispatch = useDispatch()
18
    const [err, setErr] = useState('')
19

  
20
    useEffect(() => {
21
        if (error) {
22
            setErr(`${error}`)
23
            dispatch(consumeFn())
24
        }
25
    }, [consumeFn, dispatch, error])
26

  
27
    return <Fragment>
28
        {err ? <Typography variant="h6" fontWeight="400">{err}</Typography> : null}
29
    </Fragment>
30
}
31

  
32
export default ConsumeLastError
frontend/src/features/Reusables/ContentLoading.tsx
1
import { Skeleton, Stack, Typography } from "@mui/material";
2
import { Fragment } from "react";
3

  
4
/**
5
 * Component that shows a skeleton while the specified item is loading
6
 * @returns 
7
 */
8
const ContentLoading = () => (
9
    <Fragment>
10
        <Typography align="center" fontWeight={400}>
11
            Loading ...
12
        </Typography>
13
        <Stack justifyContent="center" alignItems="center">
14
            <Skeleton variant="rectangular" width="100%" height="25%" />
15
        </Stack>
16
    </Fragment>
17
)
18

  
19
export default ContentLoading

Také k dispozici: Unified diff