mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-02 13:15:23 -04:00
fix(frontend): 🐛 fix section titles carrying over on deleted items (#765)
* fix(frontend): 🐛 fix section titles carrying over on deleted items
Added a UUID generator to generate unique id's and prevent list changes from causing proper virtual dom re-renders.
* lazy load json editor
* fix ingredient rendering error
* move text to input
* update settings styling
* improve mobile view
Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
909bc85205
commit
40462a95f1
@ -8,50 +8,71 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="headline"> {{ $t("new-recipe.bulk-add") }} </v-card-title>
|
<v-app-bar dark color="primary" class="mt-n1 mb-3">
|
||||||
|
<v-icon large left>
|
||||||
|
{{ $globals.icons.createAlt }}
|
||||||
|
</v-icon>
|
||||||
|
<v-toolbar-title class="headline"> {{ $t("new-recipe.bulk-add") }}</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<p>
|
<v-textarea
|
||||||
{{ $t("new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list") }}
|
v-model="inputText"
|
||||||
</p>
|
outlined
|
||||||
<v-textarea v-model="inputText"> </v-textarea>
|
rows="10"
|
||||||
|
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
|
||||||
|
>
|
||||||
|
</v-textarea>
|
||||||
|
<v-btn outlined color="info" small @click="trimAllLines"> Trim Whitespace </v-btn>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
|
<BaseButton cancel @click="dialog = false"> </BaseButton>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="success" text @click="save"> {{ $t("general.save") }} </v-btn>
|
<BaseButton save color="success" @click="save"> </BaseButton>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
import { reactive, toRefs } from "@nuxtjs/composition-api";
|
||||||
export default {
|
export default {
|
||||||
data() {
|
setup(_, context) {
|
||||||
return {
|
const state = reactive({
|
||||||
dialog: false,
|
dialog: false,
|
||||||
inputText: "",
|
inputText: "",
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
splitText() {
|
|
||||||
const split = this.inputText.split("\n");
|
|
||||||
|
|
||||||
split.forEach((element, index) => {
|
|
||||||
if ((element === "\n") | (element === false)) {
|
|
||||||
split.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return split;
|
function splitText() {
|
||||||
},
|
return state.inputText.split("\n").filter((line) => !(line === "\n" || !line));
|
||||||
save() {
|
}
|
||||||
this.$emit("bulk-data", this.splitText());
|
|
||||||
this.dialog = false;
|
function trimAllLines() {
|
||||||
},
|
const splitLintes = splitText();
|
||||||
|
|
||||||
|
splitLintes.forEach((element: string, index: number) => {
|
||||||
|
splitLintes[index] = element.trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
state.inputText = splitLintes.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
context.emit("bulk-data", splitText());
|
||||||
|
state.dialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
splitText,
|
||||||
|
trimAllLines,
|
||||||
|
save,
|
||||||
|
...toRefs(state),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template slot="append-outer">
|
<template slot="append-outer">
|
||||||
<v-icon class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
|
<v-icon class="handle mt-1">{{ $globals.icons.arrowUpDown }}</v-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
@ -96,6 +96,7 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
disabledSteps: [],
|
disabledSteps: [],
|
||||||
|
@ -16,14 +16,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<v-card-text class="mt-n5">
|
<v-card-text class="mt-n5 pt-6 pb-2">
|
||||||
<v-switch
|
<v-switch
|
||||||
v-for="(itemValue, key) in value"
|
v-for="(itemValue, key) in value"
|
||||||
:key="key"
|
:key="key"
|
||||||
v-model="value[key]"
|
v-model="value[key]"
|
||||||
|
xs
|
||||||
dense
|
dense
|
||||||
flat
|
class="my-1"
|
||||||
inset
|
|
||||||
:label="labels[key]"
|
:label="labels[key]"
|
||||||
hide-details
|
hide-details
|
||||||
></v-switch>
|
></v-switch>
|
||||||
|
22
frontend/components/global/RecipeJsonEditor.vue
Normal file
22
frontend/components/global/RecipeJsonEditor.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<VJsoneditor v-model="value" height="1500px" :options="options" :attrs="$attrs"></VJsoneditor>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VJsoneditor from "v-jsoneditor";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { VJsoneditor },
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -14,9 +14,17 @@ export const useRecipeContext = function () {
|
|||||||
}, slug);
|
}, slug);
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
return recipe;
|
return recipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchRecipe(slug: string) {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await api.recipes.getOne(slug);
|
||||||
|
loading.value = false;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteRecipe(slug: string) {
|
async function deleteRecipe(slug: string) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.recipes.deleteOne(slug);
|
const { data } = await api.recipes.deleteOne(slug);
|
||||||
@ -31,5 +39,5 @@ export const useRecipeContext = function () {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { loading, getBySlug, deleteRecipe, updateRecipe };
|
return { loading, getBySlug, deleteRecipe, updateRecipe, fetchRecipe };
|
||||||
};
|
};
|
||||||
|
12
frontend/composables/use-uuid.ts
Normal file
12
frontend/composables/use-uuid.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const max = 1000000;
|
||||||
|
|
||||||
|
export function uniqueId() {
|
||||||
|
return Date.now() + Math.random() * max;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uuid4() {
|
||||||
|
// @ts-ignore
|
||||||
|
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||||
|
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||||
|
);
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container
|
||||||
|
:class="{
|
||||||
|
'pa-0': $vuetify.breakpoint.smAndDown,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<v-card v-if="skeleton" :color="`white ${false ? 'darken-2' : 'lighten-4'}`" class="pa-3">
|
<v-card v-if="skeleton" :color="`white ${false ? 'darken-2' : 'lighten-4'}`" class="pa-3">
|
||||||
<v-skeleton-loader class="mx-auto" height="700px" type="card"></v-skeleton-loader>
|
<v-skeleton-loader class="mx-auto" height="700px" type="card"></v-skeleton-loader>
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-card v-else-if="recipe">
|
<v-card v-else-if="recipe">
|
||||||
|
<!-- Recipe Header -->
|
||||||
<div class="d-flex justify-end flex-wrap align-stretch">
|
<div class="d-flex justify-end flex-wrap align-stretch">
|
||||||
<v-card
|
<v-card v-if="!enableLandscape" width="50%" flat class="d-flex flex-column justify-center align-center">
|
||||||
v-if="!recipe.settings.landscapeView"
|
|
||||||
width="50%"
|
|
||||||
flat
|
|
||||||
class="d-flex flex-column justify-center align-center"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-card-title class="headline pa-0 flex-column align-center">
|
<v-card-title class="headline pa-0 flex-column align-center">
|
||||||
{{ recipe.name }}
|
{{ recipe.name }}
|
||||||
@ -21,6 +21,7 @@
|
|||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<div class="d-flex justify-center mt-5">
|
<div class="d-flex justify-center mt-5">
|
||||||
<RecipeTimeCard
|
<RecipeTimeCard
|
||||||
|
class="d-flex justify-center flex-wrap"
|
||||||
:class="true ? undefined : 'force-bottom'"
|
:class="true ? undefined : 'force-bottom'"
|
||||||
:prep-time="recipe.prepTime"
|
:prep-time="recipe.prepTime"
|
||||||
:total-time="recipe.totalTime"
|
:total-time="recipe.totalTime"
|
||||||
@ -31,14 +32,14 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
<v-img
|
<v-img
|
||||||
:key="imageKey"
|
:key="imageKey"
|
||||||
:max-width="recipe.settings.landscapeView ? null : '50%'"
|
:max-width="enableLandscape ? null : '50%'"
|
||||||
:height="hideImage ? '50' : imageHeight"
|
:height="hideImage ? '50' : imageHeight"
|
||||||
:src="recipeImage(recipe.slug, imageKey)"
|
:src="recipeImage(recipe.slug, imageKey)"
|
||||||
class="d-print-none"
|
class="d-print-none"
|
||||||
@error="hideImage = true"
|
@error="hideImage = true"
|
||||||
>
|
>
|
||||||
<RecipeTimeCard
|
<RecipeTimeCard
|
||||||
v-if="recipe.settings.landscapeView"
|
v-if="enableLandscape"
|
||||||
:class="true ? undefined : 'force-bottom'"
|
:class="true ? undefined : 'force-bottom'"
|
||||||
:prep-time="recipe.prepTime"
|
:prep-time="recipe.prepTime"
|
||||||
:total-time="recipe.totalTime"
|
:total-time="recipe.totalTime"
|
||||||
@ -54,8 +55,8 @@
|
|||||||
:logged-in="$auth.loggedIn"
|
:logged-in="$auth.loggedIn"
|
||||||
:open="form"
|
:open="form"
|
||||||
class="ml-auto"
|
class="ml-auto"
|
||||||
@close="form = false"
|
@close="closeEditor"
|
||||||
@json="jsonEditor = !jsonEditor"
|
@json="toggleJson"
|
||||||
@edit="
|
@edit="
|
||||||
jsonEditor = false;
|
jsonEditor = false;
|
||||||
form = true;
|
form = true;
|
||||||
@ -64,14 +65,20 @@
|
|||||||
@delete="deleteRecipe(recipe.slug)"
|
@delete="deleteRecipe(recipe.slug)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<!-- Editors -->
|
||||||
<v-card-text>
|
<LazyRecipeJsonEditor v-if="jsonEditor" v-model="recipe" class="mt-10" :options="jsonEditorOptions" />
|
||||||
|
<div v-else>
|
||||||
|
<v-card-text
|
||||||
|
:class="{
|
||||||
|
'px-2': $vuetify.breakpoint.smAndDown,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div v-if="form" class="d-flex justify-start align-center">
|
<div v-if="form" class="d-flex justify-start align-center">
|
||||||
<RecipeImageUploadBtn class="my-1" :slug="recipe.slug" @upload="uploadImage" @refresh="imageKey++" />
|
<RecipeImageUploadBtn class="my-1" :slug="recipe.slug" @upload="uploadImage" @refresh="imageKey++" />
|
||||||
<RecipeSettingsMenu class="my-1 mx-1" :value="recipe.settings" @upload="uploadImage" />
|
<RecipeSettingsMenu class="my-1 mx-1" :value="recipe.settings" @upload="uploadImage" />
|
||||||
</div>
|
</div>
|
||||||
<!-- Recipe Title Section -->
|
<!-- Recipe Title Section -->
|
||||||
<template v-if="!form && recipe.settings.landscapeView">
|
<template v-if="!form && !enableLandscape">
|
||||||
<v-card-title class="pa-0 ma-0 headline">
|
<v-card-title class="pa-0 ma-0 headline">
|
||||||
{{ recipe.name }}
|
{{ recipe.name }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
@ -95,6 +102,7 @@
|
|||||||
|
|
||||||
<v-textarea v-model="recipe.description" auto-grow min-height="100" :label="$t('recipe.description')">
|
<v-textarea v-model="recipe.description" auto-grow min-height="100" :label="$t('recipe.description')">
|
||||||
</v-textarea>
|
</v-textarea>
|
||||||
|
<v-text-field v-model="recipe.recipeYield" dense :label="$t('recipe.servings')"> </v-text-field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Advanced Editor -->
|
<!-- Advanced Editor -->
|
||||||
@ -103,10 +111,10 @@
|
|||||||
<draggable v-model="recipe.recipeIngredient" handle=".handle">
|
<draggable v-model="recipe.recipeIngredient" handle=".handle">
|
||||||
<RecipeIngredientEditor
|
<RecipeIngredientEditor
|
||||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||||
:key="index + 'ing-editor'"
|
:key="ingredient.ref"
|
||||||
v-model="recipe.recipeIngredient[index]"
|
v-model="recipe.recipeIngredient[index]"
|
||||||
:disable-amount="recipe.settings.disableAmount"
|
:disable-amount="recipe.settings.disableAmount"
|
||||||
@delete="removeByIndex(recipe.recipeIngredient, index)"
|
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||||
/>
|
/>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div class="d-flex justify-end mt-2">
|
<div class="d-flex justify-end mt-2">
|
||||||
@ -115,9 +123,8 @@
|
|||||||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-space-between align-center pb-3">
|
<div class="d-flex justify-space-between align-center pb-3">
|
||||||
<v-tooltip small top color="secondary darken-1">
|
<v-tooltip v-if="!form" small top color="secondary darken-1">
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="recipe.recipeYield"
|
v-if="recipe.recipeYield"
|
||||||
@ -139,7 +146,7 @@
|
|||||||
<span> Reset Scale </span>
|
<span> Reset Scale </span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
|
||||||
<template v-if="!recipe.settings.disableAmount">
|
<template v-if="!recipe.settings.disableAmount && !form">
|
||||||
<v-btn color="secondary darken-1" class="mx-1" small @click="scale > 1 ? scale-- : null">
|
<v-btn color="secondary darken-1" class="mx-1" small @click="scale > 1 ? scale-- : null">
|
||||||
<v-icon>
|
<v-icon>
|
||||||
{{ $globals.icons.minus }}
|
{{ $globals.icons.minus }}
|
||||||
@ -154,7 +161,7 @@
|
|||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
<RecipeRating
|
<RecipeRating
|
||||||
v-if="recipe.settings.landscapeView"
|
v-if="!enableLandscape"
|
||||||
:key="recipe.slug"
|
:key="recipe.slug"
|
||||||
:value="recipe.rating"
|
:value="recipe.rating"
|
||||||
:name="recipe.name"
|
:name="recipe.name"
|
||||||
@ -222,8 +229,9 @@
|
|||||||
|
|
||||||
<v-col cols="12" sm="12" md="8" lg="8">
|
<v-col cols="12" sm="12" md="8" lg="8">
|
||||||
<RecipeInstructions v-model="recipe.recipeInstructions" :edit="form" />
|
<RecipeInstructions v-model="recipe.recipeInstructions" :edit="form" />
|
||||||
<div class="d-flex">
|
<div v-if="form" class="d-flex">
|
||||||
<BaseButton v-if="form" class="ml-auto my-2" @click="addStep()"> {{ $t("general.new") }}</BaseButton>
|
<RecipeDialogBulkAdd class="ml-auto my-2 mr-1" @bulk-data="addStep" />
|
||||||
|
<BaseButton class="my-2" @click="addStep()"> {{ $t("general.new") }}</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -263,8 +271,8 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
|
||||||
toRefs,
|
toRefs,
|
||||||
|
useContext,
|
||||||
useMeta,
|
useMeta,
|
||||||
useRoute,
|
useRoute,
|
||||||
useRouter,
|
useRouter,
|
||||||
@ -292,6 +300,7 @@ import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientE
|
|||||||
import RecipeIngredientParserMenu from "~/components/Domain/Recipe/RecipeIngredientParserMenu.vue";
|
import RecipeIngredientParserMenu from "~/components/Domain/Recipe/RecipeIngredientParserMenu.vue";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
import { useStaticRoutes } from "~/composables/api";
|
import { useStaticRoutes } from "~/composables/api";
|
||||||
|
import { uuid4 } from "~/composables/use-uuid";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -318,21 +327,53 @@ export default defineComponent({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const slug = route.value.params.slug;
|
const slug = route.value.params.slug;
|
||||||
const api = useApiSingleton();
|
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();
|
const { recipeImage } = useStaticRoutes();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const { $vuetify } = useContext();
|
||||||
|
|
||||||
const recipe = getBySlug(slug);
|
const recipe = getBySlug(slug);
|
||||||
|
|
||||||
const form = ref<boolean>(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) {
|
async function updateRecipe(slug: string, recipe: Recipe) {
|
||||||
const { data } = await api.recipes.updateOne(slug, recipe);
|
const { data } = await api.recipes.updateOne(slug, recipe);
|
||||||
form.value = false;
|
state.form = false;
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
router.push("/recipe/" + 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 scaledYield = computed(() => {
|
||||||
const regMatchNum = /\d+/;
|
const regMatchNum = /\d+/;
|
||||||
const yieldString = recipe.value?.recipeYield;
|
const yieldString = recipe.value?.recipeYield;
|
||||||
@ -366,11 +417,7 @@ export default defineComponent({
|
|||||||
if (newVersion?.data?.version) {
|
if (newVersion?.data?.version) {
|
||||||
recipe.value.image = newVersion.data.version;
|
recipe.value.image = newVersion.data.version;
|
||||||
}
|
}
|
||||||
imageKey.value++;
|
state.imageKey++;
|
||||||
}
|
|
||||||
|
|
||||||
function removeByIndex(list: Array<any>, index: number) {
|
|
||||||
list.splice(index, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStep(steps: Array<string> | null = null) {
|
function addStep(steps: Array<string> | null = null) {
|
||||||
@ -393,10 +440,11 @@ export default defineComponent({
|
|||||||
if (ingredients?.length) {
|
if (ingredients?.length) {
|
||||||
const newIngredients = ingredients.map((x) => {
|
const newIngredients = ingredients.map((x) => {
|
||||||
return {
|
return {
|
||||||
|
ref: uuid4(),
|
||||||
title: "",
|
title: "",
|
||||||
note: x,
|
note: x,
|
||||||
unit: {},
|
unit: null,
|
||||||
food: {},
|
food: null,
|
||||||
disableAmount: true,
|
disableAmount: true,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
};
|
};
|
||||||
@ -407,24 +455,22 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recipe?.value?.recipeIngredient?.push({
|
recipe?.value?.recipeIngredient?.push({
|
||||||
|
ref: uuid4(),
|
||||||
title: "",
|
title: "",
|
||||||
note: "",
|
note: "",
|
||||||
unit: {},
|
unit: null,
|
||||||
food: {},
|
food: null,
|
||||||
disableAmount: true,
|
disableAmount: true,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
scale: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
// Metadata
|
// Metadata
|
||||||
|
|
||||||
const structuredData = computed(() => {
|
const structuredData = computed(() => {
|
||||||
|
// TODO: Get this working with other scrapers, unsure why it isn't properly being delivered to clients.
|
||||||
return {
|
return {
|
||||||
"@context": "http://schema.org",
|
"@context": "http://schema.org",
|
||||||
"@type": "Recipe",
|
"@type": "Recipe",
|
||||||
@ -450,34 +496,21 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
enableLandscape,
|
||||||
scaledYield,
|
scaledYield,
|
||||||
|
toggleJson,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
imageKey,
|
|
||||||
recipe,
|
recipe,
|
||||||
api,
|
api,
|
||||||
form,
|
|
||||||
loading,
|
loading,
|
||||||
addStep,
|
addStep,
|
||||||
deleteRecipe,
|
deleteRecipe,
|
||||||
|
closeEditor,
|
||||||
updateRecipe,
|
updateRecipe,
|
||||||
uploadImage,
|
uploadImage,
|
||||||
validators,
|
validators,
|
||||||
recipeImage,
|
recipeImage,
|
||||||
addIngredient,
|
addIngredient,
|
||||||
removeByIndex,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hideImage: false,
|
|
||||||
loadFailed: false,
|
|
||||||
skeleton: false,
|
|
||||||
jsonEditor: false,
|
|
||||||
jsonEditorOptions: {
|
|
||||||
mode: "code",
|
|
||||||
search: false,
|
|
||||||
mainMenuBar: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head: {},
|
head: {},
|
||||||
|
@ -80,10 +80,11 @@ export interface Recipe {
|
|||||||
comments?: CommentOut[];
|
comments?: CommentOut[];
|
||||||
}
|
}
|
||||||
export interface RecipeIngredient {
|
export interface RecipeIngredient {
|
||||||
|
ref: string;
|
||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
unit: RecipeIngredientUnit;
|
unit?: RecipeIngredientUnit | null;
|
||||||
food: RecipeIngredientFood;
|
food?: RecipeIngredientFood | null;
|
||||||
disableAmount: boolean;
|
disableAmount: boolean;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import enum
|
import enum
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
uuid4()
|
||||||
|
|
||||||
|
|
||||||
class CreateIngredientFood(CamelModel):
|
class CreateIngredientFood(CamelModel):
|
||||||
@ -36,6 +40,11 @@ class RecipeIngredient(CamelModel):
|
|||||||
disable_amount: bool = True
|
disable_amount: bool = True
|
||||||
quantity: float = 1
|
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:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user