1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 13:05:21 +02:00

feat: Remove "Is Food" and "Disable Amounts" Flags (#5684)

Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Michael Genson 2025-07-31 10:36:24 -05:00 committed by GitHub
parent efc0d31724
commit 245ca5fe3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 173 additions and 364 deletions

View file

@ -44,7 +44,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1 cup unsalted butter, cut into cubes", "note": "1 cup unsalted butter, cut into cubes",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26", "referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26",
@ -54,7 +53,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1 cup light brown sugar", "note": "1 cup light brown sugar",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82", "referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82",
@ -64,7 +62,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1/2 cup granulated white sugar", "note": "1/2 cup granulated white sugar",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "034f481b-c426-4a17-b983-5aea9be4974b", "referenceId": "034f481b-c426-4a17-b983-5aea9be4974b",
@ -74,7 +71,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "2 large eggs", "note": "2 large eggs",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4", "referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4",
@ -84,7 +80,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "2 tsp vanilla extract", "note": "2 tsp vanilla extract",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "85561ace-f249-401d-834c-e600a2f6280e", "referenceId": "85561ace-f249-401d-834c-e600a2f6280e",
@ -94,7 +89,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1/2 cup creamy peanut butter", "note": "1/2 cup creamy peanut butter",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd", "referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd",
@ -104,7 +98,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1 tsp cornstarch", "note": "1 tsp cornstarch",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0", "referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0",
@ -114,7 +107,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1 tsp baking soda", "note": "1 tsp baking soda",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "64627441-39f9-4ee3-8494-bafe36451d12", "referenceId": "64627441-39f9-4ee3-8494-bafe36451d12",
@ -124,7 +116,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1/2 tsp salt", "note": "1/2 tsp salt",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384", "referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384",
@ -134,7 +125,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1 cup cake flour", "note": "1 cup cake flour",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "06967994-8548-4952-a8cc-16e8db228ebd", "referenceId": "06967994-8548-4952-a8cc-16e8db228ebd",
@ -144,7 +134,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "2 cups all-purpose flour", "note": "2 cups all-purpose flour",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691", "referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691",
@ -154,7 +143,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "2 cups peanut butter chips", "note": "2 cups peanut butter chips",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef", "referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef",
@ -164,7 +152,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"note": "1½ cups Reese's Pieces candies", "note": "1½ cups Reese's Pieces candies",
"unit": None, "unit": None,
"food": None, "food": None,
"disableAmount": True,
"quantity": 1, "quantity": 1,
"originalText": None, "originalText": None,
"referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2", "referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2",
@ -221,7 +208,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"showAssets": False, "showAssets": False,
"landscapeView": False, "landscapeView": False,
"disableComments": False, "disableComments": False,
"disableAmount": True,
"locked": False, "locked": False,
}, },
"assets": [], "assets": [],

File diff suppressed because one or more lines are too long

View file

@ -79,11 +79,6 @@ const recipePreferences: Preference[] = [
label: i18n.t("group.disable-users-from-commenting-on-recipes"), label: i18n.t("group.disable-users-from-commenting-on-recipes"),
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"), description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
}, },
{
key: "recipeDisableAmount",
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
},
]; ];
const allDays = [ const allDays = [

View file

@ -130,20 +130,23 @@
.ingredients[i] .ingredients[i]
.checked" .checked"
> >
<v-checkbox <v-container class="pa-0 ma-0">
hide-details <v-row no-gutters>
:model-value="ingredientData.checked" <v-checkbox
class="pt-0 my-auto py-auto" hide-details
color="secondary" :model-value="ingredientData.checked"
density="compact" class="pt-0 my-auto py-auto mr-2"
/> color="secondary"
<div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`"> density="compact"
<RecipeIngredientListItem />
:ingredient="ingredientData.ingredient" <div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`" class="pa-auto my-auto">
:disable-amount="ingredientData.disableAmount" <RecipeIngredientListItem
:scale="recipeSection.recipeScale" :ingredient="ingredientData.ingredient"
/> :scale="recipeSection.recipeScale"
</div> />
</div>
</v-row>
</v-container>
</v-list-item> </v-list-item>
</div> </div>
</div> </div>
@ -188,7 +191,6 @@ export interface RecipeWithScale extends Recipe {
export interface ShoppingListIngredient { export interface ShoppingListIngredient {
checked: boolean; checked: boolean;
ingredient: RecipeIngredient; ingredient: RecipeIngredient;
disableAmount: boolean;
} }
export interface ShoppingListIngredientSection { export interface ShoppingListIngredientSection {
@ -290,7 +292,6 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
return { return {
checked: !householdsWithFood.includes(userHousehold.value), checked: !householdsWithFood.includes(userHousehold.value),
ingredient: ing, ingredient: ing,
disableAmount: recipe.settings?.disableAmount || false,
}; };
}); });

View file

@ -17,7 +17,6 @@
class="d-flex flex-wrap my-1" class="d-flex flex-wrap my-1"
> >
<v-col <v-col
v-if="!disableAmount"
sm="12" sm="12"
md="2" md="2"
cols="12" cols="12"
@ -42,7 +41,6 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col <v-col
v-if="!disableAmount"
sm="12" sm="12"
md="3" md="3"
cols="12" cols="12"
@ -98,7 +96,6 @@
<!-- Foods Input --> <!-- Foods Input -->
<v-col <v-col
v-if="!disableAmount"
m="12" m="12"
md="3" md="3"
cols="12" cols="12"
@ -166,16 +163,7 @@
:placeholder="$t('recipe.notes')" :placeholder="$t('recipe.notes')"
class="mb-auto" class="mb-auto"
@click="$emit('clickIngredientField', 'note')" @click="$emit('clickIngredientField', 'note')"
> />
<template #prepend>
<v-icon
v-if="disableAmount && $attrs && $attrs.delete"
class="mr-n1 handle"
>
{{ $globals.icons.arrowUpDown }}
</v-icon>
</template>
</v-text-field>
<BaseButtonGroup <BaseButtonGroup
hover hover
:large="false" :large="false"
@ -216,10 +204,6 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
const model = defineModel<RecipeIngredient>({ required: true }); const model = defineModel<RecipeIngredient>({ required: true });
defineProps({ defineProps({
disableAmount: {
type: Boolean,
default: false,
},
unitError: { unitError: {
type: Boolean, type: Boolean,
default: false, default: false,

View file

@ -34,16 +34,14 @@ import { useParsedIngredientText } from "~/composables/recipes";
interface Props { interface Props {
ingredient: RecipeIngredient; ingredient: RecipeIngredient;
disableAmount?: boolean;
scale?: number; scale?: number;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
disableAmount: false,
scale: 1, scale: 1,
}); });
const parsedIng = computed(() => { const parsedIng = computed(() => {
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale); return useParsedIngredientText(props.ingredient, props.scale);
}); });
</script> </script>

View file

@ -43,7 +43,6 @@
<v-list-item-title> <v-list-item-title>
<RecipeIngredientListItem <RecipeIngredientListItem
:ingredient="ingredient" :ingredient="ingredient"
:disable-amount="disableAmount"
:scale="scale" :scale="scale"
/> />
</v-list-item-title> </v-list-item-title>
@ -60,13 +59,11 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
interface Props { interface Props {
value?: RecipeIngredient[]; value?: RecipeIngredient[];
disableAmount?: boolean;
scale?: number; scale?: number;
isCookMode?: boolean; isCookMode?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
value: () => [], value: () => [],
disableAmount: false,
scale: 1, scale: 1,
isCookMode: false, isCookMode: false,
}); });
@ -89,7 +86,7 @@ const ingredientCopyText = computed(() => {
components.push(`[${ingredient.title}]`); components.push(`[${ingredient.title}]`);
} }
components.push(parseIngredientText(ingredient, props.disableAmount, props.scale, false)); components.push(parseIngredientText(ingredient, props.scale, false));
}); });
return components.join("\n"); return components.join("\n");

View file

@ -141,7 +141,6 @@
<RecipeIngredients <RecipeIngredients
:value="notLinkedIngredients" :value="notLinkedIngredients"
:scale="scale" :scale="scale"
:disable-amount="recipe.settings.disableAmount"
:is-cook-mode="isCookMode" :is-cook-mode="isCookMode"
/> />
</v-card> </v-card>

View file

@ -1,9 +1,14 @@
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<div> <div>
<h2 class="mb-4 text-h5 font-weight-medium opacity-80"> <div class="mb-4">
{{ $t("recipe.ingredients") }} <h2 class="mb-4 text-h5 font-weight-medium opacity-80">
</h2> {{ $t("recipe.ingredients") }}
</h2>
<BannerWarning v-if="!hasFoodOrUnit">
{{ $t("recipe.ingredients-not-parsed-description", { parse: $t('recipe.parse') }) }}
</BannerWarning>
</div>
<VueDraggable <VueDraggable
v-if="recipe.recipeIngredient.length > 0" v-if="recipe.recipeIngredient.length > 0"
v-model="recipe.recipeIngredient" v-model="recipe.recipeIngredient"
@ -27,7 +32,6 @@
:key="ingredient.referenceId" :key="ingredient.referenceId"
v-model="recipe.recipeIngredient[index]" v-model="recipe.recipeIngredient[index]"
class="list-group-item" class="list-group-item"
:disable-amount="recipe.settings.disableAmount"
@delete="recipe.recipeIngredient.splice(index, 1)" @delete="recipe.recipeIngredient.splice(index, 1)"
@insert-above="insertNewIngredient(index)" @insert-above="insertNewIngredient(index)"
@insert-below="insertNewIngredient(index + 1)" @insert-below="insertNewIngredient(index + 1)"
@ -49,7 +53,7 @@
<span> <span>
<BaseButton <BaseButton
class="mb-1" class="mb-1"
:disabled="recipe.settings.disableAmount || hasFoodOrUnit" :disabled="hasFoodOrUnit"
color="accent" color="accent"
:to="`/g/${groupSlug}/r/${recipe.slug}/ingredient-parser`" :to="`/g/${groupSlug}/r/${recipe.slug}/ingredient-parser`"
v-bind="props" v-bind="props"
@ -109,10 +113,7 @@ const hasFoodOrUnit = computed(() => {
}); });
const parserToolTip = computed(() => { const parserToolTip = computed(() => {
if (recipe.value.settings.disableAmount) { if (hasFoodOrUnit.value) {
return i18n.t("recipe.enable-ingredient-amounts-to-use-this-feature");
}
else if (hasFoodOrUnit.value) {
return i18n.t("recipe.recipes-with-units-or-foods-defined-cannot-be-parsed"); return i18n.t("recipe.recipes-with-units-or-foods-defined-cannot-be-parsed");
} }
return i18n.t("recipe.parse-ingredients"); return i18n.t("recipe.parse-ingredients");
@ -127,7 +128,6 @@ function addIngredient(ingredients: Array<string> | null = null) {
note: x, note: x,
unit: undefined, unit: undefined,
food: undefined, food: undefined,
disableAmount: true,
quantity: 1, quantity: 1,
}; };
}); });
@ -146,7 +146,6 @@ function addIngredient(ingredients: Array<string> | null = null) {
unit: undefined, unit: undefined,
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set // @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
food: undefined, food: undefined,
disableAmount: true,
quantity: 1, quantity: 1,
}); });
} }
@ -161,7 +160,6 @@ function insertNewIngredient(dest: number) {
unit: undefined, unit: undefined,
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set // @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
food: undefined, food: undefined,
disableAmount: true,
quantity: 1, quantity: 1,
}); });
} }

View file

@ -3,7 +3,6 @@
<RecipeIngredients <RecipeIngredients
:value="recipe.recipeIngredient" :value="recipe.recipeIngredient"
:scale="scale" :scale="scale"
:disable-amount="recipe.settings.disableAmount"
:is-cook-mode="isCookMode" :is-cook-mode="isCookMode"
/> />
<div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0"> <div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0">

View file

@ -36,7 +36,7 @@
:value="ing.referenceId" :value="ing.referenceId"
> >
<template #label> <template #label>
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" /> <RecipeIngredientHtml :markup="parseIngredientText(ing)" />
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
@ -51,7 +51,7 @@
:value="ing.referenceId" :value="ing.referenceId"
> >
<template #label> <template #label>
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" /> <RecipeIngredientHtml :markup="parseIngredientText(ing)" />
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
</template> </template>
@ -323,7 +323,6 @@
return step.ingredientReferences.map((ref) => ref.referenceId).includes(ing.referenceId || '') return step.ingredientReferences.map((ref) => ref.referenceId).includes(ing.referenceId || '')
})" })"
:scale="scale" :scale="scale"
:disable-amount="recipe.settings.disableAmount"
:is-cook-mode="isCookMode" :is-cook-mode="isCookMode"
/> />
</div> </div>
@ -552,7 +551,6 @@ function autoSetReferences() {
props.recipe.recipeIngredient, props.recipe.recipeIngredient,
activeRefs.value, activeRefs.value,
activeText.value, activeText.value,
props.recipe.settings.disableAmount,
).forEach((ingredient: string) => activeRefs.value.push(ingredient)); ).forEach((ingredient: string) => activeRefs.value.push(ingredient));
} }
@ -574,7 +572,7 @@ function getIngredientByRefId(refId: string | undefined) {
const ing = ingredientLookup.value[refId]; const ing = ingredientLookup.value[refId];
if (!ing) return ""; if (!ing) return "";
return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale); return parseIngredientText(ing, props.scale);
} }
// =============================================================== // ===============================================================

View file

@ -4,7 +4,7 @@
v-if="!isEditMode" v-if="!isEditMode"
v-model.number="scale" v-model.number="scale"
:recipe-servings="recipeServings" :recipe-servings="recipeServings"
:edit-scale="!recipe.settings.disableAmount && !isEditMode" :edit-scale="hasFoodOrUnit && !isEditMode"
/> />
</div> </div>
</template> </template>
@ -24,4 +24,15 @@ const { isEditMode } = usePageState(props.recipe.slug);
const recipeServings = computed<number>(() => { const recipeServings = computed<number>(() => {
return props.recipe.recipeServings || props.recipe.recipeYieldQuantity || 1; return props.recipe.recipeServings || props.recipe.recipeYieldQuantity || 1;
}); });
const hasFoodOrUnit = computed(() => {
if (props.recipe.recipeIngredient) {
for (const ingredient of props.recipe.recipeIngredient) {
if (ingredient.food || ingredient.unit) {
return true;
}
}
}
return false;
});
</script> </script>

View file

@ -321,7 +321,7 @@ const hasNotes = computed(() => {
}); });
function parseText(ingredient: RecipeIngredient) { function parseText(ingredient: RecipeIngredient) {
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale); return parseIngredientText(ingredient, props.scale);
} }
</script> </script>

View file

@ -31,7 +31,6 @@ const labels: Record<keyof RecipeSettings, string> = {
showAssets: i18n.t("asset.show-assets"), showAssets: i18n.t("asset.show-assets"),
landscapeView: i18n.t("recipe.landscape-view-coming-soon"), landscapeView: i18n.t("recipe.landscape-view-coming-soon"),
disableComments: i18n.t("recipe.disable-comments"), disableComments: i18n.t("recipe.disable-comments"),
disableAmount: i18n.t("recipe.disable-amount"),
locked: i18n.t("recipe.locked"), locked: i18n.t("recipe.locked"),
}; };
</script> </script>

View file

@ -22,10 +22,7 @@
:class="listItem.checked ? 'strike-through' : ''" :class="listItem.checked ? 'strike-through' : ''"
style="min-width: 0;" style="min-width: 0;"
> >
<RecipeIngredientListItem <RecipeIngredientListItem :ingredient="listItem" />
:ingredient="listItem"
:disable-amount="!(listItem.isFood || listItem.quantity !== 1)"
/>
</div> </div>
</div> </div>
</v-col> </v-col>
@ -172,7 +169,6 @@
@save="save" @save="save"
@cancel="toggleEdit(false)" @cancel="toggleEdit(false)"
@delete="$emit('delete')" @delete="$emit('delete')"
@toggle-foods="localListItem.isFood = !localListItem.isFood"
/> />
</div> </div>
</template> </template>

View file

@ -2,7 +2,7 @@
<div> <div>
<v-card variant="outlined"> <v-card variant="outlined">
<v-card-text class="pb-3 pt-1"> <v-card-text class="pb-3 pt-1">
<div v-if="listItem.isFood" class="d-md-flex align-center mb-2" style="gap: 20px"> <div class="d-md-flex align-center mb-2" style="gap: 20px">
<div> <div>
<InputQuantity v-model="listItem.quantity" /> <InputQuantity v-model="listItem.quantity" />
</div> </div>
@ -26,9 +26,6 @@
/> />
</div> </div>
<div class="d-md-flex align-center" style="gap: 20px"> <div class="d-md-flex align-center" style="gap: 20px">
<div v-if="!listItem.isFood">
<InputQuantity v-model="listItem.quantity" />
</div>
<v-textarea <v-textarea
v-model="listItem.note" v-model="listItem.note"
hide-details hide-details
@ -99,11 +96,6 @@
text: $t('general.cancel'), text: $t('general.cancel'),
event: 'cancel', event: 'cancel',
}, },
{
icon: $globals.icons.foods,
text: $t('shopping-list.toggle-food'),
event: 'toggle-foods',
},
{ {
icon: $globals.icons.save, icon: $globals.icons.save,
text: $t('general.save'), text: $t('general.save'),
@ -113,7 +105,6 @@
@save="$emit('save')" @save="$emit('save')"
@cancel="$emit('cancel')" @cancel="$emit('cancel')"
@delete="$emit('delete')" @delete="$emit('delete')"
@toggle-foods="listItem.isFood = !listItem.isFood"
/> />
</v-card-actions> </v-card-actions>
</v-card> </v-card>

View file

@ -18,8 +18,8 @@ function removeStartingPunctuation(word: string): string {
return word.replace(punctuationAtBeginning, ""); return word.replace(punctuationAtBeginning, "");
} }
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string, recipeIngredientAmountsDisabled: boolean) { function ingredientMatchesWord(ingredient: RecipeIngredient, word: string) {
const searchText = parseIngredientText(ingredient, recipeIngredientAmountsDisabled); const searchText = parseIngredientText(ingredient);
return searchText.toLowerCase().includes(word.toLowerCase()); return searchText.toLowerCase().includes(word.toLowerCase());
} }
@ -39,7 +39,7 @@ function isBlackListedWord(word: string) {
return blackListedText.includes(word) || word.match(blackListedRegexMatch); return blackListedText.includes(word) || word.match(blackListedRegexMatch);
} }
export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string, recipeIngredientAmountsDisabled: boolean): Set<string> { export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string): Set<string> {
const availableIngredients = recipeIngredients const availableIngredients = recipeIngredients
.filter(ingredient => ingredient.referenceId !== undefined) .filter(ingredient => ingredient.referenceId !== undefined)
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string)); .filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
@ -50,7 +50,7 @@ export function useExtractIngredientReferences(recipeIngredients: RecipeIngredie
.map(normalize) .map(normalize)
.filter(word => word.length > 2) .filter(word => word.length > 2)
.filter(word => !isBlackListedWord(word)) .filter(word => !isBlackListedWord(word))
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word, recipeIngredientAmountsDisabled))) .flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word)))
.map(ingredient => ingredient.referenceId as string); .map(ingredient => ingredient.referenceId as string);
// deduplicate // deduplicate

View file

@ -16,33 +16,27 @@ describe(parseIngredientText.name, () => {
...overrides, ...overrides,
}); });
test("uses ingredient note if disableAmount: true", () => {
const ingredient = createRecipeIngredient({ note: "foo" });
expect(parseIngredientText(ingredient, true)).toEqual("foo");
});
test("adds note section if note present", () => { test("adds note section if note present", () => {
const ingredient = createRecipeIngredient({ note: "custom note" }); const ingredient = createRecipeIngredient({ note: "custom note" });
expect(parseIngredientText(ingredient, false)).toContain("custom note"); expect(parseIngredientText(ingredient)).toContain("custom note");
}); });
test("ingredient text with fraction", () => { test("ingredient text with fraction", () => {
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } }); const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
expect(parseIngredientText(ingredient, false, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>"); expect(parseIngredientText(ingredient, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
}); });
test("ingredient text with fraction when unit is null", () => { test("ingredient text with fraction when unit is null", () => {
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: undefined }); const ingredient = createRecipeIngredient({ quantity: 1.5, unit: undefined });
expect(parseIngredientText(ingredient, false, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>"); expect(parseIngredientText(ingredient, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
}); });
test("ingredient text with fraction no formatting", () => { test("ingredient text with fraction no formatting", () => {
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } }); const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
const result = parseIngredientText(ingredient, false, 1, false); const result = parseIngredientText(ingredient, 1, false);
expect(result).not.contain("<"); expect(result).not.contain("<");
expect(result).not.contain(">"); expect(result).not.contain(">");
@ -52,7 +46,7 @@ describe(parseIngredientText.name, () => {
test("sanitizes html", () => { test("sanitizes html", () => {
const ingredient = createRecipeIngredient({ note: "<script>alert('foo')</script>" }); const ingredient = createRecipeIngredient({ note: "<script>alert('foo')</script>" });
expect(parseIngredientText(ingredient, false)).not.toContain("<script>"); expect(parseIngredientText(ingredient)).not.toContain("<script>");
}); });
test("plural test : plural qty : use abbreviation", () => { test("plural test : plural qty : use abbreviation", () => {
@ -62,7 +56,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("2 tbsps diced onions"); expect(parseIngredientText(ingredient)).toEqual("2 tbsps diced onions");
}); });
test("plural test : plural qty : not abbreviation", () => { test("plural test : plural qty : not abbreviation", () => {
@ -72,7 +66,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("2 tablespoons diced onions"); expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onions");
}); });
test("plural test : single qty : use abbreviation", () => { test("plural test : single qty : use abbreviation", () => {
@ -82,7 +76,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("1 tbsp diced onion"); expect(parseIngredientText(ingredient)).toEqual("1 tbsp diced onion");
}); });
test("plural test : single qty : not abbreviation", () => { test("plural test : single qty : not abbreviation", () => {
@ -92,7 +86,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("1 tablespoon diced onion"); expect(parseIngredientText(ingredient)).toEqual("1 tablespoon diced onion");
}); });
test("plural test : small qty : use abbreviation", () => { test("plural test : small qty : use abbreviation", () => {
@ -102,7 +96,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tbsp diced onion"); expect(parseIngredientText(ingredient)).toEqual("0.5 tbsp diced onion");
}); });
test("plural test : small qty : not abbreviation", () => { test("plural test : small qty : not abbreviation", () => {
@ -112,7 +106,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tablespoon diced onion"); expect(parseIngredientText(ingredient)).toEqual("0.5 tablespoon diced onion");
}); });
test("plural test : zero qty", () => { test("plural test : zero qty", () => {
@ -122,7 +116,7 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false)).toEqual("diced onions"); expect(parseIngredientText(ingredient)).toEqual("diced onions");
}); });
test("plural test : single qty, scaled", () => { test("plural test : single qty, scaled", () => {
@ -132,6 +126,6 @@ describe(parseIngredientText.name, () => {
food: { id: "1", name: "diced onion", pluralName: "diced onions" }, food: { id: "1", name: "diced onion", pluralName: "diced onions" },
}); });
expect(parseIngredientText(ingredient, false, 2)).toEqual("2 tablespoons diced onions"); expect(parseIngredientText(ingredient, 2)).toEqual("2 tablespoons diced onions");
}); });
}); });

View file

@ -36,16 +36,7 @@ function useUnitName(unit: CreateIngredientUnit | IngredientUnit | undefined, us
return returnVal; return returnVal;
} }
export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true) { export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true) {
if (disableAmount) {
return {
name: ingredient.note ? sanitizeIngredientHTML(ingredient.note) : undefined,
quantity: undefined,
unit: undefined,
note: undefined,
};
}
const { quantity, food, unit, note } = ingredient; const { quantity, food, unit, note } = ingredient;
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0); const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
const usePluralFood = (!quantity) || quantity * scale > 1; const usePluralFood = (!quantity) || quantity * scale > 1;
@ -82,8 +73,8 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
}; };
} }
export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true): string { export function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, disableAmount, scale, includeFormating); const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim(); const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
return sanitizeIngredientHTML(text); return sanitizeIngredientHTML(text);

View file

@ -587,6 +587,7 @@
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key", "message-key": "Message Key",
"parse": "Parse", "parse": "Parse",
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
"attach-images-hint": "Attach images by dragging & dropping them into the editor", "attach-images-hint": "Attach images by dragging & dropping them into the editor",
"drop-image": "Drop image", "drop-image": "Drop image",
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature", "enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",

View file

@ -22,7 +22,6 @@ export interface CreateHouseholdPreferences {
recipeShowAssets?: boolean; recipeShowAssets?: boolean;
recipeLandscapeView?: boolean; recipeLandscapeView?: boolean;
recipeDisableComments?: boolean; recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
} }
export interface CreateInviteToken { export interface CreateInviteToken {
uses: number; uses: number;
@ -191,7 +190,6 @@ export interface ReadHouseholdPreferences {
recipeShowAssets?: boolean; recipeShowAssets?: boolean;
recipeLandscapeView?: boolean; recipeLandscapeView?: boolean;
recipeDisableComments?: boolean; recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
id: string; id: string;
} }
export interface HouseholdUserSummary { export interface HouseholdUserSummary {
@ -269,7 +267,6 @@ export interface SaveHouseholdPreferences {
recipeShowAssets?: boolean; recipeShowAssets?: boolean;
recipeLandscapeView?: boolean; recipeLandscapeView?: boolean;
recipeDisableComments?: boolean; recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
householdId: string; householdId: string;
} }
export interface SaveInviteToken { export interface SaveInviteToken {
@ -303,8 +300,6 @@ export interface RecipeIngredient {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean;
display?: string; display?: string;
title?: string | null; title?: string | null;
originalText?: string | null; originalText?: string | null;
@ -409,8 +404,6 @@ export interface ShoppingListItemBase {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string; display?: string;
shoppingListId: string; shoppingListId: string;
checked?: boolean; checked?: boolean;
@ -427,8 +420,6 @@ export interface ShoppingListItemCreate {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string; display?: string;
shoppingListId: string; shoppingListId: string;
checked?: boolean; checked?: boolean;
@ -453,8 +444,6 @@ export interface ShoppingListItemOut {
unit?: IngredientUnit | null; unit?: IngredientUnit | null;
food?: IngredientFood | null; food?: IngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string; display?: string;
shoppingListId: string; shoppingListId: string;
checked?: boolean; checked?: boolean;
@ -494,8 +483,6 @@ export interface ShoppingListItemUpdate {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string; display?: string;
shoppingListId: string; shoppingListId: string;
checked?: boolean; checked?: boolean;
@ -513,8 +500,6 @@ export interface ShoppingListItemUpdateBulk {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string; display?: string;
shoppingListId: string; shoppingListId: string;
checked?: boolean; checked?: boolean;
@ -679,14 +664,11 @@ export interface UpdateHouseholdPreferences {
recipeShowAssets?: boolean; recipeShowAssets?: boolean;
recipeLandscapeView?: boolean; recipeLandscapeView?: boolean;
recipeDisableComments?: boolean; recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
} }
export interface RecipeIngredientBase { export interface RecipeIngredientBase {
quantity?: number | null; quantity?: number | null;
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean | null;
display?: string; display?: string;
} }

View file

@ -31,7 +31,6 @@ export interface RecipeSettings {
showAssets?: boolean; showAssets?: boolean;
landscapeView?: boolean; landscapeView?: boolean;
disableComments?: boolean; disableComments?: boolean;
disableAmount?: boolean;
locked?: boolean; locked?: boolean;
} }
export interface AssignTags { export interface AssignTags {
@ -212,8 +211,6 @@ export interface RecipeIngredient {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean;
display?: string; display?: string;
title?: string | null; title?: string | null;
originalText?: string | null; originalText?: string | null;
@ -347,8 +344,6 @@ export interface RecipeIngredientBase {
unit?: IngredientUnit | CreateIngredientUnit | null; unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null; food?: IngredientFood | CreateIngredientFood | null;
note?: string | null; note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean | null;
display?: string; display?: string;
} }
export interface RecipeLastMade { export interface RecipeLastMade {

View file

@ -364,7 +364,6 @@ export default defineNuxtComponent({
confidence: {}, confidence: {},
ingredient: { ingredient: {
quantity: 1.0, quantity: 1.0,
disableAmount: false,
referenceId: uuid4(), referenceId: uuid4(),
}, },
} as ParsedIngredient; } as ParsedIngredient;
@ -409,10 +408,6 @@ export default defineNuxtComponent({
} }
recipe.value.recipeIngredient = ingredients; recipe.value.recipeIngredient = ingredients;
if (recipe.value.settings) {
recipe.value.settings.disableAmount = false;
}
const { response } = await api.recipes.updateOne(recipe.value.slug, recipe.value); const { response } = await api.recipes.updateOne(recipe.value.slug, recipe.value);
if (response?.status === 200) { if (response?.status === 200) {

View file

@ -415,7 +415,6 @@ export default defineNuxtComponent({
showAssets: false, showAssets: false,
landscapeView: false, landscapeView: false,
disableComments: false, disableComments: false,
disableAmount: false,
locked: false, locked: false,
}); });

View file

@ -95,12 +95,6 @@ export default defineNuxtComponent({
label: i18n.t("group.disable-users-from-commenting-on-recipes"), label: i18n.t("group.disable-users-from-commenting-on-recipes"),
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"), description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
} as Preference, } as Preference,
{
key: "recipeDisableAmount",
value: household.value.preferences.recipeDisableAmount || false,
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
} as Preference,
]; ];
}); });

View file

@ -762,31 +762,12 @@ export default defineNuxtComponent({
const contextActions = { const contextActions = {
delete: "delete", delete: "delete",
setIngredient: "setIngredient",
}; };
const contextMenu = [ const contextMenu = [
{ title: i18n.t("general.delete"), action: contextActions.delete }, { title: i18n.t("general.delete"), action: contextActions.delete },
{ title: i18n.t("recipe.ingredient"), action: contextActions.setIngredient },
]; ];
function contextMenuAction(action: string, item: ShoppingListItemOut, idx: number) {
if (!shoppingList.value?.listItems) {
return;
}
switch (action) {
case contextActions.delete:
shoppingList.value.listItems = shoppingList.value?.listItems.filter(itm => itm.id !== item.id);
break;
case contextActions.setIngredient:
shoppingList.value.listItems[idx].isFood = !shoppingList.value.listItems[idx].isFood;
break;
default:
break;
}
}
// ===================================== // =====================================
// Labels, Units, Foods // Labels, Units, Foods
// TODO: Extract to Composable // TODO: Extract to Composable
@ -901,7 +882,7 @@ export default defineNuxtComponent({
shoppingList.value.listItems.forEach((item) => { shoppingList.value.listItems.forEach((item) => {
const key = item.checked const key = item.checked
? checkedItemKey ? checkedItemKey
: item.isFood && item.food?.name : item.food?.name
? item.food.name ? item.food.name
: item.note || ""; : item.note || "";
@ -1087,13 +1068,12 @@ export default defineNuxtComponent({
const createEditorOpen = ref(false); const createEditorOpen = ref(false);
const createListItemData = ref<ShoppingListItemOut>(listItemFactory()); const createListItemData = ref<ShoppingListItemOut>(listItemFactory());
function listItemFactory(isFood = false): ShoppingListItemOut { function listItemFactory(): ShoppingListItemOut {
return { return {
id: uuid4(), id: uuid4(),
shoppingListId: id, shoppingListId: id,
checked: false, checked: false,
position: shoppingList.value?.listItems?.length || 1, position: shoppingList.value?.listItems?.length || 1,
isFood,
quantity: 0, quantity: 0,
note: "", note: "",
labelId: undefined, labelId: undefined,
@ -1144,7 +1124,7 @@ export default defineNuxtComponent({
shoppingList.value.listItems.push(createListItemData.value); shoppingList.value.listItems.push(createListItemData.value);
updateListItemOrder(); updateListItemOrder();
} }
createListItemData.value = listItemFactory(createListItemData.value.isFood || false); createListItemData.value = listItemFactory();
refresh(); refresh();
} }
@ -1217,7 +1197,6 @@ export default defineNuxtComponent({
addRecipeReferenceToList, addRecipeReferenceToList,
allLabels, allLabels,
contextMenu, contextMenu,
contextMenuAction,
copyListItems, copyListItems,
createEditorOpen, createEditorOpen,
createListItem, createListItem,

View file

@ -0,0 +1,45 @@
"""empty migration to fix food flag data
Revision ID: d7b3ce6fa31a
Revises: 7cf3054cbbcc
Create Date: 2025-07-11 20:17:10.543280
"""
from textwrap import dedent
from alembic import op
# revision identifiers, used by Alembic.
revision = "d7b3ce6fa31a"
down_revision: str | None = "7cf3054cbbcc"
branch_labels: str | tuple[str, ...] | None = None
depends_on: str | tuple[str, ...] | None = None
def is_postgres():
return op.get_context().dialect.name == "postgresql"
def upgrade():
# Update recipes with disable_amount=True: set ingredient quantities of 1 to 0
op.execute(
dedent(
f"""
UPDATE recipes_ingredients
SET quantity = 0
WHERE quantity = 1
AND recipe_id IN (
SELECT r.id
FROM recipes r
JOIN recipe_settings rs ON r.id = rs.recipe_id
WHERE rs.disable_amount = {"true" if is_postgres() else "1"}
)
"""
)
)
def downgrade():
pass

View file

@ -31,6 +31,8 @@ class HouseholdPreferencesModel(SqlAlchemyBase, BaseMixins):
recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False) recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
recipe_landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean, default=False) recipe_landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
recipe_disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False) recipe_disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
# Deprecated
recipe_disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True) recipe_disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
@auto_init() @auto_init()

View file

@ -65,7 +65,6 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
quantity: Mapped[float | None] = mapped_column(Float, default=1) quantity: Mapped[float | None] = mapped_column(Float, default=1)
note: Mapped[str | None] = mapped_column(String) note: Mapped[str | None] = mapped_column(String)
is_food: Mapped[bool | None] = mapped_column(Boolean, default=False)
extras: Mapped[list[ShoppingListItemExtras]] = orm.relationship( extras: Mapped[list[ShoppingListItemExtras]] = orm.relationship(
"ShoppingListItemExtras", cascade="all, delete-orphan" "ShoppingListItemExtras", cascade="all, delete-orphan"
) )
@ -88,6 +87,9 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
) )
model_config = ConfigDict(exclude={"label", "food", "unit"}) model_config = ConfigDict(exclude={"label", "food", "unit"})
# Deprecated
is_food: Mapped[bool | None] = mapped_column(Boolean, default=False)
@api_extras @api_extras
@auto_init() @auto_init()
def __init__(self, **_) -> None: def __init__(self, **_) -> None:

View file

@ -13,10 +13,12 @@ class RecipeSettings(SqlAlchemyBase):
show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean) show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean)
show_assets: Mapped[bool | None] = mapped_column(sa.Boolean) show_assets: Mapped[bool | None] = mapped_column(sa.Boolean)
landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean) landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean)
disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False) disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
locked: Mapped[bool | None] = mapped_column(sa.Boolean, default=False) locked: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
# Deprecated
disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
def __init__( def __init__(
self, self,
public=True, public=True,

View file

@ -440,7 +440,6 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
) )
q = ( q = (
q.join(settings_alias, self.model.settings) q.join(settings_alias, self.model.settings)
.filter(settings_alias.disable_amount == False) # noqa: E712 - required for SQLAlchemy comparison
.outerjoin(unmatched_foods_query, self.model.id == unmatched_foods_query.c.recipe_id) .outerjoin(unmatched_foods_query, self.model.id == unmatched_foods_query.c.recipe_id)
.outerjoin(total_user_foods_query, self.model.id == total_user_foods_query.c.recipe_id) .outerjoin(total_user_foods_query, self.model.id == total_user_foods_query.c.recipe_id)
.filter( .filter(

View file

@ -101,22 +101,18 @@ def content_with_meta(group_slug: str, recipe: Recipe) -> str:
image_url = "https://raw.githubusercontent.com/mealie-recipes/mealie/9571816ac4eed5beacfc0abf6c03eff1427fd0eb/frontend/static/icons/android-chrome-512x512.png" image_url = "https://raw.githubusercontent.com/mealie-recipes/mealie/9571816ac4eed5beacfc0abf6c03eff1427fd0eb/frontend/static/icons/android-chrome-512x512.png"
ingredients: list[str] = [] ingredients: list[str] = []
if recipe.settings.disable_amount: # type: ignore for ing in recipe.recipe_ingredient:
ingredients = [escape(i.note) for i in recipe.recipe_ingredient if i.note] s = ""
if ing.quantity:
s += f"{ing.quantity} "
if ing.unit:
s += f"{ing.unit.name} "
if ing.food:
s += f"{ing.food.name} "
if ing.note:
s += f"{ing.note}"
else: ingredients.append(escape(s))
for ing in recipe.recipe_ingredient:
s = ""
if ing.quantity:
s += f"{ing.quantity} "
if ing.unit:
s += f"{ing.unit.name} "
if ing.food:
s += f"{ing.food.name} "
if ing.note:
s += f"{ing.note}"
ingredients.append(escape(s))
nutrition: dict[str, str | None] = recipe.nutrition.model_dump(by_alias=True) if recipe.nutrition else {} nutrition: dict[str, str | None] = recipe.nutrition.model_dump(by_alias=True) if recipe.nutrition else {}
for k, v in nutrition.items(): for k, v in nutrition.items():

View file

@ -66,7 +66,6 @@ class ShoppingListItemBase(RecipeIngredientBase):
label_id: UUID4 | None = None label_id: UUID4 | None = None
unit_id: UUID4 | None = None unit_id: UUID4 | None = None
is_food: bool = False
extras: dict | None = {} extras: dict | None = {}
@field_validator("extras", mode="before") @field_validator("extras", mode="before")

View file

@ -18,7 +18,6 @@ class UpdateHouseholdPreferences(MealieModel):
recipe_show_assets: bool = False recipe_show_assets: bool = False
recipe_landscape_view: bool = False recipe_landscape_view: bool = False
recipe_disable_comments: bool = False recipe_disable_comments: bool = False
recipe_disable_amount: bool = True
class CreateHouseholdPreferences(UpdateHouseholdPreferences): ... class CreateHouseholdPreferences(UpdateHouseholdPreferences): ...

View file

@ -6,7 +6,7 @@ from pathlib import Path
from typing import Annotated, Any, ClassVar from typing import Annotated, Any, ClassVar
from uuid import uuid4 from uuid import uuid4
from pydantic import UUID4, BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic import UUID4, BaseModel, ConfigDict, Field, field_validator
from pydantic_core.core_schema import ValidationInfo from pydantic_core.core_schema import ValidationInfo
from slugify import slugify from slugify import slugify
from sqlalchemy import Select, desc, func, or_, select, text from sqlalchemy import Select, desc, func, or_, select, text
@ -228,18 +228,6 @@ class Recipe(RecipeSummary):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@model_validator(mode="after")
def calculate_missing_food_flags_and_format_display(self):
disable_amount = self.settings.disable_amount if self.settings else True
for ingredient in self.recipe_ingredient:
ingredient.disable_amount = disable_amount
ingredient.is_food = not ingredient.disable_amount
# recalculate the display property, since it depends on the disable_amount flag
ingredient.display = ingredient._format_display()
return self
@field_validator("slug", mode="before") @field_validator("slug", mode="before")
def validate_slug(slug: str, info: ValidationInfo): def validate_slug(slug: str, info: ValidationInfo):
if not info.data.get("name"): if not info.data.get("name"):

View file

@ -152,13 +152,11 @@ class IngredientUnit(CreateIngredientUnit):
class RecipeIngredientBase(MealieModel): class RecipeIngredientBase(MealieModel):
quantity: NoneFloat = 1 quantity: NoneFloat = 0
unit: IngredientUnit | CreateIngredientUnit | None = None unit: IngredientUnit | CreateIngredientUnit | None = None
food: IngredientFood | CreateIngredientFood | None = None food: IngredientFood | CreateIngredientFood | None = None
note: str | None = "" note: str | None = ""
is_food: bool | None = None
disable_amount: bool | None = None
display: str = "" display: str = ""
""" """
How the ingredient should be displayed How the ingredient should be displayed
@ -166,20 +164,6 @@ class RecipeIngredientBase(MealieModel):
Automatically calculated after the object is created, unless overwritten Automatically calculated after the object is created, unless overwritten
""" """
@model_validator(mode="after")
def calculate_missing_food_flags(self):
# calculate missing is_food and disable_amount values
# we can't do this in a validator since they depend on each other
if self.is_food is None and self.disable_amount is not None:
self.is_food = not self.disable_amount
elif self.disable_amount is None and self.is_food is not None:
self.disable_amount = not self.is_food
elif self.is_food is None and self.disable_amount is None:
self.is_food = bool(self.food)
self.disable_amount = not self.is_food
return self
@model_validator(mode="after") @model_validator(mode="after")
def format_display(self): def format_display(self):
if not self.display: if not self.display:
@ -266,28 +250,17 @@ class RecipeIngredientBase(MealieModel):
def _format_display(self) -> str: def _format_display(self) -> str:
components = [] components = []
use_food = True if self.quantity:
if self.is_food is False:
use_food = False
elif self.disable_amount is True:
use_food = False
# ingredients with no food come across with a qty of 1, which looks weird
# e.g. "1 2 tbsp of olive oil"
if self.quantity and (use_food or self.quantity != 1):
components.append(self._format_quantity_for_display()) components.append(self._format_quantity_for_display())
if not use_food: if self.quantity and self.unit:
components.append(self.note or "") components.append(self._format_unit_for_display())
else:
if self.quantity and self.unit:
components.append(self._format_unit_for_display())
if self.food: if self.food:
components.append(self._format_food_for_display()) components.append(self._format_food_for_display())
if self.note: if self.note:
components.append(self.note) components.append(self.note)
return " ".join(components).strip() return " ".join(components).strip()
@ -299,7 +272,6 @@ class IngredientUnitPagination(PaginationBase):
class RecipeIngredient(RecipeIngredientBase): class RecipeIngredient(RecipeIngredientBase):
title: str | None = None title: str | None = None
original_text: str | None = None original_text: str | None = None
disable_amount: bool = True
# 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
# It is required for the reorder and section titles to function properly because of how # It is required for the reorder and section titles to function properly because of how

View file

@ -9,6 +9,5 @@ class RecipeSettings(MealieModel):
show_assets: bool = False show_assets: bool = False
landscape_view: bool = False landscape_view: bool = False
disable_comments: bool = True disable_comments: bool = True
disable_amount: bool = True
locked: bool = False locked: bool = False
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)

View file

@ -312,12 +312,10 @@ class ShoppingListService:
list_items: list[ShoppingListItemCreate] = [] list_items: list[ShoppingListItemCreate] = []
for ingredient in recipe_ingredients: for ingredient in recipe_ingredients:
if isinstance(ingredient.food, IngredientFood): if isinstance(ingredient.food, IngredientFood):
is_food = True
food_id = ingredient.food.id food_id = ingredient.food.id
label_id = ingredient.food.label_id label_id = ingredient.food.label_id
else: else:
is_food = False
food_id = None food_id = None
label_id = None label_id = None
@ -329,7 +327,6 @@ class ShoppingListService:
new_item = ShoppingListItemCreate( new_item = ShoppingListItemCreate(
shopping_list_id=list_id, shopping_list_id=list_id,
is_food=is_food,
note=ingredient.note, note=ingredient.note,
quantity=ingredient.quantity * scale if ingredient.quantity else 0, quantity=ingredient.quantity * scale if ingredient.quantity else 0,
food_id=food_id, food_id=food_id,

View file

@ -177,7 +177,6 @@ class BaseMigrator(BaseService):
show_assets=self.household.preferences.recipe_show_assets, show_assets=self.household.preferences.recipe_show_assets,
landscape_view=self.household.preferences.recipe_landscape_view, landscape_view=self.household.preferences.recipe_landscape_view,
disable_comments=self.household.preferences.recipe_disable_comments, disable_comments=self.household.preferences.recipe_disable_comments,
disable_amount=self.household.preferences.recipe_disable_amount,
) )
for recipe in validated_recipes: for recipe in validated_recipes:

View file

@ -36,7 +36,6 @@ class BruteForceParser(ABCIngredientParser):
ingredient=RecipeIngredient( ingredient=RecipeIngredient(
unit=CreateIngredientUnit(name=bfi.unit), unit=CreateIngredientUnit(name=bfi.unit),
food=CreateIngredientFood(name=bfi.food), food=CreateIngredientFood(name=bfi.food),
disable_amount=False,
quantity=bfi.amount, quantity=bfi.amount,
note=bfi.note, note=bfi.note,
), ),
@ -151,7 +150,6 @@ class NLPParser(ABCIngredientParser):
quantity=qty, quantity=qty,
unit=CreateIngredientUnit(name=unit) if unit else None, unit=CreateIngredientUnit(name=unit) if unit else None,
food=CreateIngredientFood(name=food) if food else None, food=CreateIngredientFood(name=food) if food else None,
disable_amount=False,
note=note, note=note,
), ),
) )

View file

@ -173,7 +173,6 @@ class RecipeService(RecipeServiceBase):
show_assets=self.household.preferences.recipe_show_assets, show_assets=self.household.preferences.recipe_show_assets,
landscape_view=self.household.preferences.recipe_landscape_view, landscape_view=self.household.preferences.recipe_landscape_view,
disable_comments=self.household.preferences.recipe_disable_comments, disable_comments=self.household.preferences.recipe_disable_comments,
disable_amount=self.household.preferences.recipe_disable_amount,
) )
else: else:
data.settings = RecipeSettings() data.settings = RecipeSettings()

View file

@ -74,7 +74,6 @@ class RegistrationService:
recipe_show_assets=self.registration.advanced, recipe_show_assets=self.registration.advanced,
recipe_landscape_view=False, recipe_landscape_view=False,
recipe_disable_comments=self.registration.advanced, recipe_disable_comments=self.registration.advanced,
recipe_disable_amount=self.registration.advanced,
) )
return HouseholdService.create_household(group_repos, household_data, household_preferences) return HouseholdService.create_household(group_repos, household_data, household_preferences)

View file

@ -37,12 +37,12 @@ def recipe_ingredient_only(unique_user: TestUser):
group_id=unique_user.group_id, group_id=unique_user.group_id,
name=random_string(10), name=random_string(10),
recipe_ingredient=[ recipe_ingredient=[
RecipeIngredient(note="Ingredient 1"), RecipeIngredient(quantity=1, note="Ingredient 1"),
RecipeIngredient(note="Ingredient 2"), RecipeIngredient(quantity=1, note="Ingredient 2"),
RecipeIngredient(note="Ingredient 3"), RecipeIngredient(quantity=1, note="Ingredient 3"),
RecipeIngredient(note="Ingredient 4"), RecipeIngredient(quantity=1, note="Ingredient 4"),
RecipeIngredient(note="Ingredient 5"), RecipeIngredient(quantity=1, note="Ingredient 5"),
RecipeIngredient(note="Ingredient 6"), RecipeIngredient(quantity=1, note="Ingredient 6"),
], ],
) )

View file

@ -12,7 +12,6 @@ def create_item(list_id: UUID4) -> dict:
"shopping_list_id": str(list_id), "shopping_list_id": str(list_id),
"checked": False, "checked": False,
"position": 0, "position": 0,
"is_food": False,
"note": random_string(10), "note": random_string(10),
"quantity": 1, "quantity": 1,
"unit_id": None, "unit_id": None,

View file

@ -49,7 +49,6 @@ def test_admin_update_household(api_client: TestClient, admin_user: TestUser, un
"recipeShowAssets": random_bool(), "recipeShowAssets": random_bool(),
"recipeLandscapeView": random_bool(), "recipeLandscapeView": random_bool(),
"recipeDisableComments": random_bool(), "recipeDisableComments": random_bool(),
"recipeDisableAmount": random_bool(),
}, },
} }

View file

@ -377,7 +377,6 @@ def test_get_suggested_recipes(
SaveIngredientFood(id=uuid4(), name=random_string(), group_id=unique_user.group_id) SaveIngredientFood(id=uuid4(), name=random_string(), group_id=unique_user.group_id)
) )
random_recipe.recipe_ingredient = [RecipeIngredient(food_id=known_food.id, food=known_food)] random_recipe.recipe_ingredient = [RecipeIngredient(food_id=known_food.id, food=known_food)]
random_recipe.settings.disable_amount = False
database.recipes.update(random_recipe.slug, random_recipe) database.recipes.update(random_recipe.slug, random_recipe)
## Try to find suggested recipes ## Try to find suggested recipes

View file

@ -145,6 +145,11 @@ from mealie.schema.recipe.recipe_ingredient import (
@pytest.mark.parametrize( @pytest.mark.parametrize(
["food", "expected_food_singular_string", "expected_food_plural_string"], ["food", "expected_food_singular_string", "expected_food_plural_string"],
[ [
[
None,
"",
"",
],
[ [
IngredientFood(id=uuid4(), name="chopped onion", plural_name=None), IngredientFood(id=uuid4(), name="chopped onion", plural_name=None),
"chopped onion", "chopped onion",
@ -157,16 +162,14 @@ from mealie.schema.recipe.recipe_ingredient import (
], ],
], ],
) )
@pytest.mark.parametrize("note", ["very thin", ""]) @pytest.mark.parametrize("note", ["very thin", "", None])
@pytest.mark.parametrize("use_food", [True, False])
def test_ingredient_display( def test_ingredient_display(
quantity: float | None, quantity: float | None,
quantity_display_decimal: str, quantity_display_decimal: str,
quantity_display_fraction: str, quantity_display_fraction: str,
unit: IngredientUnit | None, unit: IngredientUnit | None,
food: IngredientFood, food: IngredientFood | None,
note: str, note: str | None,
use_food: bool,
expect_display_fraction: bool, expect_display_fraction: bool,
expect_plural_unit: bool, expect_plural_unit: bool,
expect_plural_food: bool, expect_plural_food: bool,
@ -176,36 +179,25 @@ def test_ingredient_display(
expected_food_plural_string: str, expected_food_plural_string: str,
): ):
expected_components = [] expected_components = []
if use_food: if expect_display_fraction:
if expect_display_fraction: expected_components.append(quantity_display_fraction)
expected_components.append(quantity_display_fraction) else:
expected_components.append(quantity_display_decimal)
if quantity:
if expect_plural_unit:
expected_components.append(expected_unit_plural_string)
else: else:
expected_components.append(quantity_display_decimal) expected_components.append(expected_unit_singular_string)
if quantity:
if expect_plural_unit:
expected_components.append(expected_unit_plural_string)
else:
expected_components.append(expected_unit_singular_string)
if food:
if expect_plural_food: if expect_plural_food:
expected_components.append(expected_food_plural_string) expected_components.append(expected_food_plural_string)
else: else:
expected_components.append(expected_food_singular_string) expected_components.append(expected_food_singular_string)
expected_components.append(note) expected_components.append(note or "")
else:
if quantity != 0 and quantity != 1:
if expect_display_fraction:
expected_components.append(quantity_display_fraction)
else:
expected_components.append(quantity_display_decimal)
expected_components.append(note)
expected_display_value = " ".join(c for c in expected_components if c) expected_display_value = " ".join(c for c in expected_components if c)
ingredient = RecipeIngredient( ingredient = RecipeIngredient(quantity=quantity, unit=unit, food=food, note=note)
quantity=quantity, unit=unit, food=food, note=note, use_food=use_food, disable_amount=not use_food
)
assert ingredient.display == expected_display_value assert ingredient.display == expected_display_value

View file

@ -48,7 +48,6 @@ def create_recipe(
*, *,
foods: list[IngredientFood] | None = None, foods: list[IngredientFood] | None = None,
tools: list[RecipeToolOut] | None = None, tools: list[RecipeToolOut] | None = None,
disable_amount: bool = False,
**kwargs, **kwargs,
): ):
if foods: if foods:
@ -63,7 +62,7 @@ def create_recipe(
name=kwargs.pop("name", random_string()), name=kwargs.pop("name", random_string()),
recipe_ingredient=ingredients, recipe_ingredient=ingredients,
tools=tools or [], tools=tools or [],
settings=RecipeSettings(disable_amount=disable_amount), settings=RecipeSettings(),
**kwargs, **kwargs,
) )
) )
@ -338,60 +337,6 @@ def test_include_recipes_with_no_tools(api_client: TestClient, unique_user: Test
unique_user.repos.recipes.delete(recipe.slug) unique_user.repos.recipes.delete(recipe.slug)
def test_ignore_recipes_with_ingredient_amounts_disabled_with_foods(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user)
recipe_with_amounts = create_recipe(unique_user, foods=[known_food])
recipe_without_amounts = create_recipe(unique_user, foods=[known_food], disable_amount=True)
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={"maxMissingFoods": 0, "maxMissingTools": 0, "foods": [str(known_food.id)]},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {str(recipe_with_amounts.id)}
for item in data["items"]:
assert item["missingFoods"] == []
finally:
for recipe in [recipe_with_amounts, recipe_without_amounts]:
unique_user.repos.recipes.delete(recipe.slug)
def test_include_recipes_with_ingredient_amounts_disabled_without_foods(api_client: TestClient, unique_user: TestUser):
known_tool = create_tool(unique_user)
recipe_with_amounts = create_recipe(unique_user, tools=[known_tool])
recipe_without_amounts = create_recipe(unique_user, tools=[known_tool], disable_amount=True)
try:
response = api_client.get(
api_routes.recipes_suggestions,
params={
"maxMissingFoods": 0,
"maxMissingTools": 0,
"includeFoodsOnHand": False,
"tools": [str(known_tool.id)],
},
headers=unique_user.token,
)
response.raise_for_status()
data = response.json()
assert {item["recipe"]["id"] for item in data["items"]} == {
str(recipe_with_amounts.id),
str(recipe_without_amounts.id),
}
for item in data["items"]:
assert item["missingFoods"] == []
finally:
for recipe in [recipe_with_amounts, recipe_without_amounts]:
unique_user.repos.recipes.delete(recipe.slug)
def test_exclude_recipes_with_no_user_foods(api_client: TestClient, unique_user: TestUser): def test_exclude_recipes_with_no_user_foods(api_client: TestClient, unique_user: TestUser):
known_food = create_food(unique_user) known_food = create_food(unique_user)
food_on_hand = create_food(unique_user, on_hand=True) food_on_hand = create_food(unique_user, on_hand=True)

View file

@ -20,8 +20,6 @@ def test_shopping_list_ingredient_validation():
"updatedAt": "2024-02-26T18:29:46.190758", "updatedAt": "2024-02-26T18:29:46.190758",
}, },
"note": "", "note": "",
"isFood": True,
"disableAmount": False,
"shoppingListId": "dc8bce82-2da9-49f0-94e6-6d69d311490e", "shoppingListId": "dc8bce82-2da9-49f0-94e6-6d69d311490e",
"checked": False, "checked": False,
"position": 5, "position": 5,