@@ -132,7 +140,6 @@ export default defineComponent({
workingUnitData.name = unitSearch.value;
await unitActions.createOne();
value.unit = units.value?.find((unit) => unit.name === unitSearch.value);
- console.log(value.unit);
}
const state = reactive({
@@ -149,7 +156,23 @@ export default defineComponent({
}
}
+ function handleUnitEnter() {
+ if (value.unit === null || !value.unit.name.includes(unitSearch.value)) {
+ console.log("Creating");
+ createAssignUnit();
+ }
+ }
+
+ function handleFoodEnter() {
+ if (value.food === null || !value.food.name.includes(foodSearch.value)) {
+ console.log("Creating");
+ createAssignFood();
+ }
+ }
+
return {
+ handleUnitEnter,
+ handleFoodEnter,
...toRefs(state),
createAssignFood,
createAssignUnit,
diff --git a/frontend/components/Domain/Recipe/RecipeIngredientParserMenu.vue b/frontend/components/Domain/Recipe/RecipeIngredientParserMenu.vue
deleted file mode 100644
index 4456249275ff..000000000000
--- a/frontend/components/Domain/Recipe/RecipeIngredientParserMenu.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
- {{ $globals.icons.foods }}
-
- Parse
-
-
-
-
- {{ $globals.icons.alert }} Experimental
-
-
-
- Mealie can use natural language processing to attempt to parse and create units, and foods for your Recipe
- ingredients. This is experimental and may not work as expected. If you choose to not use the parsed results you
- can click the close button at the top of the page and your changes will not be saved.
-
-
-
-
-
- {{ $globals.icons.check }}
-
- {{ $t("general.confirm") }}
-
-
-
-
-
-
-
diff --git a/frontend/components/global/BaseDialog.vue b/frontend/components/global/BaseDialog.vue
index da3b3133c47c..78e831d2ed36 100644
--- a/frontend/components/global/BaseDialog.vue
+++ b/frontend/components/global/BaseDialog.vue
@@ -26,7 +26,14 @@
-
+
{{ $t("general.cancel") }}
diff --git a/frontend/composables/use-utils.ts b/frontend/composables/use-utils.ts
index 073451b77373..2bd5cafbb0e5 100644
--- a/frontend/composables/use-utils.ts
+++ b/frontend/composables/use-utils.ts
@@ -8,3 +8,49 @@ export function uuid4() {
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
}
+
+// https://stackoverflow.com/questions/28876300/deep-copying-array-of-nested-objects-in-javascript
+const toString = Object.prototype.toString;
+export function deepCopy(obj: any) {
+ let rv;
+
+ switch (typeof obj) {
+ case "object":
+ if (obj === null) {
+ // null => null
+ rv = null;
+ } else {
+ switch (toString.call(obj)) {
+ case "[object Array]":
+ // It's an array, create a new array with
+ // deep copies of the entries
+ rv = obj.map(deepCopy);
+ break;
+ case "[object Date]":
+ // Clone the date
+ rv = new Date(obj);
+ break;
+ case "[object RegExp]":
+ // Clone the RegExp
+ rv = new RegExp(obj);
+ break;
+ // ...probably a few others
+ default:
+ // Some other kind of object, deep-copy its
+ // properties into a new object
+ rv = Object.keys(obj).reduce(function (prev, key) {
+ // @ts-ignore
+ prev[key] = deepCopy(obj[key]);
+ return prev;
+ }, {});
+ break;
+ }
+ }
+ break;
+ default:
+ // It's a primitive, copy via assignment
+ rv = obj;
+ break;
+ }
+ return rv;
+}
diff --git a/frontend/package.json b/frontend/package.json
index c11574f8d5a9..25c228989d3e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,7 +22,7 @@
"@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/pwa": "^3.3.5",
"@vue/composition-api": "^1.0.5",
- "@vueuse/core": "^5.2.0",
+ "@vueuse/core": "^6.8.0",
"core-js": "^3.15.1",
"date-fns": "^2.23.0",
"fuse.js": "^6.4.6",
@@ -35,7 +35,7 @@
"@babel/eslint-parser": "^7.14.7",
"@nuxt/types": "^2.15.7",
"@nuxt/typescript-build": "^2.1.0",
- "@nuxtjs/composition-api": "^0.28.0",
+ "@nuxtjs/composition-api": "^0.30.0",
"@nuxtjs/eslint-config-typescript": "^6.0.1",
"@nuxtjs/eslint-module": "^3.0.2",
"@nuxtjs/vuetify": "^1.12.1",
@@ -54,4 +54,4 @@
"resolutions": {
"vite": "2.3.8"
}
-}
+}
\ No newline at end of file
diff --git a/frontend/pages/recipe/_slug/index.vue b/frontend/pages/recipe/_slug/index.vue
index 0ec69786a517..12db397841d3 100644
--- a/frontend/pages/recipe/_slug/index.vue
+++ b/frontend/pages/recipe/_slug/index.vue
@@ -120,7 +120,26 @@
/>
-
+
+
+
+
+
+ {{ $globals.icons.foods }}
+
+ Parse
+
+
+
+ {{
+ recipe.settings.disableAmount ? "Enable ingredient amounts to use this feature" : "Parse ingredients"
+ }}
+
{{ $t("general.new") }}
@@ -279,6 +298,7 @@ import {
computed,
defineComponent,
reactive,
+ ref,
toRefs,
useContext,
useMeta,
@@ -288,6 +308,7 @@ import {
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable";
+import { invoke, until } from "@vueuse/core";
import RecipeCategoryTagSelector from "@/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue";
import { useUserApi, useStaticRoutes } from "~/composables/api";
@@ -305,10 +326,9 @@ import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue";
import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue";
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
-import RecipeIngredientParserMenu from "~/components/Domain/Recipe/RecipeIngredientParserMenu.vue";
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
import { Recipe } from "~/types/api-types/recipe";
-import { uuid4 } from "~/composables/use-utils";
+import { uuid4, deepCopy } from "~/composables/use-utils";
export default defineComponent({
components: {
@@ -320,7 +340,6 @@ export default defineComponent({
RecipeDialogBulkAdd,
RecipeImageUploadBtn,
RecipeIngredientEditor,
- RecipeIngredientParserMenu,
RecipeIngredients,
RecipeInstructions,
RecipeNotes,
@@ -331,12 +350,31 @@ export default defineComponent({
RecipeTimeCard,
VueMarkdown,
},
+ async beforeRouteLeave(_to, _from, next) {
+ const isSame = JSON.stringify(this.recipe) === JSON.stringify(this.originalRecipe);
+
+ console.log({ working: this.recipe, saved: this.originalRecipe });
+
+ if (this.form && !isSame) {
+ if (window.confirm("You have unsaved changes. Do you want to save before leaving?")) {
+ // @ts-ignore
+ await this.api.recipes.updateOne(this.recipe.slug, this.recipe);
+ }
+ }
+ next();
+ },
+
setup() {
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const api = useUserApi();
+ // ===============================================================
+ // Check Before Leaving
+
+ const domSaveChangesDialog = ref(null);
+
const state = reactive({
form: false,
scale: 1,
@@ -354,6 +392,16 @@ export default defineComponent({
const { recipe, loading, fetchRecipe } = useRecipe(slug);
+ // Manage a deep copy of the recipe so we can detect if changes have occured and inform
+ // the user if they try to navigate away from the page without saving.
+ const originalRecipe = ref(null);
+
+ invoke(async () => {
+ await until(recipe).not.toBeNull();
+
+ originalRecipe.value = deepCopy(recipe.value);
+ });
+
const { recipeImage } = useStaticRoutes();
// @ts-ignore
@@ -504,6 +552,8 @@ export default defineComponent({
});
return {
+ originalRecipe,
+ domSaveChangesDialog,
enableLandscape,
scaledYield,
toggleJson,
diff --git a/frontend/pages/recipe/_slug/ingredient-parser.vue b/frontend/pages/recipe/_slug/ingredient-parser.vue
index b7788b4cc1e1..c96207efc7b7 100644
--- a/frontend/pages/recipe/_slug/ingredient-parser.vue
+++ b/frontend/pages/recipe/_slug/ingredient-parser.vue
@@ -1,6 +1,15 @@
+
+ Experimental Feature
+
+ Mealie can use natural language processing to attempt to parse and create units, and foods for your Recipe
+ ingredients. This is experimental and may not work as expected. If you choose to not use the parsed results
+ you can seleect cancel and your changes will not be saved.
+
+
+
To use the ingredient parser, click the "Parse All" button and the process will start. When the processed
ingredients are available, you can look through the items and verify that they were parsed correctly. The models
@@ -30,13 +39,14 @@
-