mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-28 00:34:47 -04:00
fix: ingredient linker and instructions titles (#6146)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
cursor: grab;
|
cursor: grab !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
|
|||||||
@@ -1,15 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<div class="ingredient-link-label links-disabled">
|
||||||
<div v-html="safeMarkup" />
|
<SafeMarkdown v-if="baseText" :source="baseText" />
|
||||||
|
<SafeMarkdown
|
||||||
|
v-if="ingredient?.note"
|
||||||
|
class="d-inline"
|
||||||
|
:source="` ${ingredient.note}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
|
import { computed } from "vue";
|
||||||
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
|
import { useParsedIngredientText } from "~/composables/recipes";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
markup: string;
|
ingredient?: RecipeIngredient;
|
||||||
|
scale?: number;
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
const { ingredient, scale = 1 } = defineProps<Props>();
|
||||||
|
|
||||||
|
const baseText = computed(() => {
|
||||||
|
if (!ingredient) return "";
|
||||||
|
const parsed = useParsedIngredientText(ingredient, scale);
|
||||||
|
return [parsed.quantity, parsed.unit, parsed.name].filter(Boolean).join(" ").trim();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ingredient-link-label {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.25;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.links-disabled :deep(a) {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
color: var(--v-theme-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
class="ml-4"
|
class="ml-4"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
class="ml-4"
|
class="ml-4"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -184,17 +184,17 @@
|
|||||||
<v-hover v-slot="{ isHovering }">
|
<v-hover v-slot="{ isHovering }">
|
||||||
<v-card
|
<v-card
|
||||||
class="my-3"
|
class="my-3"
|
||||||
:class="[{ 'on-hover': isHovering }, isChecked(index)]"
|
:class="[{ 'on-hover': isHovering }, { 'cursor-default': isEditForm }, isChecked(index)]"
|
||||||
:elevation="isHovering ? 12 : 2"
|
:elevation="isHovering ? 12 : 2"
|
||||||
:ripple="false"
|
:ripple="false"
|
||||||
@click="toggleDisabled(index)"
|
@click="toggleDisabled(index)"
|
||||||
>
|
>
|
||||||
<v-card-title :class="{ 'pb-0': !isChecked(index) }">
|
<v-card-title class="recipe-step-title pt-3" :class="!isChecked(index) ? 'pb-0' : 'pb-3'">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center w-100">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="isEditForm"
|
v-if="isEditForm"
|
||||||
v-model="step.summary"
|
v-model="step.summary"
|
||||||
class="headline handle"
|
class="headline"
|
||||||
hide-details
|
hide-details
|
||||||
density="compact"
|
density="compact"
|
||||||
variant="solo"
|
variant="solo"
|
||||||
@@ -202,14 +202,27 @@
|
|||||||
:placeholder="$t('recipe.step-index', { step: index + 1 })"
|
:placeholder="$t('recipe.step-index', { step: index + 1 })"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon size="26">
|
<v-icon size="26" class="handle">
|
||||||
{{ $globals.icons.arrowUpDown }}
|
{{ $globals.icons.arrowUpDown }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
<span v-else>
|
<div
|
||||||
{{ step.summary ? step.summary : $t("recipe.step-index", { step: index + 1 }) }}
|
v-else
|
||||||
|
class="summary-wrapper"
|
||||||
|
>
|
||||||
|
<template v-if="step.summary">
|
||||||
|
<SafeMarkdown
|
||||||
|
class="pr-2"
|
||||||
|
:source="step.summary"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>
|
||||||
|
{{ $t('recipe.step-index', { step: index + 1 }) }}
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<template v-if="isEditForm">
|
<template v-if="isEditForm">
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<BaseButtonGroup
|
<BaseButtonGroup
|
||||||
@@ -314,11 +327,22 @@
|
|||||||
persistentHint: true,
|
persistentHint: true,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="step.ingredientReferences && step.ingredientReferences.length"
|
||||||
|
class="linked-ingredients-editor"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(linkRef, i) in step.ingredientReferences"
|
||||||
|
:key="linkRef.referenceId ?? i"
|
||||||
|
class="mb-1"
|
||||||
|
>
|
||||||
<RecipeIngredientHtml
|
<RecipeIngredientHtml
|
||||||
v-for="ing in step.ingredientReferences"
|
v-if="linkRef.referenceId && ingredientLookup[linkRef.referenceId]"
|
||||||
:key="ing.referenceId!"
|
:ingredient="ingredientLookup[linkRef.referenceId]"
|
||||||
:markup="getIngredientByRefId(ing.referenceId!)"
|
:scale="scale"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</DropZone>
|
</DropZone>
|
||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
@@ -373,9 +397,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||||
import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue";
|
|
||||||
import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
|
import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
|
||||||
import { uuid4 } from "~/composables/use-utils";
|
import { uuid4 } from "~/composables/use-utils";
|
||||||
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
||||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||||
@@ -383,6 +405,7 @@ import { useExtractIngredientReferences } from "~/composables/recipe-page/use-ex
|
|||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import DropZone from "~/components/global/DropZone.vue";
|
import DropZone from "~/components/global/DropZone.vue";
|
||||||
import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue";
|
import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue";
|
||||||
|
import RecipeIngredientHtml from "~/components/Domain/Recipe/RecipeIngredientHtml.vue";
|
||||||
|
|
||||||
interface MergerHistory {
|
interface MergerHistory {
|
||||||
target: number;
|
target: number;
|
||||||
@@ -500,10 +523,9 @@ function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
|
|||||||
instructionList.value[idx].ingredientReferences = [];
|
instructionList.value[idx].ingredientReferences = [];
|
||||||
refs = instructionList.value[idx].ingredientReferences as IngredientReferences[];
|
refs = instructionList.value[idx].ingredientReferences as IngredientReferences[];
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsedIngredients();
|
|
||||||
activeText.value = text;
|
|
||||||
activeIndex.value = idx;
|
activeIndex.value = idx;
|
||||||
|
activeText.value = text;
|
||||||
|
setUsedIngredients();
|
||||||
dialog.value = true;
|
dialog.value = true;
|
||||||
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
|
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
|
||||||
}
|
}
|
||||||
@@ -544,29 +566,26 @@ function saveAndOpenNextLinkIngredients() {
|
|||||||
function setUsedIngredients() {
|
function setUsedIngredients() {
|
||||||
const usedRefs: { [key: string]: boolean } = {};
|
const usedRefs: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
instructionList.value.forEach((element) => {
|
instructionList.value.forEach((element, idx) => {
|
||||||
|
if (idx === activeIndex.value) return;
|
||||||
element.ingredientReferences?.forEach((ref) => {
|
element.ingredientReferences?.forEach((ref) => {
|
||||||
if (ref.referenceId !== undefined) {
|
if (ref.referenceId) usedRefs[ref.referenceId] = true;
|
||||||
usedRefs[ref.referenceId!] = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
usedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
|
usedIngredients.value = props.recipe.recipeIngredient.filter(ing => !!ing.referenceId && ing.referenceId in usedRefs);
|
||||||
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
|
|
||||||
});
|
|
||||||
|
|
||||||
unusedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
|
unusedIngredients.value = props.recipe.recipeIngredient.filter(ing => !!ing.referenceId && !(ing.referenceId in usedRefs));
|
||||||
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(activeRefs, () => setUsedIngredients());
|
||||||
|
|
||||||
function autoSetReferences() {
|
function autoSetReferences() {
|
||||||
useExtractIngredientReferences(
|
useExtractIngredientReferences(
|
||||||
props.recipe.recipeIngredient,
|
props.recipe.recipeIngredient,
|
||||||
activeRefs.value,
|
activeRefs.value,
|
||||||
activeText.value,
|
activeText.value,
|
||||||
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
).forEach(ingredient => activeRefs.value.push(ingredient));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ingredientLookup = computed(() => {
|
const ingredientLookup = computed(() => {
|
||||||
@@ -603,8 +622,8 @@ const ingredientSectionTitles = computed(() => {
|
|||||||
return titleMap;
|
return titleMap;
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupedUnusedIngredients = computed(() => {
|
const groupedUnusedIngredients = computed((): Record<string, RecipeIngredient[]> => {
|
||||||
const groups: { [key: string]: RecipeIngredient[] } = {};
|
const groups: Record<string, RecipeIngredient[]> = {};
|
||||||
|
|
||||||
// Group ingredients by section title
|
// Group ingredients by section title
|
||||||
unusedIngredients.value.forEach((ingredient) => {
|
unusedIngredients.value.forEach((ingredient) => {
|
||||||
@@ -614,20 +633,14 @@ const groupedUnusedIngredients = computed(() => {
|
|||||||
|
|
||||||
// Use the section title from the mapping, or fallback to the ingredient's own title
|
// Use the section title from the mapping, or fallback to the ingredient's own title
|
||||||
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
|
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
|
||||||
|
(groups[title] ||= []).push(ingredient);
|
||||||
if (!groups[title]) {
|
|
||||||
groups[title] = [];
|
|
||||||
}
|
|
||||||
groups[title].push(ingredient);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupedUsedIngredients = computed(() => {
|
const groupedUsedIngredients = computed((): Record<string, RecipeIngredient[]> => {
|
||||||
const groups: { [key: string]: RecipeIngredient[] } = {};
|
const groups: Record<string, RecipeIngredient[]> = {};
|
||||||
|
|
||||||
// Group ingredients by section title
|
|
||||||
usedIngredients.value.forEach((ingredient) => {
|
usedIngredients.value.forEach((ingredient) => {
|
||||||
if (ingredient.referenceId === undefined) {
|
if (ingredient.referenceId === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -635,26 +648,12 @@ const groupedUsedIngredients = computed(() => {
|
|||||||
|
|
||||||
// Use the section title from the mapping, or fallback to the ingredient's own title
|
// Use the section title from the mapping, or fallback to the ingredient's own title
|
||||||
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
|
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
|
||||||
|
(groups[title] ||= []).push(ingredient);
|
||||||
if (!groups[title]) {
|
|
||||||
groups[title] = [];
|
|
||||||
}
|
|
||||||
groups[title].push(ingredient);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
function getIngredientByRefId(refId: string | undefined) {
|
|
||||||
if (refId === undefined) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const ing = ingredientLookup.value[refId];
|
|
||||||
if (!ing) return "";
|
|
||||||
return parseIngredientText(ing, props.scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
// Instruction Merger
|
// Instruction Merger
|
||||||
const mergeHistory = ref<MergerHistory[]>([]);
|
const mergeHistory = ref<MergerHistory[]>([]);
|
||||||
@@ -847,7 +846,21 @@ function openImageUpload(index: number) {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-text-field >>> input {
|
.v-text-field :deep(input) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recipe-step-title {
|
||||||
|
/* Multiline display */
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 1.25;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.summary-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0; /* wrapping in flex container */
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user