Browse Source

fix saving/loading issue where changes were not correctly updated after having restructured the routing by adding a saving animation and staying on the site until saving is finished

Ramona Plogmann 1 year ago
parent
commit
b0014ea915

+ 44 - 0
client/src/components/Buttons/SavingButton.jsx

@@ -0,0 +1,44 @@
+import React from "react";
+import { Button, CircularProgress } from "@material-ui/core";
+import { bool } from "prop-types";
+import { makeStyles } from "@material-ui/core/styles";
+
+const useStyles = makeStyles(theme => ({
+  loadingCircle: {
+    position: 'absolute',
+    top: '50%',
+    left: '50%',
+    marginTop: '-12px',
+    marginLeft: '-12px',
+  }
+}));
+
+// while saving display progress spinner
+const SavingButton = ({ children, ...props }) => {
+
+  const classes = useStyles();
+  const { isSaving, disabled, ...buttonProps } = props;
+
+  return (
+    <>
+      <Button disabled={isSaving || disabled} {...buttonProps}>
+        {children}
+        {isSaving && (
+          <CircularProgress size={24} color="inherit" className={classes.loadingCircle} />
+        )}
+      </Button>
+    </>
+  );
+};
+
+SavingButton.propTypes = {
+  // true while saving, otherwise false
+  isSaving: bool.isRequired,
+  disabled: bool,
+};
+
+SavingButton.defaultProps = {
+  disabled: false,
+};
+
+export default SavingButton;

+ 4 - 4
client/src/components/ContentWrapper.jsx

@@ -44,23 +44,23 @@ const ContentWrapper = (props) => {
       <Box className={classes.content}>
         <Routes>
           <Route path="/" element={<Home />} />
-          <Route path="/meals/">
+          <Route path="meals">
             <Route index element={<OwnMeals />} />
             <Route path="add" element={<AddMeal />} />
             <Route path={'detail/:mealId'} element={<MealDetailView />} />
             <Route path={'edit/:mealId'} element={<EditMeal />} />
           </Route>
-          <Route path="/plans/">
+          <Route path="plans">
             <Route index element={<OwnPlans />} />
             <Route path="add" element={<AddPlanItem onDoneAdding={goToPlans} />} />
             <Route path="edit/:planItemId" element={<EditPlanItem />} />
             <Route path="shoppingList/:userId" element={<ShoppingList />} />
           </Route>
-          <Route path="/social/">
+          <Route path="social">
             <Route index element={<Social />} />
             <Route path="contact/:userId/:tab" element={<ContactsContent />} />
           </Route>
-          <Route path="/settings/">
+          <Route path="settings">
             <Route index element={<Settings />} />
             <Route path="editProfile" element={<EditProfile />} />
             <Route path="advanced" element={<AdvancedSettings setDarkModeInAppLevel={props.setDarkMode} />} />

+ 1 - 1
client/src/components/Images/ImageUpload.jsx

@@ -210,7 +210,7 @@ const ImageUpload = (props) => {
   return (
     <>
       {multiple ?
-        <GridList cellHeight={160} className={classes.gridList} cols={3}>
+        <GridList rowHeight={160} className={classes.gridList} cols={3}>
           <ImageGrid images={uploadedImages} allowDelete allowChoosingMain onDelete={deleteImage} onChoosingMain={chooseImageAsMain} disableSurroundingGrid>
             <GridListTile key={-1} cols={1} className={classes.dropzoneTile}>
               <PhotoDropzone multiple

+ 16 - 9
client/src/components/Meals/AddMeal.jsx

@@ -11,6 +11,7 @@ import EditMealCore from "./EditMealCore";
 import { useAuth0 } from "@auth0/auth0-react";
 import { withLoginRequired } from "../util";
 import DoneButton from "../Buttons/DoneButton";
+import SavingButton from "../Buttons/SavingButton";
 
 const useStyles = makeStyles(theme => ({
   form: {
@@ -65,6 +66,7 @@ const AddMeal = () => {
   const [meal, setMeal] = useState(emptyMeal);
   const [isLoading, setIsLoading] = useState(false);
   const [loadingImagesTakesLong, setLoadingImagesTakesLong] = useState(false);
+  const [isSaving, setIsSaving] = useState(false);
 
   const updateMeal = (key, value) => {
     setMeal(prevState => ({
@@ -77,30 +79,35 @@ const AddMeal = () => {
     navigate('/meals/');
   }
 
+  const onDoneAdding = () => {
+    setIsSaving(false);
+    goToMeals();
+  };
+
   const addNewMeal = (event) => {
     event.preventDefault();
-    addMeal(meal, goToMeals);
+    setIsSaving(true);
+    addMeal(meal, onDoneAdding);
   }
 
   return (
     <>
       <Navbar pageTitle={t('New Meal')} leftSideComponent={<BackButton onClick={() => {
-        console.log(meal.images);
-        if (meal.images) {
+        if (meal.images && meal.images.length > 0) {
           deleteAllImagesFromMeal(meal._id);
         }
         navigate(-1);
       }} />} rightSideComponent={meal.title && !isLoading ? <DoneButton onClick={addNewMeal} /> : null} />
 
       <form noValidate onSubmit={addNewMeal} className={classes.form}>
-        <EditMealCore updateMeal={updateMeal} meal={meal} autoFocusFirstInput  setImagesLoading={setIsLoading} setLoadingImagesTakesLong={setLoadingImagesTakesLong}  />
-        {meal.title && isLoading && loadingImagesTakesLong ?
+        <EditMealCore updateMeal={updateMeal} meal={meal} autoFocusFirstInput setImagesLoading={setIsLoading} setLoadingImagesTakesLong={setLoadingImagesTakesLong} />
+        {meal.title && isLoading && loadingImagesTakesLong &&
           <>
-          <Typography className={classes.waitForPictures} color="error">{t('LOADING_IMAGES_TAKES_LONG')}</Typography>
-          <Button type="submit" disabled={!meal.title} className={classes.submitWithoutImagesButton} variant='outlined'>{t('Add without images')}</Button>
-          </> : ''
+            <Typography className={classes.waitForPictures} color="error">{t('LOADING_IMAGES_TAKES_LONG')}</Typography>
+            <SavingButton isSaving={isSaving} type="submit" disabled={!meal.title} className={classes.submitWithoutImagesButton} variant='outlined'>{t('Add without images')}</SavingButton>
+          </>
         }
-        <Button type="submit" disabled={!meal.title || isLoading} className={classes.submitButton} variant='contained' color='primary'>{t('Add')}</Button>
+        <SavingButton isSaving={isSaving} type="submit" disabled={!meal.title || isLoading} className={classes.submitButton} variant='contained' color='primary'>{t('Add')}</SavingButton>
       </form>
 
     </>

+ 6 - 2
client/src/components/Meals/EditMeal.jsx

@@ -14,6 +14,7 @@ import Loading from "../Loading";
 import useSnackbars from "../util/useSnackbars";
 import DoneButton from "../Buttons/DoneButton";
 import { useLocation, useNavigate, useParams } from "react-router-dom";
+import SavingButton from "../Buttons/SavingButton";
 
 const useStyles = makeStyles((theme) => ({
   form: {
@@ -60,6 +61,7 @@ const EditMeal = (props) => {
   const [meal, setMeal] = useState();
   const [imagesLoading, setImagesLoading] = useState(false);
   const [loadingImagesTakesLong, setLoadingImagesTakesLong] = useState(false);
+  const [isSaving, setIsSaving] = useState(false);
 
   useEffect(() => {
     fetchAndUpdateMeal(mealId, setMeal);
@@ -114,9 +116,11 @@ const EditMeal = (props) => {
 
   const editAndClose = (event) => {
     event.preventDefault();
+    setIsSaving(true);
     if (meal.title) {
       axios.post(serverURL + '/meals/edit/' + meal._id, meal).then((result) => {
         console.log('edit request sent', result.data);
+        setIsSaving(false);
         navigate(-1, { state: { meal: result.data.meal } });
       });
     }
@@ -143,10 +147,10 @@ const EditMeal = (props) => {
               <DeleteButton onClick={deleteMeal} />
             </Grid>
             <Grid item xs className={classes.saveButton}>
-              <Button type="submit" disabled={!meal.title || imagesLoading} color={inverseColors ? "secondary" : "primary"} variant="contained">{t('Save')}</Button>
+              <SavingButton isSaving={isSaving} type="submit" disabled={!meal.title || imagesLoading} color={inverseColors ? "secondary" : "primary"} variant="contained">{t('Save')}</SavingButton>
             </Grid>
           </Grid>
-          {meal.title && imagesLoading && loadingImagesTakesLong ? <Button type="submit" className={classes.submitWithoutImagesButton} disabled={!meal.title} variant="outlined">{t('Save without new images')}</Button> : null }
+          {meal.title && imagesLoading && loadingImagesTakesLong ? <SavingButton isSaving={isSaving} type="submit" className={classes.submitWithoutImagesButton} disabled={!meal.title} variant="outlined">{t('Save without new images')}</SavingButton> : null }
         </form>
         : ''}
     </>

+ 1 - 1
client/src/components/Meals/EditMealCore.jsx

@@ -139,7 +139,7 @@ EditMealCore.defaultProps = {
   autoFocusFirstInput: false,
   setImagesLoading: undefined,
   setLoadingImagesTakesLong: undefined,
-  loadingImagesTakesLongAfter: 6000,
+  loadingImagesTakesLongAfter: process.env.REACT_APP_LOADING_TAKES_LONG_AFTER || 8000,
 }
 
 export default withAuthenticationRequired(EditMealCore, {

+ 0 - 39
client/src/components/Meals/MealDetailViewExtern.jsx

@@ -1,39 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useLocation, useNavigate, useParams } from "react-router-dom";
-import { useAuth0 } from "@auth0/auth0-react";
-import MealDetailView from "./MealDetailView";
-import { fetchAndUpdateMeal } from "./meals.util";
-
-/** Wrapper for MealDetailView that determines whether the viewed meal belongs to the logged in user.
- * Also shows Meal without import function to users that are not logged in. */
-const MealDetailViewExtern = (props) => {
-  const { user } = useAuth0();
-  const navigate = useNavigate();
-  const location = useLocation();
-  const { mealId } = useParams();
-
-  const [meal, setMeal] = useState(null);
-  const [own, setOwn] = useState(false);
-
-  useEffect(() => {
-    fetchAndUpdateMeal(mealId, (meal) => {
-      setMeal(meal);
-      if (user) {
-        setOwn(user.sub === meal.userId);
-      }
-    });
-  }, [mealId, user]);
-
-  const backAction = () => {
-    console.log('from', location.state.from);
-    if(location.state.from) {
-      navigate(-1);
-    } else {
-      navigate('meals');
-    }
-  };
-
-  return <MealDetailView meal={meal} allowEditing={own} closeDialog={backAction} />;
-}
-
-export default MealDetailViewExtern;

+ 0 - 26
client/src/components/Meals/MealWrapper.jsx

@@ -1,26 +0,0 @@
-import { Route, Routes } from "react-router-dom";
-import React from "react";
-import OwnMeals from "./OwnMeals";
-import AddMeal from "./AddMeal";
-import EditMeal from "./EditMeal";
-import MealDetailView from "./MealDetailView";
-
-/** Wrapper component for Meals. Makes sure that Meals component receives the complete routing path to render the correct view.
- *
- * Might become obsolete in a future version if routing is properly handled by Meals */
-const MealWrapper = () => {
-
-
-  return (
-    <>
-      <Routes>
-        <Route index element={<OwnMeals />} />
-        <Route path="add" element={<AddMeal />} />
-        <Route path={'detail/:mealId'} element={<MealDetailView />} />
-        <Route path={'edit/:mealId'} element={<EditMeal />} />
-      </Routes>
-    </>
-  );
-}
-
-export default MealWrapper;

+ 1 - 0
client/src/components/Meals/Meals.jsx

@@ -115,6 +115,7 @@ const Meals = (props) => {
       mealsByCategory.set(key, mealsWithoutCategory);
       updateIsCategoryOpen(key, categoriesInitiallyExpanded);
     }
+    // I don't remember why this was necessary but now I don't dare to remove it
     forceUpdate();
   }
 

+ 1 - 0
client/src/components/Meals/SelectMealTags.jsx

@@ -85,6 +85,7 @@ const SelectMealTags = (props) => {
     getOptionLabel: option => option.label || option, // option.label is necessary for typing new options
     getOptionValue: option => option.label || option,
     onChange: handleTagChange,
+    blurInputOnSelects: false,
   }
 
   if (allowCreate) {

+ 8 - 2
client/src/components/Plans/AddPlanItem.jsx

@@ -11,6 +11,7 @@ import { withLoginRequired } from "../util";
 import { any, array, arrayOf, bool, func, shape, string } from "prop-types";
 import DoneButton from "../Buttons/DoneButton";
 import { addPlan } from "./plans.util";
+import SavingButton from "../Buttons/SavingButton";
 
 const useStyles = makeStyles(theme => ({
   form: {
@@ -42,6 +43,7 @@ const AddPlanItem = (props) => {
   };
 
   const [planItem, setPlanItem] = useState(presetPlanItem || emptyPlanItem);
+  const [isSaving, setIsSaving] = useState(false);
 
   const updatePlanItem = (key, value) => {
     setPlanItem(prevState => ({
@@ -64,7 +66,11 @@ const AddPlanItem = (props) => {
         connectedMealId: planItem.connectedMeal ? planItem.connectedMeal._id : null,
       }
 
-      addPlan(newPlan, onDoneAdding);
+      setIsSaving(true);
+      addPlan(newPlan, () => {
+        setIsSaving(false);
+        onDoneAdding();
+      });
     }
   }
 
@@ -77,7 +83,7 @@ const AddPlanItem = (props) => {
               rightSideComponent={planItem.title ? <DoneButton label={t('Done')} onClick={addNewPlan} /> : null} />
       <form noValidate onSubmit={addNewPlan} className={classes.form}>
         <EditPlanItemCore updatePlanItem={updatePlanItem} planItem={planItem} autoFocusFirstInput={autoFocusTitle} />
-        <Button type="submit" disabled={!planItem.title} className={classes.submitButton} variant='contained' color='primary'>{t('Add Plan')}</Button>
+        <SavingButton isSaving={isSaving} type="submit" disabled={!planItem.title} className={classes.submitButton} variant='contained' color='primary'>{t('Add Plan')}</SavingButton>
       </form>
     </>
   );

+ 8 - 7
client/src/components/Plans/EditPlanItem.jsx

@@ -12,6 +12,7 @@ import Loading from "../Loading";
 import DoneButton from "../Buttons/DoneButton";
 import { useLocation, useNavigate, useParams } from "react-router-dom";
 import { getSinglePlan } from "./plans.util";
+import SavingButton from "../Buttons/SavingButton";
 
 /** Dialog page that allows user to edit plan */
 const useStyles = makeStyles((theme) => ({
@@ -39,7 +40,7 @@ const inverseColors = true;
 const serverURL = process.env.REACT_APP_SERVER_URL;
 
 /** page that allows editing a plan */
-const EditPlanItem = (props) => {
+const EditPlanItem = () => {
   const classes = useStyles();
   const { t } = useTranslation();
   const { state } = useLocation();
@@ -47,8 +48,7 @@ const EditPlanItem = (props) => {
   let { planItemId } = useParams();
   const { user } = useAuth0();
 
-  console.log(state, planItemId);
-
+  const [isSaving, setIsSaving] = useState(false);
   const [planItem, setPlanItem] = useState((state && state.planItem) ? state.planItem : null);
 
   // get planItem if it is not there
@@ -72,8 +72,11 @@ const EditPlanItem = (props) => {
         connectedMealId: planItem.connectedMeal ? planItem.connectedMeal._id : null,
       }
 
+      setIsSaving(true);
       axios.post(serverURL + '/plans/edit/' + planItem._id, newPlan).then((result) => {
-        console.log('edit request sent', result.data);
+        // editing done
+        setIsSaving(false);
+        goBackToPlans();
       });
     }
   }
@@ -89,7 +92,6 @@ const EditPlanItem = (props) => {
   const editAndClose = (event) => {
     event.preventDefault();
     editPlan();
-    goBackToPlans();
   }
 
   const goBackToPlans = () => {
@@ -97,7 +99,6 @@ const EditPlanItem = (props) => {
   }
 
   const updatePlanItem = (key, value) => {
-    console.log('editing', key, 'to', value);
     setPlanItem(prevState => ({
       ...prevState,
       [key]: value,
@@ -122,7 +123,7 @@ const EditPlanItem = (props) => {
               <DeleteButton onClick={deletePlan} />
             </Grid>
             <Grid item xs className={classes.saveButton}>
-              <Button type="submit" disabled={!planItem.title} color={inverseColors ? "secondary" : "primary"} variant="contained">{t('Save')}</Button>
+              <SavingButton isSaving={isSaving} type="submit" disabled={!planItem.title} color={inverseColors ? "secondary" : "primary"} variant="contained">{t('Save')}</SavingButton>
             </Grid>
           </Grid>
         </form>

+ 0 - 1
client/src/components/Plans/Plans.jsx

@@ -79,7 +79,6 @@ const Plans = (props) => {
   }
 
   useEffect(() => {
-    console.log("Plans ", params);
     if (own && params.planId && (!itemBeingEdited || itemBeingEdited._id !== params.planId)) {
       getSinglePlan(params.planId, setItemBeingEdited);
     }

+ 15 - 10
client/src/components/Settings/EditProfile.jsx

@@ -10,6 +10,7 @@ import Navbar from "../Navbar";
 import BackButton from "../Buttons/BackButton";
 import { muiTableBorder } from "../util";
 import { Navigate, useLocation, useNavigate } from "react-router-dom";
+import SavingButton from "../Buttons/SavingButton";
 
 const useStyles = makeStyles(theme => ({
   userProfile: {
@@ -89,6 +90,7 @@ const EditProfile = () => {
   const [usingOAuth, setUsingOAuth] = useState(false);
   const [infoCollapsed, setInfoCollapsed] = useState(false);
   const [foreignAccountProvider, setForeignAccountProvider] = useState('');
+  const [isSaving, setIsSaving] = useState(false);
 
   useEffect(() => {
     setUsingOAuth(userId.includes("oauth"));
@@ -97,8 +99,12 @@ const EditProfile = () => {
     }
   }, [userId]);
 
-  const updateUserData = () => {
-    updateUserMetadata(userId, { username: username }, null);
+  const editAndClose = (event) => {
+    event.preventDefault();
+    setIsSaving(true);
+
+    // saving is only waiting for metadata for now because in Emilia there is only google login, not email/password
+    updateUserMetadata(userId, { username: username }, onSave);
     if (!usingOAuth) {
       const newUserData = {
         name, email, nickname: username
@@ -107,6 +113,11 @@ const EditProfile = () => {
     }
   }
 
+  const onSave = () => {
+    setIsSaving(false);
+    goToSettings();
+  };
+
   const updateProfileImage = (image) => {
     const imageSrc = image.url;
     console.log('set uploaded source', imageSrc);
@@ -127,12 +138,6 @@ const EditProfile = () => {
     updateProfileImageInMetadata(null);
   }
 
-  const editAndClose = (event) => {
-    event.preventDefault();
-    updateUserData();
-    goToSettings();
-  }
-
   const goToSettings = () => {
     // send state to make Profile reload new data
     navigate('../', { state: { newUsername: username } });
@@ -194,9 +199,9 @@ const EditProfile = () => {
             </Table>
           </TableContainer>
 
-          <Button color={colorA} type="submit" variant="contained" size="large" className={classes.submitButton}>
+          <SavingButton isSaving={isSaving} color={colorA} type="submit" variant="contained" size="large" className={classes.submitButton}>
             {t('Save Changes')}
-          </Button>
+          </SavingButton>
         </form>
       </Box>
     </>

+ 3 - 5
client/src/components/util.jsx

@@ -27,9 +27,9 @@ export const Auth0ProviderWithRedirectCallback = ({ children, ...props }) => {
 
 /**
  * This HOC wraps components in Auth0's withAuthenticationRequired HOC.
- * Additionally it detects and sets the language that is set in the query (z.B. .../plans?lang=de)
+ * Additionally, it detects and sets the language that is set in the query (z.B. .../plans?lang=de)
  * @param WrappedComponent
- * @returns {React.FC<object>} new Component that can be only accessed by logged in users and sets the language based on the query parameter
+ * @returns new Component that can be only accessed by logged in users and sets the language based on the query parameter
  * @public
  */
 export const withLoginRequired = (WrappedComponent) => {
@@ -67,6 +67,4 @@ export const muiTableBorder = (theme) => {
 
 export const dateStringOptions = { year: 'numeric', month: 'numeric', day: 'numeric' };
 
-/**This is nothing, just to display the rest.*/
-const util = () => {};
-export default util;
+