-
{{ $t("general.new") }}
+
+
+ {{ $t("general.new") }}
@@ -263,8 +271,8 @@ import {
computed,
defineComponent,
reactive,
- ref,
toRefs,
+ useContext,
useMeta,
useRoute,
useRouter,
@@ -292,6 +300,7 @@ import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientE
import RecipeIngredientParserMenu from "~/components/Domain/Recipe/RecipeIngredientParserMenu.vue";
import { Recipe } from "~/types/api-types/recipe";
import { useStaticRoutes } from "~/composables/api";
+import { uuid4 } from "~/composables/use-uuid";
export default defineComponent({
components: {
@@ -318,21 +327,53 @@ export default defineComponent({
const router = useRouter();
const slug = route.value.params.slug;
const api = useApiSingleton();
- const imageKey = ref(1);
- const { getBySlug, loading } = useRecipeContext();
+ const state = reactive({
+ form: false,
+ scale: 1,
+ hideImage: false,
+ imageKey: 1,
+ loadFailed: false,
+ skeleton: false,
+ jsonEditor: false,
+ jsonEditorOptions: {
+ mode: "code",
+ search: false,
+ mainMenuBar: false,
+ },
+ });
+
+ const { getBySlug, loading, fetchRecipe } = useRecipeContext();
const { recipeImage } = useStaticRoutes();
+ // @ts-ignore
+ const { $vuetify } = useContext();
+
const recipe = getBySlug(slug);
- const form = ref
(false);
+ // ===========================================================================
+ // Layout Helpers
- useMeta(() => ({ title: recipe?.value?.name || "Recipe" }));
+ const enableLandscape = computed(() => {
+ const preferLandscape = recipe?.value?.settings?.landscapeView;
+ const smallScreen = !$vuetify.breakpoint.smAndUp;
+
+ if (preferLandscape) {
+ return true;
+ } else if (smallScreen) {
+ return true;
+ }
+
+ return false;
+ });
+
+ // ===========================================================================
+ // Button Click Event Handlers
async function updateRecipe(slug: string, recipe: Recipe) {
const { data } = await api.recipes.updateOne(slug, recipe);
- form.value = false;
+ state.form = false;
if (data?.slug) {
router.push("/recipe/" + data.slug);
}
@@ -345,6 +386,16 @@ export default defineComponent({
}
}
+ async function closeEditor() {
+ state.form = false;
+ state.jsonEditor = false;
+ recipe.value = await fetchRecipe(slug);
+ }
+
+ function toggleJson() {
+ state.jsonEditor = !state.jsonEditor;
+ }
+
const scaledYield = computed(() => {
const regMatchNum = /\d+/;
const yieldString = recipe.value?.recipeYield;
@@ -366,11 +417,7 @@ export default defineComponent({
if (newVersion?.data?.version) {
recipe.value.image = newVersion.data.version;
}
- imageKey.value++;
- }
-
- function removeByIndex(list: Array, index: number) {
- list.splice(index, 1);
+ state.imageKey++;
}
function addStep(steps: Array | null = null) {
@@ -393,10 +440,11 @@ export default defineComponent({
if (ingredients?.length) {
const newIngredients = ingredients.map((x) => {
return {
+ ref: uuid4(),
title: "",
note: x,
- unit: {},
- food: {},
+ unit: null,
+ food: null,
disableAmount: true,
quantity: 1,
};
@@ -407,24 +455,22 @@ export default defineComponent({
}
} else {
recipe?.value?.recipeIngredient?.push({
+ ref: uuid4(),
title: "",
note: "",
- unit: {},
- food: {},
+ unit: null,
+ food: null,
disableAmount: true,
quantity: 1,
});
}
}
- const state = reactive({
- scale: 1,
- });
-
// ===============================================================
// Metadata
const structuredData = computed(() => {
+ // TODO: Get this working with other scrapers, unsure why it isn't properly being delivered to clients.
return {
"@context": "http://schema.org",
"@type": "Recipe",
@@ -450,34 +496,21 @@ export default defineComponent({
});
return {
+ enableLandscape,
scaledYield,
+ toggleJson,
...toRefs(state),
- imageKey,
recipe,
api,
- form,
loading,
addStep,
deleteRecipe,
+ closeEditor,
updateRecipe,
uploadImage,
validators,
recipeImage,
addIngredient,
- removeByIndex,
- };
- },
- data() {
- return {
- hideImage: false,
- loadFailed: false,
- skeleton: false,
- jsonEditor: false,
- jsonEditorOptions: {
- mode: "code",
- search: false,
- mainMenuBar: false,
- },
};
},
head: {},
diff --git a/frontend/types/api-types/recipe.ts b/frontend/types/api-types/recipe.ts
index ed585ec83efb..5033d4fb64c7 100644
--- a/frontend/types/api-types/recipe.ts
+++ b/frontend/types/api-types/recipe.ts
@@ -80,10 +80,11 @@ export interface Recipe {
comments?: CommentOut[];
}
export interface RecipeIngredient {
+ ref: string;
title: string;
note: string;
- unit: RecipeIngredientUnit;
- food: RecipeIngredientFood;
+ unit?: RecipeIngredientUnit | null;
+ food?: RecipeIngredientFood | null;
disableAmount: boolean;
quantity: number;
}
diff --git a/mealie/schema/recipe/recipe_ingredient.py b/mealie/schema/recipe/recipe_ingredient.py
index add5c31d0a67..7189959b7cba 100644
--- a/mealie/schema/recipe/recipe_ingredient.py
+++ b/mealie/schema/recipe/recipe_ingredient.py
@@ -1,7 +1,11 @@
import enum
from typing import Optional, Union
+from uuid import UUID, uuid4
from fastapi_camelcase import CamelModel
+from pydantic import Field
+
+uuid4()
class CreateIngredientFood(CamelModel):
@@ -36,6 +40,11 @@ class RecipeIngredient(CamelModel):
disable_amount: bool = True
quantity: float = 1
+ # Ref is used as a way to distinguish between an individual ingredient on the frontend
+ # It is required for the reorder and section titles to function properly because of how
+ # Vue handles reactivity. ref may serve another purpose in the future.
+ ref: UUID = Field(default_factory=uuid4)
+
class Config:
orm_mode = True