mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: sort by labels in shopping list copy if labels toggled (#3226)
* feat: sort by labels in shopping list copy if labels toggled * fix: call parent validator in shopping list item out (#3227) * fix: add a unit test for (#3227) * fixed messy post_validate logic * feat: label headings in shopping list copy * feat: blank line for each group in shopping list copy --------- Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
parent
2809b87ab0
commit
52de8afe2d
@ -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}]`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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"
|
Loading…
x
Reference in New Issue
Block a user