mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-24 23:59:45 +02: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
10 changed files with 195 additions and 88 deletions
|
@ -1,16 +1,16 @@
|
|||
<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-skeleton-loader class="mx-auto" height="700px" type="card"></v-skeleton-loader>
|
||||
</v-card>
|
||||
<v-card v-else-if="recipe">
|
||||
<!-- Recipe Header -->
|
||||
<div class="d-flex justify-end flex-wrap align-stretch">
|
||||
<v-card
|
||||
v-if="!recipe.settings.landscapeView"
|
||||
width="50%"
|
||||
flat
|
||||
class="d-flex flex-column justify-center align-center"
|
||||
>
|
||||
<v-card v-if="!enableLandscape" width="50%" flat class="d-flex flex-column justify-center align-center">
|
||||
<v-card-text>
|
||||
<v-card-title class="headline pa-0 flex-column align-center">
|
||||
{{ recipe.name }}
|
||||
|
@ -21,6 +21,7 @@
|
|||
<v-divider></v-divider>
|
||||
<div class="d-flex justify-center mt-5">
|
||||
<RecipeTimeCard
|
||||
class="d-flex justify-center flex-wrap"
|
||||
:class="true ? undefined : 'force-bottom'"
|
||||
:prep-time="recipe.prepTime"
|
||||
:total-time="recipe.totalTime"
|
||||
|
@ -31,14 +32,14 @@
|
|||
</v-card>
|
||||
<v-img
|
||||
:key="imageKey"
|
||||
:max-width="recipe.settings.landscapeView ? null : '50%'"
|
||||
:max-width="enableLandscape ? null : '50%'"
|
||||
:height="hideImage ? '50' : imageHeight"
|
||||
:src="recipeImage(recipe.slug, imageKey)"
|
||||
class="d-print-none"
|
||||
@error="hideImage = true"
|
||||
>
|
||||
<RecipeTimeCard
|
||||
v-if="recipe.settings.landscapeView"
|
||||
v-if="enableLandscape"
|
||||
:class="true ? undefined : 'force-bottom'"
|
||||
:prep-time="recipe.prepTime"
|
||||
:total-time="recipe.totalTime"
|
||||
|
@ -54,8 +55,8 @@
|
|||
:logged-in="$auth.loggedIn"
|
||||
:open="form"
|
||||
class="ml-auto"
|
||||
@close="form = false"
|
||||
@json="jsonEditor = !jsonEditor"
|
||||
@close="closeEditor"
|
||||
@json="toggleJson"
|
||||
@edit="
|
||||
jsonEditor = false;
|
||||
form = true;
|
||||
|
@ -64,14 +65,20 @@
|
|||
@delete="deleteRecipe(recipe.slug)"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<v-card-text>
|
||||
<!-- Editors -->
|
||||
<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">
|
||||
<RecipeImageUploadBtn class="my-1" :slug="recipe.slug" @upload="uploadImage" @refresh="imageKey++" />
|
||||
<RecipeSettingsMenu class="my-1 mx-1" :value="recipe.settings" @upload="uploadImage" />
|
||||
</div>
|
||||
<!-- Recipe Title Section -->
|
||||
<template v-if="!form && recipe.settings.landscapeView">
|
||||
<template v-if="!form && !enableLandscape">
|
||||
<v-card-title class="pa-0 ma-0 headline">
|
||||
{{ recipe.name }}
|
||||
</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-text-field v-model="recipe.recipeYield" dense :label="$t('recipe.servings')"> </v-text-field>
|
||||
</template>
|
||||
|
||||
<!-- Advanced Editor -->
|
||||
|
@ -103,10 +111,10 @@
|
|||
<draggable v-model="recipe.recipeIngredient" handle=".handle">
|
||||
<RecipeIngredientEditor
|
||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||
:key="index + 'ing-editor'"
|
||||
:key="ingredient.ref"
|
||||
v-model="recipe.recipeIngredient[index]"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
@delete="removeByIndex(recipe.recipeIngredient, index)"
|
||||
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||
/>
|
||||
</draggable>
|
||||
<div class="d-flex justify-end mt-2">
|
||||
|
@ -115,9 +123,8 @@
|
|||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 }">
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
|
@ -139,7 +146,7 @@
|
|||
<span> Reset Scale </span>
|
||||
</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-icon>
|
||||
{{ $globals.icons.minus }}
|
||||
|
@ -154,7 +161,7 @@
|
|||
<v-spacer></v-spacer>
|
||||
|
||||
<RecipeRating
|
||||
v-if="recipe.settings.landscapeView"
|
||||
v-if="!enableLandscape"
|
||||
:key="recipe.slug"
|
||||
:value="recipe.rating"
|
||||
:name="recipe.name"
|
||||
|
@ -222,8 +229,9 @@
|
|||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<RecipeInstructions v-model="recipe.recipeInstructions" :edit="form" />
|
||||
<div class="d-flex">
|
||||
<BaseButton v-if="form" class="ml-auto my-2" @click="addStep()"> {{ $t("general.new") }}</BaseButton>
|
||||
<div v-if="form" class="d-flex">
|
||||
<RecipeDialogBulkAdd class="ml-auto my-2 mr-1" @bulk-data="addStep" />
|
||||
<BaseButton class="my-2" @click="addStep()"> {{ $t("general.new") }}</BaseButton>
|
||||
</div>
|
||||
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
||||
</v-col>
|
||||
|
@ -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<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) {
|
||||
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<any>, index: number) {
|
||||
list.splice(index, 1);
|
||||
state.imageKey++;
|
||||
}
|
||||
|
||||
function addStep(steps: Array<string> | 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: {},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue