mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-31 12:15:42 -04:00
fix: recipe ingredient editor bugs (#1251)
* filter unallowed fields #1140 * fix type and layout * propery validate none type quantites * fix rendering error #1237
This commit is contained in:
parent
d06d4d2fd9
commit
cd0da36e7c
@ -20,6 +20,7 @@
|
|||||||
class="mx-1"
|
class="mx-1"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Quantity"
|
placeholder="Quantity"
|
||||||
|
@keypress="quantityFilter"
|
||||||
>
|
>
|
||||||
<v-icon v-if="$listeners && $listeners.delete" slot="prepend" class="mr-n1 handle">
|
<v-icon v-if="$listeners && $listeners.delete" slot="prepend" class="mr-n1 handle">
|
||||||
{{ $globals.icons.arrowUpDown }}
|
{{ $globals.icons.arrowUpDown }}
|
||||||
@ -166,7 +167,7 @@ export default defineComponent({
|
|||||||
if (state.showTitle) {
|
if (state.showTitle) {
|
||||||
value.title = "";
|
value.title = "";
|
||||||
}
|
}
|
||||||
state.showTitle = !state.showTitle
|
state.showTitle = !state.showTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleOriginalText() {
|
function toggleOriginalText() {
|
||||||
@ -211,7 +212,15 @@ export default defineComponent({
|
|||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function quantityFilter(e: KeyboardEvent) {
|
||||||
|
// if digit is pressed, add to quantity
|
||||||
|
if (e.key === "-" || e.key === "+" || e.key === "e") {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
quantityFilter,
|
||||||
toggleOriginalText,
|
toggleOriginalText,
|
||||||
contextMenuOptions,
|
contextMenuOptions,
|
||||||
handleUnitEnter,
|
handleUnitEnter,
|
||||||
|
@ -10,11 +10,8 @@
|
|||||||
<v-divider v-if="showTitleEditor[index]"></v-divider>
|
<v-divider v-if="showTitleEditor[index]"></v-divider>
|
||||||
<v-list-item dense @click="toggleChecked(index)">
|
<v-list-item dense @click="toggleChecked(index)">
|
||||||
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary" />
|
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary" />
|
||||||
<v-list-item-content>
|
<v-list-item-content :key="ingredient.quantity">
|
||||||
<VueMarkdown
|
<VueMarkdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredientDisplay[index]" />
|
||||||
class="ma-0 pa-0 text-subtitle-1 dense-markdown"
|
|
||||||
:source="parseIngredientText(ingredient, disableAmount, scale)"
|
|
||||||
/>
|
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</div>
|
</div>
|
||||||
@ -58,11 +55,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ingredientCopyText = computed(() => {
|
const ingredientCopyText = computed(() => {
|
||||||
return props.value
|
return ingredientDisplay.value.join("\n");
|
||||||
.map((ingredient) => {
|
|
||||||
return `- [ ] ${parseIngredientText(ingredient, props.disableAmount, props.scale)}`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleChecked(index: number) {
|
function toggleChecked(index: number) {
|
||||||
@ -71,7 +64,14 @@ export default defineComponent({
|
|||||||
state.checked.splice(index, 1, !state.checked[index]);
|
state.checked.splice(index, 1, !state.checked[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ingredientDisplay = computed(() => {
|
||||||
|
return props.value.map((ingredient) => {
|
||||||
|
return `${parseIngredientText(ingredient, props.disableAmount, props.scale)}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
ingredientDisplay,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
parseIngredientText,
|
parseIngredientText,
|
||||||
ingredientCopyText,
|
ingredientCopyText,
|
||||||
|
@ -5,8 +5,8 @@ const { frac } = useFraction();
|
|||||||
|
|
||||||
function sanitizeIngredientHTML(rawHtml: string) {
|
function sanitizeIngredientHTML(rawHtml: string) {
|
||||||
return DOMPurify.sanitize(rawHtml, {
|
return DOMPurify.sanitize(rawHtml, {
|
||||||
"USE_PROFILES": {html: true},
|
USE_PROFILES: { html: true },
|
||||||
"ALLOWED_TAGS": ["b", "q", "i", "strong", "sup"]
|
ALLOWED_TAGS: ["b", "q", "i", "strong", "sup"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,10 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
|
|||||||
const { quantity, food, unit, note } = ingredient;
|
const { quantity, food, unit, note } = ingredient;
|
||||||
|
|
||||||
let returnQty = "";
|
let returnQty = "";
|
||||||
if (quantity !== undefined && quantity !== 0) {
|
|
||||||
|
// casting to number is required as sometimes quantity is a string
|
||||||
|
if (quantity && Number(quantity) !== 0) {
|
||||||
|
console.log("Using Quantity", quantity, typeof quantity);
|
||||||
if (unit?.fraction) {
|
if (unit?.fraction) {
|
||||||
const fraction = frac(quantity * scale, 10, true);
|
const fraction = frac(quantity * scale, 10, true);
|
||||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<v-divider class="icon-divider"></v-divider>
|
<v-divider class="icon-divider"></v-divider>
|
||||||
<v-avatar class="pa-2 icon-avatar" color="primary" size="75">
|
<v-avatar class="pa-2 icon-avatar" color="primary" size="75">
|
||||||
<svg class="icon-white" style="width: 75;" viewBox="0 0 24 24">
|
<svg class="icon-white" style="width: 75" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||||
/>
|
/>
|
||||||
@ -225,14 +225,14 @@
|
|||||||
<span class="headline">{{ $t("general.confirm") }}</span>
|
<span class="headline">{{ $t("general.confirm") }}</span>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-list>
|
<v-list>
|
||||||
<template v-for="(item, idx) in confirmationData.value">
|
<template v-for="(item, idx) in confirmationData">
|
||||||
<v-list-item v-if="item.display" :key="idx">
|
<v-list-item v-if="item.display" :key="idx">
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title> {{ item.text }} </v-list-item-title>
|
<v-list-item-title> {{ item.text }} </v-list-item-title>
|
||||||
<v-list-item-subtitle> {{ item.value }} </v-list-item-subtitle>
|
<v-list-item-subtitle> {{ item.value }} </v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider v-if="idx !== confirmationData.value.length - 1" :key="`divider-${idx}`" />
|
<v-divider v-if="idx !== confirmationData.length - 1" :key="`divider-${idx}`" />
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
@ -263,9 +263,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref, useRouter, Ref, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, onMounted, ref, useRouter, Ref, useContext, computed } from "@nuxtjs/composition-api";
|
||||||
import { useDark } from "@vueuse/core";
|
import { useDark } from "@vueuse/core";
|
||||||
import { computed } from "@vue/reactivity";
|
|
||||||
import { States, RegistrationType, useRegistration } from "./states";
|
import { States, RegistrationType, useRegistration } from "./states";
|
||||||
import { useRouteQuery } from "~/composables/use-router";
|
import { useRouteQuery } from "~/composables/use-router";
|
||||||
import { validators, useAsyncValidator } from "~/composables/use-validators";
|
import { validators, useAsyncValidator } from "~/composables/use-validators";
|
||||||
@ -285,7 +284,7 @@ const inputAttrs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
layout: "basic",
|
layout: "blank",
|
||||||
setup() {
|
setup() {
|
||||||
const { i18n } = useContext();
|
const { i18n } = useContext();
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import enum
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from pydantic import UUID4, Field
|
from pydantic import UUID4, Field, validator
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema._mealie.types import NoneFloat
|
from mealie.schema._mealie.types import NoneFloat
|
||||||
@ -53,7 +53,7 @@ class RecipeIngredient(MealieModel):
|
|||||||
unit: Optional[Union[IngredientUnit, CreateIngredientUnit]]
|
unit: Optional[Union[IngredientUnit, CreateIngredientUnit]]
|
||||||
food: Optional[Union[IngredientFood, CreateIngredientFood]]
|
food: Optional[Union[IngredientFood, CreateIngredientFood]]
|
||||||
disable_amount: bool = True
|
disable_amount: bool = True
|
||||||
quantity: float = 1
|
quantity: NoneFloat = 1
|
||||||
original_text: Optional[str]
|
original_text: Optional[str]
|
||||||
|
|
||||||
# Ref is used as a way to distinguish between an individual ingredient on the frontend
|
# Ref is used as a way to distinguish between an individual ingredient on the frontend
|
||||||
@ -64,6 +64,20 @@ class RecipeIngredient(MealieModel):
|
|||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
@validator("quantity", pre=True)
|
||||||
|
@classmethod
|
||||||
|
def validate_quantity(cls, value, values) -> NoneFloat:
|
||||||
|
"""
|
||||||
|
Sometimes the frontend UI will provide an emptry string as a "null" value because of the default
|
||||||
|
bindings in Vue. This validator will ensure that the quantity is set to None if the value is an
|
||||||
|
empty string.
|
||||||
|
"""
|
||||||
|
if isinstance(value, float):
|
||||||
|
return value
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class IngredientConfidence(MealieModel):
|
class IngredientConfidence(MealieModel):
|
||||||
average: NoneFloat = None
|
average: NoneFloat = None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user