mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feat: group recipe ingredients by section titles (#5864)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
		| @@ -29,32 +29,49 @@ | |||||||
|             {{ activeText }} |             {{ activeText }} | ||||||
|           </p> |           </p> | ||||||
|           <v-divider class="mb-4" /> |           <v-divider class="mb-4" /> | ||||||
|  |           <template v-if="Object.keys(groupedUnusedIngredients).length > 0"> | ||||||
|  |             <h4 class="py-3 ml-1"> | ||||||
|  |               {{ $t("recipe.unlinked") }} | ||||||
|  |             </h4> | ||||||
|  |             <template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title"> | ||||||
|  |             <h4 v-if="title" class="py-3 ml-1 pl-4"> | ||||||
|  |               {{ title }} | ||||||
|  |             </h4> | ||||||
|             <v-checkbox-btn |             <v-checkbox-btn | ||||||
|             v-for="ing in unusedIngredients" |               v-for="ing in ingredients" | ||||||
|               :key="ing.referenceId" |               :key="ing.referenceId" | ||||||
|               v-model="activeRefs" |               v-model="activeRefs" | ||||||
|               :value="ing.referenceId" |               :value="ing.referenceId" | ||||||
|  |               class="ml-4" | ||||||
|             > |             > | ||||||
|               <template #label> |               <template #label> | ||||||
|                 <RecipeIngredientHtml :markup="parseIngredientText(ing)" /> |                 <RecipeIngredientHtml :markup="parseIngredientText(ing)" /> | ||||||
|               </template> |               </template> | ||||||
|             </v-checkbox-btn> |             </v-checkbox-btn> | ||||||
|  |           </template> | ||||||
|  |           </template> | ||||||
|  |  | ||||||
|           <template v-if="usedIngredients.length > 0"> |           <template v-if="Object.keys(groupedUsedIngredients).length > 0"> | ||||||
|             <h4 class="py-3 ml-1"> |             <h4 class="py-3 ml-1"> | ||||||
|               {{ $t("recipe.linked-to-other-step") }} |               {{ $t("recipe.linked-to-other-step") }} | ||||||
|             </h4> |             </h4> | ||||||
|  |             <template v-for="(ingredients, title) in groupedUsedIngredients" :key="title"> | ||||||
|  |               <h4 v-if="title" class="py-3 ml-1 pl-4"> | ||||||
|  |                 {{ title }} | ||||||
|  |               </h4> | ||||||
|               <v-checkbox-btn |               <v-checkbox-btn | ||||||
|               v-for="ing in usedIngredients" |                 v-for="ing in ingredients" | ||||||
|                 :key="ing.referenceId" |                 :key="ing.referenceId" | ||||||
|                 v-model="activeRefs" |                 v-model="activeRefs" | ||||||
|                 :value="ing.referenceId" |                 :value="ing.referenceId" | ||||||
|  |                 class="ml-4" | ||||||
|               > |               > | ||||||
|                 <template #label> |                 <template #label> | ||||||
|                   <RecipeIngredientHtml :markup="parseIngredientText(ing)" /> |                   <RecipeIngredientHtml :markup="parseIngredientText(ing)" /> | ||||||
|                 </template> |                 </template> | ||||||
|               </v-checkbox-btn> |               </v-checkbox-btn> | ||||||
|             </template> |             </template> | ||||||
|  |           </template> | ||||||
|         </v-card-text> |         </v-card-text> | ||||||
|  |  | ||||||
|         <v-divider /> |         <v-divider /> | ||||||
| @@ -563,6 +580,71 @@ const ingredientLookup = computed(() => { | |||||||
|   }, results); |   }, results); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | // Map each ingredient's referenceId to its section title | ||||||
|  | const ingredientSectionTitles = computed(() => { | ||||||
|  |   const titleMap: { [key: string]: string } = {}; | ||||||
|  |   let currentTitle = ""; | ||||||
|  |  | ||||||
|  |   // Go through all ingredients in order | ||||||
|  |   props.recipe.recipeIngredient.forEach((ingredient) => { | ||||||
|  |     if (ingredient.referenceId === undefined) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // If this ingredient has a title, update the current title | ||||||
|  |     if (ingredient.title) { | ||||||
|  |       currentTitle = ingredient.title; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Assign the current title to this ingredient | ||||||
|  |     titleMap[ingredient.referenceId] = currentTitle; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return titleMap; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const groupedUnusedIngredients = computed(() => { | ||||||
|  |   const groups: { [key: string]: RecipeIngredient[] } = {}; | ||||||
|  |  | ||||||
|  |   // Group ingredients by section title | ||||||
|  |   unusedIngredients.value.forEach((ingredient) => { | ||||||
|  |     if (ingredient.referenceId === undefined) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Use the section title from the mapping, or fallback to the ingredient's own title | ||||||
|  |     const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || ""; | ||||||
|  |  | ||||||
|  |     if (!groups[title]) { | ||||||
|  |       groups[title] = []; | ||||||
|  |     } | ||||||
|  |     groups[title].push(ingredient); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return groups; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const groupedUsedIngredients = computed(() => { | ||||||
|  |   const groups: { [key: string]: RecipeIngredient[] } = {}; | ||||||
|  |  | ||||||
|  |   // Group ingredients by section title | ||||||
|  |   usedIngredients.value.forEach((ingredient) => { | ||||||
|  |     if (ingredient.referenceId === undefined) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Use the section title from the mapping, or fallback to the ingredient's own title | ||||||
|  |     const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || ""; | ||||||
|  |  | ||||||
|  |     if (!groups[title]) { | ||||||
|  |       groups[title] = []; | ||||||
|  |     } | ||||||
|  |     groups[title].push(ingredient); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return groups; | ||||||
|  | }); | ||||||
|  |  | ||||||
| function getIngredientByRefId(refId: string | undefined) { | function getIngredientByRefId(refId: string | undefined) { | ||||||
|   if (refId === undefined) { |   if (refId === undefined) { | ||||||
|     return ""; |     return ""; | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ function useUnitName(unit: CreateIngredientUnit | IngredientUnit | undefined, us | |||||||
| } | } | ||||||
|  |  | ||||||
| export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true) { | export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true) { | ||||||
|   const { quantity, food, unit, note } = ingredient; |   const { quantity, food, unit, note, title } = 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; | ||||||
|  |  | ||||||
| @@ -66,6 +66,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, | |||||||
|   const foodName = useFoodName(food || undefined, usePluralFood); |   const foodName = useFoodName(food || undefined, usePluralFood); | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|  |     title: title ? sanitizeIngredientHTML(title) : undefined, | ||||||
|     quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined, |     quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined, | ||||||
|     unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined, |     unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined, | ||||||
|     name: foodName ? sanitizeIngredientHTML(foodName) : undefined, |     name: foodName ? sanitizeIngredientHTML(foodName) : undefined, | ||||||
|   | |||||||
| @@ -561,6 +561,7 @@ | |||||||
|     "see-original-text": "See Original Text", |     "see-original-text": "See Original Text", | ||||||
|     "original-text-with-value": "Original Text: {originalText}", |     "original-text-with-value": "Original Text: {originalText}", | ||||||
|     "ingredient-linker": "Ingredient Linker", |     "ingredient-linker": "Ingredient Linker", | ||||||
|  |     "unlinked": "Not linked yet", | ||||||
|     "linked-to-other-step": "Linked to other step", |     "linked-to-other-step": "Linked to other step", | ||||||
|     "auto": "Auto", |     "auto": "Auto", | ||||||
|     "cook-mode": "Cook Mode", |     "cook-mode": "Cook Mode", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user