diff --git a/frontend/pages/shopping-lists/_id.vue b/frontend/pages/shopping-lists/_id.vue index c5bdd2cc8249..b4c31f32c2e9 100644 --- a/frontend/pages/shopping-lists/_id.vue +++ b/frontend/pages/shopping-lists/_id.vue @@ -337,21 +337,50 @@ export default defineComponent({ const copy = useCopyList(); function copyListItems(copyType: CopyTypes) { - const items = shoppingList.value?.listItems?.filter((item) => !item.checked); + const text: string[] = []; - if (!items) { - return; + if (preferences.value.viewByLabel) { + // if we're sorting by label, we want the copied text in subsections + Object.entries(itemsByLabel.value).forEach(([label, items], idx) => { + // for every group except the first, add a blank line + if (idx) { + text.push("") + } + + // add an appropriate heading for the label depending on the copy format + text.push(formatCopiedLabelHeading(copyType, label)) + + // now add the appropriately formatted list items with the given label + items.forEach((item) => text.push(formatCopiedListItem(copyType, item))) + }) + } else { + // labels are toggled off, so just copy in the order they come in + const items = shoppingList.value?.listItems?.filter((item) => !item.checked) + + items?.forEach((item) => { + text.push(formatCopiedListItem(copyType, item)) + }); } - const text: string[] = items.map((itm) => itm.display || ""); + copy.copyPlain(text); + } + function formatCopiedListItem(copyType: CopyTypes, item: ShoppingListItemOut): string { + const display = item.display || "" switch (copyType) { case "markdown": - copy.copyMarkdownCheckList(text); - break; + return `- [ ] ${display}` default: - copy.copyPlain(text); - break; + return display + } + } + + function formatCopiedLabelHeading(copyType: CopyTypes, label: string): string { + switch (copyType) { + case "markdown": + return `# ${label}` + default: + return `[${label}]` } } diff --git a/mealie/schema/group/group_shopping_list.py b/mealie/schema/group/group_shopping_list.py index ae5a27da5ece..99bb0afe62cf 100644 --- a/mealie/schema/group/group_shopping_list.py +++ b/mealie/schema/group/group_shopping_list.py @@ -101,7 +101,7 @@ class ShoppingListItemOut(ShoppingListItemBase): update_at: datetime | None = None @model_validator(mode="after") - def post_validate(self): + def populate_missing_label(self): # if we're missing a label, but the food has a label, use that as the label if (not self.label) and (self.food and self.food.label): self.label = self.food.label diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py index 483247d07f20..e0914ed080a1 100644 --- a/mealie/schema/recipe/recipe.py +++ b/mealie/schema/recipe/recipe.py @@ -184,13 +184,13 @@ class Recipe(RecipeSummary): model_config = ConfigDict(from_attributes=True) @model_validator(mode="after") - def post_validate(self): - # the ingredient disable_amount property is unreliable, - # so we set it here and recalculate the display property + 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 diff --git a/mealie/schema/recipe/recipe_ingredient.py b/mealie/schema/recipe/recipe_ingredient.py index 5eaa0e9f12c3..7609fa5486e4 100644 --- a/mealie/schema/recipe/recipe_ingredient.py +++ b/mealie/schema/recipe/recipe_ingredient.py @@ -145,7 +145,7 @@ class RecipeIngredientBase(MealieModel): """ @model_validator(mode="after") - def post_validate(self): + 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: @@ -156,7 +156,10 @@ class RecipeIngredientBase(MealieModel): self.is_food = bool(self.food) self.disable_amount = not self.is_food - # format the display property + return self + + @model_validator(mode="after") + def format_display(self): if not self.display: self.display = self._format_display() diff --git a/tests/unit_tests/schema_tests/test_shopping_list_ingredient.py b/tests/unit_tests/schema_tests/test_shopping_list_ingredient.py new file mode 100644 index 000000000000..408c25e8cb63 --- /dev/null +++ b/tests/unit_tests/schema_tests/test_shopping_list_ingredient.py @@ -0,0 +1,37 @@ +from mealie.schema.group.group_shopping_list import ShoppingListItemOut + + +def test_shopping_list_ingredient_validation(): + db_obj = { + "quantity": 8, + "unit": None, + "food": { + "id": "4cf32eeb-d136-472d-86c7-287b6328d21f", + "name": "bell peppers", + "pluralName": None, + "description": "", + "extras": {}, + "labelId": None, + "aliases": [], + "label": None, + "createdAt": "2024-02-26T18:29:46.190754", + "updateAt": "2024-02-26T18:29:46.190758", + }, + "note": "", + "isFood": True, + "disableAmount": False, + "shoppingListId": "dc8bce82-2da9-49f0-94e6-6d69d311490e", + "checked": False, + "position": 5, + "foodId": "4cf32eeb-d136-472d-86c7-287b6328d21f", + "labelId": None, + "unitId": None, + "extras": {}, + "id": "80f4df25-6139-4d30-be0c-4100f50e5396", + "label": None, + "recipeReferences": [], + "createdAt": "2024-02-27T10:18:19.274677", + "updateAt": "2024-02-27T11:26:32.643392", + } + out = ShoppingListItemOut.model_validate(db_obj) + assert out.display == "8 bell peppers"