From 87068c1592452eb7bc1360a381d6802487eea663 Mon Sep 17 00:00:00 2001 From: Schwobik <michal.schwob@seznam.cz> Date: Tue, 24 May 2022 15:28:17 +0200 Subject: [PATCH 1/3] Edit catalog items - start of implementation re #9819 --- .../src/features/Catalog/EditCatalogItem.tsx | 345 ++++++++++++++++++ .../src/features/Catalog/catalogItemSlice.ts | 69 ++++ .../features/Catalog/catalogItemThunks.tsx | 53 +++ 3 files changed, 467 insertions(+) create mode 100644 frontend/src/features/Catalog/EditCatalogItem.tsx create mode 100644 frontend/src/features/Catalog/catalogItemSlice.ts create mode 100644 frontend/src/features/Catalog/catalogItemThunks.tsx diff --git a/frontend/src/features/Catalog/EditCatalogItem.tsx b/frontend/src/features/Catalog/EditCatalogItem.tsx new file mode 100644 index 0000000..91b8453 --- /dev/null +++ b/frontend/src/features/Catalog/EditCatalogItem.tsx @@ -0,0 +1,345 @@ +import { + Button, + Dialog, + DialogContent, + Divider, + Grid, + Paper, + Stack, TextField, + Typography, +} from '@mui/material' +import { Fragment, FunctionComponent, useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import axiosInstance from '../../api/api' +import { CatalogItemDto } from '../../swagger/data-contracts' +import ShowErrorIfPresent from '../Reusables/ShowErrorIfPresent' +import ContentLoading from '../Reusables/ContentLoading' +import CatalogItemMap from './CatalogItemMap' +import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos' +import { Link as RouterLink } from 'react-router-dom' +import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils' +import {RootState} from "../redux/store" +import {useDispatch, useSelector} from "react-redux" +import * as yup from "yup" +import {FieldArray, ErrorMessage, Field, Form, useFormik} from "formik" +import {register} from "../Auth/userThunks" +import {updateCatalogItem} from "./catalogItemThunks" + +const apiError = + 'Error while fetching data from the server, please try again later.' + +export interface CatalogItemEditProps { + itemId: string + showReturnToDetailButton?: boolean +} + +export interface RowData { + rowName: string + items: string[] | undefined, + isExpandable: boolean, + key: string +} + +const EditCatalogItem: FunctionComponent<CatalogItemEditProps> = ({ + itemId, + showReturnToDetailButton, +}) => { + const [item, setItem] = useState<CatalogItemDto | undefined>(undefined) + const [isItemLoading, setIsItemLoading] = useState(true) + const [err, setErr] = useState<string | undefined>(undefined) + + const canEditCatalog = useSelector( + (state: RootState) => state.user.roles.includes("WRITE") + ) + + const dispatch = useDispatch() + + // Fetch the item from the api after mounting the component + useEffect(() => { + // Function to fetch the item from the api + const fetchItem = async () => { + try { + const { data, status } = await axiosInstance.get( + `/catalog-items/${itemId}` + ) + if (status !== 200) { + setErr(apiError) + return + } + + setItem(data) + setIsItemLoading(false) + } catch (err: any) { + setErr(apiError) + } + } + + fetchItem() + }, [itemId]) + + const validationSchema = yup.object().shape({ + name: yup.string().required('Item name is required'), + longitude: yup.number().required('Longitude has to be a number'), + latitude: yup.number().required('Latitude has to be a number'), + certainty: yup.number().required('Certainty has to be a number'), + }) + + const formik = useFormik({ + initialValues: { + name: item?.name, + allNames: item?.allNames, + writtenForms: item?.writtenForms, + types: item?.types, + countries: item?.countries, + bibliography: item?.bibliography, + longitude: item?.longitude, + latitude: item?.latitude, + certainty: item?.certainty, + description: item?.description, + }, + validationSchema, + onSubmit: () => { + const response = dispatch( + updateCatalogItem({ + item: { + id: item?.id, + name: formik.values.name, + allNames: formik.values.allNames, + writtenForms: formik.values.writtenForms, + types: formik.values.types, + countries: formik.values.countries, + bibliography: formik.values.bibliography, + longitude: formik.values.longitude, + latitude: formik.values.latitude, + certainty: formik.values.certainty, + description: formik.values.description, + } + }) + ) + + }, + }) + + + const handleFormChangeExpandable = (index: number, event: React.ChangeEvent<HTMLInputElement>, array: string[], key: string | undefined) => { + if (!rows.filter(v => v.key === key)[0].items) { + rows.filter(v => v.key === key)[0].items = [event.target.value] + } else { + // @ts-ignore + rows.filter(v => v.key === key)[0].items[index] = event.target.value + } + } + + const handleFormChange = (event: React.ChangeEvent<HTMLInputElement>, array: string[], key: string | undefined) => { + if (!rows.filter(v => v.key === key)[0].items) { + rows.filter(v => v.key === key)[0].items = [event.target.value] + } else { + // @ts-ignore + rows.filter(v => v.key === key)[0].items[index] = event.target.value + } + } + + // Maps catalogItem property to corresponding table row + const mapToRow = (row: RowData) => ( + <Fragment> + <Grid sx={{ my: 2 }} container justifyContent="space-around"> + <Grid item xs={4} sx={{ px: 1 }}> + <Typography fontWeight={500}>{row.rowName}</Typography> + </Grid> + {/*<Grid item xs={8} sx={{ ml: 'auto' }}>*/} + {/* {row.isExpandable ? (*/} + {/* row.items?.map((value, index, array) => (*/} + {/* <input*/} + {/* name='name'*/} + {/* placeholder='Name'*/} + {/* value={formik.values[row.key]?.at(index)}*/} + {/* onChange={event => handleFormChangeExpandable(index, event, array, row.key)}*/} + {/* />*/} + {/* ))*/} + {/* ) : (*/} + + {/* )}*/} + + {/*</Grid>*/} + </Grid> + </Fragment> + ) + + + + // Catalog item rows + const rows: RowData[] = [ + { + rowName: 'Name', + items: [item?.name as string], + isExpandable: false, + key: 'name' + }, + { + rowName: 'All Names', + items: item?.allNames, + isExpandable: true, + key: 'allNames' + }, + { + rowName: 'Written Forms', + items: item?.writtenForms, + isExpandable: true, + key: 'writtenForms' + }, + { + rowName: 'Type', + items: item?.types, + isExpandable: true, + key: 'types' + }, + { + rowName: 'State or Territory', + items: item?.countries, + isExpandable: true, + key: 'countries' + }, + { + rowName: 'Longitude', + // Must be in array otherwise the string gets iterated + items: [item?.longitude as unknown as string], + isExpandable: false, + key: 'countries' + }, + { + rowName: 'Latitude', + // Must be in array otherwise the string gets iterated + items: [item?.latitude as unknown as string], + isExpandable: false, + key: 'latitude' + }, + { + rowName: 'Certainty', + items: [item?.certainty as unknown as string], + isExpandable: false, + key: 'certainty' + }, + { + rowName: 'Bibliography', + items: item?.bibliography, + isExpandable: false, + key: 'bibliography' + }, + ] + + return ( + <Fragment> + {showReturnToDetailButton && ( + <Stack + direction="row" + alignItems="flex-start" + spacing={2} + sx={{ mt: 1 }} + > + <Button + startIcon={<ArrowBackIosIcon />} + variant="contained" + component={RouterLink} + to="/catalog" + color="primary" + sx={{ mb: 2 }} + > + Back To Detail + </Button> + </Stack> + )} + <ShowErrorIfPresent err={err} /> + + <Paper style={{ minHeight: '100vh' }} variant="outlined"> + {isItemLoading && !err ? <ContentLoading /> : null} + {!isItemLoading && item ? ( + <form onSubmit={formik.handleSubmit}> + <Grid sx={{ my: 2 }} container justifyContent="space-around"> + <Grid item xs={4} sx={{ px: 1 }}> + <Typography fontWeight={500}>Name</Typography> + </Grid> + <Grid item xs={8} sx={{ ml: 'auto' }}> + <TextField + fullWidth + label="Name" + name="name" + sx={{ mb: 2 }} + value={formik.values.name} + onChange={formik.handleChange} + error={ + Boolean(formik.errors.name) && + formik.touched.name + } + helperText={ + formik.errors.name && + formik.touched.name && + formik.errors.name + } + /> + </Grid> + <Grid item xs={4} sx={{ px: 1 }}> + <Typography fontWeight={500}>All Names</Typography> + </Grid> + <Grid item xs={8} sx={{ ml: 'auto' }}> + <FieldArray name="allNames"> + {() => (formik.values.allNames?.map((allName, i) => { + return ( + <div key={i} className="list-group list-group-flush"> + <div className="list-group-item"> + <h5 className="card-title">Ticket {i + 1}</h5> + <div className="form-row"> + <div className="form-group col-6"> + <label>Name</label> + <Field name={`tickets.${i}.name`} type="text" /> + <ErrorMessage name={`tickets.${i}.name`} component="div" /> + </div> + <div className="form-group col-6"> + <label>Email</label> + <Field name={`tickets.${i}.email`} type="text" /> + <ErrorMessage name={`tickets.${i}.email`} component="div" /> + </div> + </div> + </div> + </div> + ); + }))} + </FieldArray> + + </Grid> + </Grid> + </form> + ) : null} + </Paper> + </Fragment> + ) +} + +export const RoutedCatalogItemEdit = () => { + const { itemId } = useParams() + return <EditCatalogItem itemId={itemId ?? ''} /> +} + +export const DialogCatalogItemEdit: FunctionComponent< + CatalogItemEditProps +> = ({ itemId }) => { + const [open, setOpen] = useState(false) + return ( + <Fragment> + <Button variant="contained" onClick={() => setOpen(true)}> + Detail + </Button> + <Dialog + open={open} + onClose={() => setOpen(false)} + fullWidth + maxWidth="lg" + > + <DialogContent> + <EditCatalogItem itemId={itemId} /> + </DialogContent> + </Dialog> + </Fragment> + ) +} + +export default EditCatalogItem diff --git a/frontend/src/features/Catalog/catalogItemSlice.ts b/frontend/src/features/Catalog/catalogItemSlice.ts new file mode 100644 index 0000000..8aaf545 --- /dev/null +++ b/frontend/src/features/Catalog/catalogItemSlice.ts @@ -0,0 +1,69 @@ +import { createSlice } from '@reduxjs/toolkit' +import { CatalogItemDto } from '../../swagger/data-contracts' +import {updateCatalogItem, updateCatalogItemsBulk} from "./catalogItemThunks" + +export interface CatalogItemState { + loading: boolean // whether the catalog is loading + error?: string +} + +export interface UpdateCatalogItem { + item: CatalogItemDto +} + +export interface UpdateCatalogItemsBulk { + items: CatalogItemDto[] +} + +const initialState: CatalogItemState = { + loading: false, + error: undefined +} + +const catalogItemSlice = createSlice({ + name: 'catalogItem', + initialState, + reducers: { + clear: (state: CatalogItemState) => ({ ...initialState }), + setLoading: (state: CatalogItemState) => ({ ...state, loading: true }), + resetLoading: (state: CatalogItemState) => ({ ...state, loading: false }), + consumeError: (state: CatalogItemState) => ({ ...state, error: undefined }), + }, + extraReducers: (builder) => { + builder.addCase(updateCatalogItem.pending, (state: CatalogItemState) => ({ + ...state, + loading: true, + })) + builder.addCase(updateCatalogItem.fulfilled, (state: CatalogItemState, action: any) => ({ + ...state, + loading: false, + })) + builder.addCase(updateCatalogItem.rejected, (state: CatalogItemState, action: any) => ({ + ...state, + loading: false, + error: action.error.message as string, + })) + builder.addCase(updateCatalogItemsBulk.pending, (state: CatalogItemState) => ({ + ...state, + loading: true, + })) + builder.addCase(updateCatalogItemsBulk.fulfilled, (state: CatalogItemState, action: any) => ({ + ...state, + loading: false, + })) + builder.addCase(updateCatalogItemsBulk.rejected, (state: CatalogItemState, action: any) => ({ + ...state, + loading: false, + error: action.error.message as string, + })) + }, +}) + +export const { + clear, + setLoading, + resetLoading, + consumeError, +} = catalogItemSlice.actions +const reducer = catalogItemSlice.reducer +export default reducer diff --git a/frontend/src/features/Catalog/catalogItemThunks.tsx b/frontend/src/features/Catalog/catalogItemThunks.tsx new file mode 100644 index 0000000..10df9f4 --- /dev/null +++ b/frontend/src/features/Catalog/catalogItemThunks.tsx @@ -0,0 +1,53 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import axiosInstance from '../../api/api' +import {CatalogItemState, UpdateCatalogItem, UpdateCatalogItemsBulk} from "./catalogItemSlice" + +const apiError = 'Error, server is currently unavailable.' + + +// Thunk to update catalog item using API +export const updateCatalogItem = createAsyncThunk( + 'catalogItem/update', + async (catalogItem: UpdateCatalogItem) => { + try { + // Send request with the filter + const { data, status } = await axiosInstance.post( + '/catalog-items', + catalogItem.item + ) + + // If the request was successful return the items + if (status === 200) { + return data + } + + return Promise.reject(apiError) + } catch (err: any) { + return Promise.reject(err.response.data) + } + } +) + +// Thunk to update catalog item using API +export const updateCatalogItemsBulk = createAsyncThunk( + 'catalogItem/updateBulk', + async (bulkItems: UpdateCatalogItemsBulk) => { + try { + + // Send request with the filter + const { data, status } = await axiosInstance.post( + '/catalog-items/batch', + bulkItems.items + ) + + // If the request was successful return the items + if (status === 200) { + return data + } + + return Promise.reject(apiError) + } catch (err: any) { + return Promise.reject(err.response.data) + } + } +) -- GitLab From 157be6e6d899e852d9972d6d5e67797f3240963b Mon Sep 17 00:00:00 2001 From: Schwobik <michal.schwob@seznam.cz> Date: Tue, 24 May 2022 15:29:03 +0200 Subject: [PATCH 2/3] Edit catalog items - start of implementation re #9819 --- frontend/src/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c2dd20d..81153ac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,8 @@ import Register from "./features/Auth/Register" import Bibliography from "./features/Bibliography/Bibliography" import EditBibliography from "./features/Bibliography/EditBibliography" import ExternalSources from "./features/ExternalSources/ExternalSources" +import {RoutedCatalogItemEdit} from "./features/Catalog/EditCatalogItem" + const App = () => { return ( @@ -37,6 +39,10 @@ const App = () => { path="/catalog/:itemId" element={<RoutedCatalogItemDetail />} /> + <Route + path="/catalog/edit/:itemId" + element={<RoutedCatalogItemEdit />} + /> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/logout" element={<Logout />} /> -- GitLab From 6187bffd522a38ac12e8fad48acad615d9ebdd92 Mon Sep 17 00:00:00 2001 From: Schwobik <michal.schwob@seznam.cz> Date: Thu, 26 May 2022 00:51:30 +0200 Subject: [PATCH 3/3] Edit catalog items - complete implementation re #9819 --- frontend/src/App.tsx | 2 +- .../features/Catalog/CatalogItemDetail.tsx | 2 +- .../src/features/Catalog/EditCatalogItem.tsx | 620 ++++++++++++------ .../src/features/Catalog/catalogItemSlice.ts | 10 +- .../features/Catalog/catalogItemThunks.tsx | 2 +- frontend/src/features/redux/store.ts | 2 + 6 files changed, 423 insertions(+), 215 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5e3a9ff..8416401 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,7 +22,7 @@ import Bibliography from "./features/Bibliography/Bibliography" import EditBibliography from "./features/Bibliography/EditBibliography" import ExternalSources from "./features/ExternalSources/ExternalSources" import ChangePassword from "./features/Administration/ChangePassword" -import {RoutedCatalogItemEdit} from "./features/Catalog/EditCatalogItem" +import { RoutedCatalogItemEdit } from "./features/Catalog/EditCatalogItem" const App = () => { diff --git a/frontend/src/features/Catalog/CatalogItemDetail.tsx b/frontend/src/features/Catalog/CatalogItemDetail.tsx index 269349f..f511ccf 100644 --- a/frontend/src/features/Catalog/CatalogItemDetail.tsx +++ b/frontend/src/features/Catalog/CatalogItemDetail.tsx @@ -150,7 +150,7 @@ const CatalogItemDetail: FunctionComponent<CatalogItemDetailProps> = ({ startIcon={<EditIcon />} variant="contained" component={RouterLink} - to="/catalog" + to={`/catalog/edit/${itemId as string}`} color="primary" > Edit diff --git a/frontend/src/features/Catalog/EditCatalogItem.tsx b/frontend/src/features/Catalog/EditCatalogItem.tsx index 91b8453..40a8683 100644 --- a/frontend/src/features/Catalog/EditCatalogItem.tsx +++ b/frontend/src/features/Catalog/EditCatalogItem.tsx @@ -1,5 +1,5 @@ import { - Button, + Button, Container, Dialog, DialogContent, Divider, @@ -8,8 +8,8 @@ import { Stack, TextField, Typography, } from '@mui/material' -import { Fragment, FunctionComponent, useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' +import {Fragment, FunctionComponent, MouseEventHandler, useEffect, useState} from 'react' +import {useNavigate, useParams} from 'react-router-dom' import axiosInstance from '../../api/api' import { CatalogItemDto } from '../../swagger/data-contracts' import ShowErrorIfPresent from '../Reusables/ShowErrorIfPresent' @@ -21,9 +21,13 @@ import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils' import {RootState} from "../redux/store" import {useDispatch, useSelector} from "react-redux" import * as yup from "yup" -import {FieldArray, ErrorMessage, Field, Form, useFormik} from "formik" -import {register} from "../Auth/userThunks" +import {Formik, FieldArray, ErrorMessage, Field, Form, useFormik} from "formik" import {updateCatalogItem} from "./catalogItemThunks" +import AddIcon from '@mui/icons-material/Add'; +import { IconButton } from '@mui/material'; +import ClearIcon from '@mui/icons-material/Clear'; +import SaveIcon from '@mui/icons-material/Save' +import {resetRequestCompleted} from "./catalogItemSlice" const apiError = 'Error while fetching data from the server, please try again later.' @@ -47,18 +51,31 @@ const EditCatalogItem: FunctionComponent<CatalogItemEditProps> = ({ const [item, setItem] = useState<CatalogItemDto | undefined>(undefined) const [isItemLoading, setIsItemLoading] = useState(true) const [err, setErr] = useState<string | undefined>(undefined) + const loading = useSelector((state: RootState) => state.catalogItem.loading) + const requestCompleted = useSelector((state: RootState) => state.catalogItem.isRequestCompleted) const canEditCatalog = useSelector( (state: RootState) => state.user.roles.includes("WRITE") ) + const navigate = useNavigate() const dispatch = useDispatch() + useEffect(() => { + if (requestCompleted) { + dispatch(resetRequestCompleted()) + navigate(`/catalog/${itemId as string}`) + } + }, [requestCompleted, navigate]) + // Fetch the item from the api after mounting the component useEffect(() => { // Function to fetch the item from the api const fetchItem = async () => { try { + if (!itemId || itemId === "") { + return + } const { data, status } = await axiosInstance.get( `/catalog-items/${itemId}` ) @@ -79,154 +96,28 @@ const EditCatalogItem: FunctionComponent<CatalogItemEditProps> = ({ const validationSchema = yup.object().shape({ name: yup.string().required('Item name is required'), - longitude: yup.number().required('Longitude has to be a number'), - latitude: yup.number().required('Latitude has to be a number'), - certainty: yup.number().required('Certainty has to be a number'), + longitude: yup.number() + .typeError('Longitude must be a number') + .positive('Longitude must be greater than zero'), + latitude: yup.number() + .typeError('Latitude must be a number'), + certainty: yup.number() + .typeError('Certainty must be a number') }) - const formik = useFormik({ - initialValues: { - name: item?.name, - allNames: item?.allNames, - writtenForms: item?.writtenForms, - types: item?.types, - countries: item?.countries, - bibliography: item?.bibliography, - longitude: item?.longitude, - latitude: item?.latitude, - certainty: item?.certainty, - description: item?.description, - }, - validationSchema, - onSubmit: () => { - const response = dispatch( - updateCatalogItem({ - item: { - id: item?.id, - name: formik.values.name, - allNames: formik.values.allNames, - writtenForms: formik.values.writtenForms, - types: formik.values.types, - countries: formik.values.countries, - bibliography: formik.values.bibliography, - longitude: formik.values.longitude, - latitude: formik.values.latitude, - certainty: formik.values.certainty, - description: formik.values.description, - } - }) - ) - - }, - }) - - - const handleFormChangeExpandable = (index: number, event: React.ChangeEvent<HTMLInputElement>, array: string[], key: string | undefined) => { - if (!rows.filter(v => v.key === key)[0].items) { - rows.filter(v => v.key === key)[0].items = [event.target.value] - } else { - // @ts-ignore - rows.filter(v => v.key === key)[0].items[index] = event.target.value - } - } - - const handleFormChange = (event: React.ChangeEvent<HTMLInputElement>, array: string[], key: string | undefined) => { - if (!rows.filter(v => v.key === key)[0].items) { - rows.filter(v => v.key === key)[0].items = [event.target.value] - } else { - // @ts-ignore - rows.filter(v => v.key === key)[0].items[index] = event.target.value - } + const initialValues = { + name: item?.name ? item?.name : "", + allNames: item?.allNames ? item?.allNames : [""], + writtenForms: item?.writtenForms ? item?.writtenForms : [""], + types: item?.types ? item?.types : [""], + countries: item?.countries ? item?.countries : [""], + bibliography: item?.bibliography ? item?.bibliography : [""], + longitude: undefined, + latitude: undefined, + certainty: undefined, + description: "", } - // Maps catalogItem property to corresponding table row - const mapToRow = (row: RowData) => ( - <Fragment> - <Grid sx={{ my: 2 }} container justifyContent="space-around"> - <Grid item xs={4} sx={{ px: 1 }}> - <Typography fontWeight={500}>{row.rowName}</Typography> - </Grid> - {/*<Grid item xs={8} sx={{ ml: 'auto' }}>*/} - {/* {row.isExpandable ? (*/} - {/* row.items?.map((value, index, array) => (*/} - {/* <input*/} - {/* name='name'*/} - {/* placeholder='Name'*/} - {/* value={formik.values[row.key]?.at(index)}*/} - {/* onChange={event => handleFormChangeExpandable(index, event, array, row.key)}*/} - {/* />*/} - {/* ))*/} - {/* ) : (*/} - - {/* )}*/} - - {/*</Grid>*/} - </Grid> - </Fragment> - ) - - - - // Catalog item rows - const rows: RowData[] = [ - { - rowName: 'Name', - items: [item?.name as string], - isExpandable: false, - key: 'name' - }, - { - rowName: 'All Names', - items: item?.allNames, - isExpandable: true, - key: 'allNames' - }, - { - rowName: 'Written Forms', - items: item?.writtenForms, - isExpandable: true, - key: 'writtenForms' - }, - { - rowName: 'Type', - items: item?.types, - isExpandable: true, - key: 'types' - }, - { - rowName: 'State or Territory', - items: item?.countries, - isExpandable: true, - key: 'countries' - }, - { - rowName: 'Longitude', - // Must be in array otherwise the string gets iterated - items: [item?.longitude as unknown as string], - isExpandable: false, - key: 'countries' - }, - { - rowName: 'Latitude', - // Must be in array otherwise the string gets iterated - items: [item?.latitude as unknown as string], - isExpandable: false, - key: 'latitude' - }, - { - rowName: 'Certainty', - items: [item?.certainty as unknown as string], - isExpandable: false, - key: 'certainty' - }, - { - rowName: 'Bibliography', - items: item?.bibliography, - isExpandable: false, - key: 'bibliography' - }, - ] - return ( <Fragment> {showReturnToDetailButton && ( @@ -234,80 +125,386 @@ const EditCatalogItem: FunctionComponent<CatalogItemEditProps> = ({ direction="row" alignItems="flex-start" spacing={2} - sx={{ mt: 1 }} + sx={{mt: 1}} > <Button - startIcon={<ArrowBackIosIcon />} + startIcon={<ArrowBackIosIcon/>} variant="contained" component={RouterLink} - to="/catalog" + to={`/catalog/${itemId as string}`} color="primary" - sx={{ mb: 2 }} + sx={{mb: 2}} > Back To Detail </Button> </Stack> )} - <ShowErrorIfPresent err={err} /> + <ShowErrorIfPresent err={err}/> - <Paper style={{ minHeight: '100vh' }} variant="outlined"> - {isItemLoading && !err ? <ContentLoading /> : null} + <Paper style={{minHeight: '100vh'}} variant="outlined"> + {isItemLoading && !err ? <ContentLoading/> : null} {!isItemLoading && item ? ( - <form onSubmit={formik.handleSubmit}> - <Grid sx={{ my: 2 }} container justifyContent="space-around"> - <Grid item xs={4} sx={{ px: 1 }}> - <Typography fontWeight={500}>Name</Typography> - </Grid> - <Grid item xs={8} sx={{ ml: 'auto' }}> - <TextField - fullWidth - label="Name" - name="name" - sx={{ mb: 2 }} - value={formik.values.name} - onChange={formik.handleChange} - error={ - Boolean(formik.errors.name) && - formik.touched.name - } - helperText={ - formik.errors.name && - formik.touched.name && - formik.errors.name + <Formik + initialValues={initialValues} + validationSchema={validationSchema} + onSubmit={values => { + console.log("submit called") + console.log(values) + const response = dispatch( + updateCatalogItem({ + item: { + id: item?.id, + name: values.name, + allNames: values.allNames, + writtenForms: values.writtenForms, + types: values.types, + countries: values.countries, + bibliography: values.bibliography, + longitude: values.longitude, + latitude: values.latitude, + certainty: values.certainty, + description: values.description, } + }) + ) + console.log(response) + }} + render={({values, handleChange, errors, touched}) => ( + + + <Form> + <Container sx={{m: 2}} maxWidth={false}> + <TextField + fullWidth + label="Name" + name="name" + value={values.name} + onChange={handleChange} + error={ + Boolean(errors.name) && + touched.name + } + helperText={ + errors.name && + touched.name && + errors.name + } + /> + </Container> + <Divider textAlign="left">Alternative Names</Divider> + <FieldArray + name="allNames" + render={arrayHelpers => ( + <Container maxWidth={false}> + {values.allNames && values.allNames.length > 0 ? ( + values.allNames.map((altName, i) => ( + <Grid + sx={{m: 2}} container justifyContent="space-around" + key={i}> + <Grid item xs={8} md={10} xl={11} > + <TextField + fullWidth + label="Alternative Name" + name={`allNames.${i}`} + + value={values.allNames[i]} + onChange={handleChange} + /> + </Grid> + <Grid item xs={4} md={2} lg={2} xl={1} sx={{p: 1}}> + <IconButton + color="primary" + onClick={() => arrayHelpers.insert(i + 1, '')} + + > + <AddIcon/> + </IconButton> + <IconButton + type="button" + color="error" + key={i} + onClick={() => arrayHelpers.remove(i)} + + > + <ClearIcon/> + </IconButton> + </Grid> + + </Grid> + + )) + ) : null} + </Container> + )} + /> + <Divider textAlign="left">Written Forms</Divider> + <FieldArray + name="writtenForms" + render={arrayHelpers => ( + <Container maxWidth={false}> + {values.writtenForms && values.writtenForms.length > 0 ? ( + values.writtenForms.map((writtenForm, i) => ( + <Grid + sx={{m: 2}} container justifyContent="space-around" + key={i}> + <Grid item xs={8} md={10} xl={11} > + <TextField + fullWidth + label="Written Form" + name={`writtenForms.${i}`} + + value={values.writtenForms[i]} + onChange={handleChange} + /> + </Grid> + <Grid item xs={4} md={2} lg={2} xl={1} sx={{p: 1}}> + <IconButton + color="primary" + onClick={() => arrayHelpers.insert(i + 1, '')} + + > + <AddIcon/> + </IconButton> + <IconButton + type="button" + color="error" + key={i} + onClick={() => arrayHelpers.remove(i)} + + > + <ClearIcon/> + </IconButton> + </Grid> + </Grid> + )) + ) : null} + </Container> + )} + /> + <Divider textAlign="left">Types</Divider> + <FieldArray + name="types" + render={arrayHelpers => ( + <Container maxWidth={false}> + {values.types && values.types.length > 0 ? ( + values.types.map((type, i) => ( + <Grid + sx={{m: 2}} container justifyContent="space-around" + key={i}> + <Grid item xs={8} md={10} xl={11} > + <TextField + fullWidth + label="Type" + name={`types.${i}`} + + value={values.types[i]} + onChange={handleChange} + /> + </Grid> + <Grid item xs={4} md={2} lg={2} xl={1} sx={{p: 1}}> + <IconButton + color="primary" + onClick={() => arrayHelpers.insert(i + 1, '')} + + > + <AddIcon/> + </IconButton> + <IconButton + type="button" + color="error" + key={i} + onClick={() => arrayHelpers.remove(i)} + + > + <ClearIcon/> + </IconButton> + </Grid> + </Grid> + )) + ) : null} + </Container> + )} + /> + <Divider textAlign="left">State or Territory</Divider> + <FieldArray + name="countries" + render={arrayHelpers => ( + <Container maxWidth={false}> + {values.countries && values.countries.length > 0 ? ( + values.countries.map((country, i) => ( + <Grid + sx={{m: 2}} container justifyContent="space-around" + key={i}> + <Grid item xs={8} md={10} xl={11} > + <TextField + fullWidth + label="State or Territory" + name={`countries.${i}`} + value={values.countries[i]} + onChange={handleChange} + /> + </Grid> + <Grid item xs={4} md={2} lg={2} xl={1} sx={{p: 1}}> + <IconButton + color="primary" + onClick={() => arrayHelpers.insert(i + 1, '')} + > + <AddIcon/> + </IconButton> + <IconButton + type="button" + color="error" + key={i} + onClick={() => arrayHelpers.remove(i)} + > + <ClearIcon/> + </IconButton> + </Grid> + </Grid> + )) + ) : null} + </Container> + )} + /> + <Divider textAlign="left">Coordinates</Divider> + <Container maxWidth={false}> + <Grid + sx={{m: 2}} container justifyContent="space-around" + > + <Grid item xs={6} sx={{pr: 1}}> + <TextField + fullWidth + label="Latitude" + name="latitude" + value={values.latitude} + onChange={handleChange} + error={ + Boolean(errors.latitude) && + touched.latitude + } + helperText={ + errors.latitude && + touched.latitude && + errors.latitude + } + /> + </Grid> + <Grid item xs={6} sx={{pl: 1}}> + <TextField + fullWidth + label="Longitude" + name="longitude" + + value={values.latitude} + onChange={handleChange} + error={ + Boolean(errors.longitude) && + touched.longitude + } + helperText={ + errors.longitude && + touched.longitude && + errors.longitude + } + /> + </Grid> + </Grid> + </Container> + <Divider textAlign="left">Certainty</Divider> + <Container sx={{m: 2}} maxWidth={false}> + <TextField + fullWidth + label="Certainty" + name="certainty" + + value={values.certainty} + onChange={handleChange} + error={ + Boolean(errors.certainty) && + touched.certainty + } + helperText={ + errors.certainty && + touched.certainty && + errors.certainty + } + /> + </Container> + <Divider textAlign="left">Bibliography</Divider> + <FieldArray + name="bibliography" + render={arrayHelpers => ( + <Container maxWidth={false}> + {values.bibliography && values.bibliography.length > 0 ? ( + values.bibliography.map((b, i) => ( + <Grid + sx={{m: 2}} container justifyContent="space-around" + key={i}> + <Grid item xs={8} md={10} xl={11}> + <TextField + fullWidth + label="Bibliography" + name={`bibliography.${i}`} + value={values.bibliography[i]} + onChange={handleChange} + /> + </Grid> + <Grid item xs={4} md={2} lg={2} xl={1} sx={{p: 1}}> + <IconButton + color="primary" + onClick={() => arrayHelpers.insert(i + 1, '')} + > + <AddIcon/> + </IconButton> + <IconButton + type="button" + color="error" + key={i} + onClick={() => arrayHelpers.remove(i)} + > + <ClearIcon/> + </IconButton> + </Grid> + </Grid> + )) + ) : null} + </Container> + )} /> - </Grid> - <Grid item xs={4} sx={{ px: 1 }}> - <Typography fontWeight={500}>All Names</Typography> - </Grid> - <Grid item xs={8} sx={{ ml: 'auto' }}> - <FieldArray name="allNames"> - {() => (formik.values.allNames?.map((allName, i) => { - return ( - <div key={i} className="list-group list-group-flush"> - <div className="list-group-item"> - <h5 className="card-title">Ticket {i + 1}</h5> - <div className="form-row"> - <div className="form-group col-6"> - <label>Name</label> - <Field name={`tickets.${i}.name`} type="text" /> - <ErrorMessage name={`tickets.${i}.name`} component="div" /> - </div> - <div className="form-group col-6"> - <label>Email</label> - <Field name={`tickets.${i}.email`} type="text" /> - <ErrorMessage name={`tickets.${i}.email`} component="div" /> - </div> - </div> - </div> - </div> - ); - }))} - </FieldArray> - - </Grid> - </Grid> - </form> + <Divider textAlign="left">Description</Divider> + <Container sx={{m: 2}} maxWidth={false}> + <TextField + fullWidth + label="Description" + name="description" + multiline + rows={3} + value={values.description} + onChange={handleChange} + /> + </Container> + <Container + maxWidth={false} + style={{ + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-end' + }} + > + <Button + sx={{ m: 2}} + startIcon={<SaveIcon />} + variant="contained" + color="primary" + type="submit" + disabled={loading} + > + Save + </Button> + </Container> + </Form> + )} + /> ) : null} </Paper> </Fragment> @@ -316,7 +513,10 @@ const EditCatalogItem: FunctionComponent<CatalogItemEditProps> = ({ export const RoutedCatalogItemEdit = () => { const { itemId } = useParams() - return <EditCatalogItem itemId={itemId ?? ''} /> + return <EditCatalogItem + itemId={itemId ?? ''} + showReturnToDetailButton + /> } export const DialogCatalogItemEdit: FunctionComponent< diff --git a/frontend/src/features/Catalog/catalogItemSlice.ts b/frontend/src/features/Catalog/catalogItemSlice.ts index 8aaf545..0330a4b 100644 --- a/frontend/src/features/Catalog/catalogItemSlice.ts +++ b/frontend/src/features/Catalog/catalogItemSlice.ts @@ -3,6 +3,7 @@ import { CatalogItemDto } from '../../swagger/data-contracts' import {updateCatalogItem, updateCatalogItemsBulk} from "./catalogItemThunks" export interface CatalogItemState { + isRequestCompleted: boolean loading: boolean // whether the catalog is loading error?: string } @@ -16,6 +17,7 @@ export interface UpdateCatalogItemsBulk { } const initialState: CatalogItemState = { + isRequestCompleted: false, loading: false, error: undefined } @@ -26,6 +28,7 @@ const catalogItemSlice = createSlice({ reducers: { clear: (state: CatalogItemState) => ({ ...initialState }), setLoading: (state: CatalogItemState) => ({ ...state, loading: true }), + resetRequestCompleted: (state: CatalogItemState) => ({ ...state, isRequestCompleted: false }), resetLoading: (state: CatalogItemState) => ({ ...state, loading: false }), consumeError: (state: CatalogItemState) => ({ ...state, error: undefined }), }, @@ -37,6 +40,7 @@ const catalogItemSlice = createSlice({ builder.addCase(updateCatalogItem.fulfilled, (state: CatalogItemState, action: any) => ({ ...state, loading: false, + isRequestCompleted: true })) builder.addCase(updateCatalogItem.rejected, (state: CatalogItemState, action: any) => ({ ...state, @@ -50,6 +54,7 @@ const catalogItemSlice = createSlice({ builder.addCase(updateCatalogItemsBulk.fulfilled, (state: CatalogItemState, action: any) => ({ ...state, loading: false, + isRequestCompleted: true })) builder.addCase(updateCatalogItemsBulk.rejected, (state: CatalogItemState, action: any) => ({ ...state, @@ -64,6 +69,7 @@ export const { setLoading, resetLoading, consumeError, + resetRequestCompleted, } = catalogItemSlice.actions -const reducer = catalogItemSlice.reducer -export default reducer +const catalogItemReducer = catalogItemSlice.reducer +export default catalogItemReducer diff --git a/frontend/src/features/Catalog/catalogItemThunks.tsx b/frontend/src/features/Catalog/catalogItemThunks.tsx index 10df9f4..167cae7 100644 --- a/frontend/src/features/Catalog/catalogItemThunks.tsx +++ b/frontend/src/features/Catalog/catalogItemThunks.tsx @@ -18,7 +18,7 @@ export const updateCatalogItem = createAsyncThunk( // If the request was successful return the items if (status === 200) { - return data + return status } return Promise.reject(apiError) diff --git a/frontend/src/features/redux/store.ts b/frontend/src/features/redux/store.ts index 9dd6113..57e32e0 100644 --- a/frontend/src/features/redux/store.ts +++ b/frontend/src/features/redux/store.ts @@ -10,6 +10,7 @@ import trackingToolReducer from '../TrackingTool/trackingToolSlice' import usersDetailReducer from '../Administration/userDetailSlice' import { enableMapSet } from 'immer' import navigationReducer from '../Navigation/navigationSlice' +import catalogItemReducer from "../Catalog/catalogItemSlice" enableMapSet() @@ -24,6 +25,7 @@ const store = createStore( notification: notificationReducer, trackingTool: trackingToolReducer, usersDetail: usersDetailReducer, + catalogItem: catalogItemReducer, navigation: navigationReducer, }), process.env.REACT_APP_DEV_ENV === 'true' -- GitLab