mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat: Ingredient Parser Enhancements (#6228)
This commit is contained in:
		| @@ -31,7 +31,7 @@ | ||||
|           :placeholder="$t('recipe.quantity')" | ||||
|           @keypress="quantityFilter" | ||||
|         > | ||||
|           <template #prepend> | ||||
|           <template v-if="enableDragHandle" #prepend> | ||||
|             <v-icon | ||||
|               class="mr-n1 handle" | ||||
|             > | ||||
| @@ -178,6 +178,7 @@ | ||||
|         </div> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|     <slot name="before-divider" /> | ||||
|     <v-divider | ||||
|       v-if="!mdAndUp" | ||||
|       class="my-4" | ||||
| @@ -196,7 +197,7 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe"; | ||||
| // defineModel replaces modelValue prop | ||||
| const model = defineModel<RecipeIngredient>({ required: true }); | ||||
|  | ||||
| defineProps({ | ||||
| const props = defineProps({ | ||||
|   unitError: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
| @@ -217,6 +218,14 @@ defineProps({ | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   enableDragHandle: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   deleteDisabled: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| defineEmits([ | ||||
| @@ -270,8 +279,8 @@ const btns = computed(() => { | ||||
|     text: i18n.t("general.delete"), | ||||
|     event: "delete", | ||||
|     children: undefined, | ||||
|     disabled: props.deleteDisabled, | ||||
|   }); | ||||
|  | ||||
|   return out; | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -192,6 +192,7 @@ import { useUserApi } from "~/composables/api"; | ||||
| import { uuid4, deepCopy } from "~/composables/use-utils"; | ||||
| import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue"; | ||||
| import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue"; | ||||
| import { useLoggedInState } from "~/composables/use-logged-in-state"; | ||||
| import { useNavigationWarning } from "~/composables/use-navigation-warning"; | ||||
|  | ||||
| const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true }); | ||||
| @@ -200,6 +201,7 @@ const display = useDisplay(); | ||||
| const i18n = useI18n(); | ||||
| const $auth = useMealieAuth(); | ||||
| const route = useRoute(); | ||||
| const { isOwnGroup } = useLoggedInState(); | ||||
|  | ||||
| const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || ""); | ||||
|  | ||||
| @@ -258,11 +260,11 @@ const paramsEdit = useRouteQuery<BooleanString>("edit", ""); | ||||
| const paramsParse = useRouteQuery<BooleanString>("parse", ""); | ||||
|  | ||||
| onMounted(() => { | ||||
|   if (paramsEdit.value === "true") { | ||||
|   if (paramsEdit.value === "true" && isOwnGroup.value) { | ||||
|     setMode(PageMode.EDIT); | ||||
|   } | ||||
|  | ||||
|   if (paramsParse.value === "true") { | ||||
|   if (paramsParse.value === "true" && isOwnGroup.value) { | ||||
|     toggleIsParsing(true); | ||||
|   } | ||||
| }); | ||||
|   | ||||
| @@ -31,6 +31,7 @@ | ||||
|           v-for="(ingredient, index) in recipe.recipeIngredient" | ||||
|           :key="ingredient.referenceId" | ||||
|           v-model="recipe.recipeIngredient[index]" | ||||
|           enable-drag-handle | ||||
|           enable-context-menu | ||||
|           class="list-group-item" | ||||
|           @delete="recipe.recipeIngredient.splice(index, 1)" | ||||
|   | ||||
| @@ -6,6 +6,10 @@ | ||||
|     @update:model-value="emit('update:modelValue', $event)" | ||||
|   > | ||||
|     <v-container class="pa-2 ma-0" style="background-color: rgb(var(--v-theme-background));"> | ||||
|       <div v-if="state.loading.parser" class="my-6"> | ||||
|         <AppLoader waiting-text="" class="my-6" /> | ||||
|       </div> | ||||
|       <div v-else> | ||||
|         <BaseCardSectionTitle :title="$t('recipe.parser.ingredient-parser')"> | ||||
|           <div v-if="!state.allReviewed" class="mb-4"> | ||||
|             <p>{{ $t("recipe.parser.ingredient-parser-description") }}</p> | ||||
| @@ -34,8 +38,7 @@ | ||||
|             </div> | ||||
|           </div> | ||||
|         </BaseCardSectionTitle> | ||||
|       <AppLoader v-if="state.loading.parser" waiting-text="" class="my-6" /> | ||||
|       <v-card v-else-if="!state.allReviewed && currentIng"> | ||||
|         <v-card v-if="!state.allReviewed && currentIng"> | ||||
|           <v-card-text class="pb-0 mb-0"> | ||||
|             <div class="text-center px-8 py-4 mb-6"> | ||||
|               <p class="text-h5 font-italic"> | ||||
| @@ -99,13 +102,11 @@ | ||||
|             </v-card-actions> | ||||
|           </v-card-text> | ||||
|         </v-card> | ||||
|       <v-expansion-panels v-else> | ||||
|         <v-card-title>{{ $t("recipe.parser.parsing-completed") }}</v-card-title> | ||||
|         <v-expansion-panel> | ||||
|           <v-expansion-panel-title> | ||||
|         <div v-else> | ||||
|           <v-card-title class="text-center pt-0 pb-8"> | ||||
|             {{ $t("recipe.parser.review-parsed-ingredients") }} | ||||
|           </v-expansion-panel-title> | ||||
|           <v-expansion-panel-text> | ||||
|           </v-card-title> | ||||
|           <v-card-text style="max-height: 60vh; overflow-y: auto;"> | ||||
|             <VueDraggable | ||||
|               v-model="parsedIngs" | ||||
|               handle=".handle" | ||||
| @@ -117,48 +118,66 @@ | ||||
|                 disabled: false, | ||||
|                 ghostClass: 'ghost', | ||||
|               }" | ||||
|               class="px-6" | ||||
|               @start="drag = true" | ||||
|               @end="drag = false" | ||||
|             > | ||||
|               <TransitionGroup | ||||
|                 type="transition" | ||||
|               > | ||||
|                 <div v-for="(ingredient, index) in parsedIngs" :key="index"> | ||||
|                 <v-lazy v-for="(ingredient, index) in parsedIngs" :key="index"> | ||||
|                   <RecipeIngredientEditor | ||||
|                     v-model="ingredient.ingredient" | ||||
|                     enable-drag-handle | ||||
|                     enable-context-menu | ||||
|                     class="list-group-item" | ||||
|                     class="list-group-item pb-8" | ||||
|                     :delete-disabled="parsedIngs.length <= 1" | ||||
|                     @delete="parsedIngs.splice(index, 1)" | ||||
|                     @insert-above="insertNewIngredient(index)" | ||||
|                     @insert-below="insertNewIngredient(index + 1)" | ||||
|                   /> | ||||
|                   <p class="pt-0 pb-4 my-0 text-caption"> | ||||
|                   > | ||||
|                     <template #before-divider> | ||||
|                       <p v-if="ingredient.input" class="py-0 my-0 text-caption"> | ||||
|                         {{ $t("recipe.original-text-with-value", { originalText: ingredient.input }) }} | ||||
|                       </p> | ||||
|                 </div> | ||||
|                     </template> | ||||
|                   </RecipeIngredientEditor> | ||||
|                 </v-lazy> | ||||
|               </TransitionGroup> | ||||
|             </VueDraggable> | ||||
|           </v-expansion-panel-text> | ||||
|         </v-expansion-panel> | ||||
|       </v-expansion-panels> | ||||
|           </v-card-text> | ||||
|         </div> | ||||
|       </div> | ||||
|     </v-container> | ||||
|     <template v-if="!state.loading.parser" #custom-card-action> | ||||
|       <BaseButton | ||||
|         v-if="!state.allReviewed" | ||||
|         color="info" | ||||
|         :icon="$globals.icons.arrowRightBold" | ||||
|         icon-right | ||||
|         :text="$t('general.next')" | ||||
|         @click="nextIngredient" | ||||
|       <!-- Parse --> | ||||
|       <div v-if="!state.allReviewed" class="d-flex justify-space-between align-center"> | ||||
|         <v-checkbox | ||||
|           v-model="currentIngShouldDelete" | ||||
|           color="error" | ||||
|           hide-details | ||||
|           density="compact" | ||||
|           :label="i18n.t('recipe.parser.delete-item')" | ||||
|           class="mr-4" | ||||
|         /> | ||||
|         <BaseButton | ||||
|         v-else | ||||
|           :color="currentIngShouldDelete ? 'error' : 'info'" | ||||
|           :icon="currentIngShouldDelete ? $globals.icons.delete : $globals.icons.arrowRightBold" | ||||
|           :icon-right="!currentIngShouldDelete" | ||||
|           :text="$t(currentIngShouldDelete ? 'recipe.parser.delete-item' : 'general.next')" | ||||
|           @click="nextIngredient" | ||||
|         /> | ||||
|        </div> | ||||
|       <!-- Review --> | ||||
|        <div v-else> | ||||
|         <BaseButton | ||||
|           create | ||||
|           :text="$t('general.save')" | ||||
|           :icon="$globals.icons.save" | ||||
|           :loading="state.loading.save" | ||||
|           @click="saveIngs" | ||||
|         /> | ||||
|        </div> | ||||
|     </template> | ||||
|   </BaseDialog> | ||||
| </template> | ||||
| @@ -226,6 +245,7 @@ const currentIng = ref<ParsedIngredient | null>(null); | ||||
| const currentMissingUnit = ref(""); | ||||
| const currentMissingFood = ref(""); | ||||
| const currentIngHasError = computed(() => currentMissingUnit.value || currentMissingFood.value); | ||||
| const currentIngShouldDelete = ref(false); | ||||
|  | ||||
| const state = reactive({ | ||||
|   currentParsedIndex: -1, | ||||
| @@ -297,6 +317,11 @@ function checkFood(ing: ParsedIngredient) { | ||||
| } | ||||
|  | ||||
| function nextIngredient() { | ||||
|   if (currentIngShouldDelete.value) { | ||||
|     parsedIngs.value.splice(state.currentParsedIndex, 1); | ||||
|     currentIngShouldDelete.value = false; | ||||
|   } | ||||
|  | ||||
|   let nextIndex = state.currentParsedIndex + 1; | ||||
|  | ||||
|   while (nextIndex < parsedIngs.value.length) { | ||||
| @@ -304,6 +329,7 @@ function nextIngredient() { | ||||
|     if (shouldReview(current)) { | ||||
|       state.currentParsedIndex = nextIndex; | ||||
|       currentIng.value = current; | ||||
|       currentIngShouldDelete.value = false; | ||||
|       checkUnit(current); | ||||
|       checkFood(current); | ||||
|       return; | ||||
| @@ -462,6 +488,16 @@ watch(parser, () => { | ||||
|   parseIngredients(); | ||||
| }); | ||||
|  | ||||
| watch([parsedIngs, () => state.allReviewed], () => { | ||||
|   if (!state.allReviewed) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (!parsedIngs.value.length) { | ||||
|     insertNewIngredient(0); | ||||
|   } | ||||
| }, { immediate: true, deep: true }); | ||||
|  | ||||
| function asPercentage(num: number | undefined): string { | ||||
|   if (!num) { | ||||
|     return "0%"; | ||||
|   | ||||
| @@ -671,12 +671,12 @@ | ||||
|       "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", | ||||
|       "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", | ||||
|       "no-food": "No Food", | ||||
|       "parsing-completed": "Parsing Completed", | ||||
|       "review-parsed-ingredients": "Review parsed ingredients", | ||||
|       "confidence-score": "Confidence Score", | ||||
|       "ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.", | ||||
|       "ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.", | ||||
|       "add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}" | ||||
|       "add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}", | ||||
|       "delete-item": "Delete Item" | ||||
|     }, | ||||
|     "reset-servings-count": "Reset Servings Count", | ||||
|     "not-linked-ingredients": "Additional Ingredients", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user