mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	fix: for several Shopping List bugs (#1912)
* prevent list refresh while re-ordering items * update position of new items to stay at the bottom * prevent refresh while loading * copy item while editing so it isn't refreshed * added loading count to handle overlapping actions * fixed recipe reference throttling * prevent merging checked and unchecked items
This commit is contained in:
		| @@ -6,7 +6,7 @@ | |||||||
|       hide-details |       hide-details | ||||||
|       dense |       dense | ||||||
|       :label="listItem.note" |       :label="listItem.note" | ||||||
|       @change="$emit('checked')" |       @change="$emit('checked', listItem)" | ||||||
|     > |     > | ||||||
|       <template #label> |       <template #label> | ||||||
|         <div :class="listItem.checked ? 'strike-through' : ''"> |         <div :class="listItem.checked ? 'strike-through' : ''"> | ||||||
| @@ -30,7 +30,7 @@ | |||||||
|           </v-list-item> |           </v-list-item> | ||||||
|         </v-list> |         </v-list> | ||||||
|       </v-menu> |       </v-menu> | ||||||
|       <v-btn small class="ml-2 mt-2 handle" icon @click="edit = true"> |       <v-btn small class="ml-2 mt-2 handle" icon @click="toggleEdit(true)"> | ||||||
|         <v-icon> |         <v-icon> | ||||||
|           {{ $globals.icons.edit }} |           {{ $globals.icons.edit }} | ||||||
|         </v-icon> |         </v-icon> | ||||||
| @@ -39,14 +39,14 @@ | |||||||
|   </div> |   </div> | ||||||
|   <div v-else class="mb-1 mt-6"> |   <div v-else class="mb-1 mt-6"> | ||||||
|     <ShoppingListItemEditor |     <ShoppingListItemEditor | ||||||
|       v-model="listItem" |       v-model="localListItem" | ||||||
|       :labels="labels" |       :labels="labels" | ||||||
|       :units="units" |       :units="units" | ||||||
|       :foods="foods" |       :foods="foods" | ||||||
|       @save="save" |       @save="save" | ||||||
|       @cancel="edit = !edit" |       @cancel="toggleEdit(false)" | ||||||
|       @delete="$emit('delete')" |       @delete="$emit('delete')" | ||||||
|       @toggle-foods="listItem.isFood = !listItem.isFood" |       @toggle-foods="localListItem.isFood = !localListItem.isFood" | ||||||
|     /> |     /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -104,24 +104,37 @@ export default defineComponent({ | |||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|  |     // copy prop value so a refresh doesn't interrupt the user | ||||||
|  |     const localListItem = ref(Object.assign({}, props.value)); | ||||||
|     const listItem = computed({ |     const listItem = computed({ | ||||||
|       get: () => { |       get: () => { | ||||||
|         return props.value; |         return props.value; | ||||||
|       }, |       }, | ||||||
|       set: (val) => { |       set: (val) => { | ||||||
|  |         // keep local copy in sync | ||||||
|  |         localListItem.value = val; | ||||||
|         context.emit("input", val); |         context.emit("input", val); | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     const edit = ref(false); |     const edit = ref(false); | ||||||
|  |     function toggleEdit(val = !edit.value) { | ||||||
|  |       if (val) { | ||||||
|  |         // update local copy of item with the current value | ||||||
|  |         localListItem.value = props.value; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       edit.value = val; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     function contextHandler(event: string) { |     function contextHandler(event: string) { | ||||||
|       if (event === "edit") { |       if (event === "edit") { | ||||||
|         edit.value = true; |         toggleEdit(true); | ||||||
|       } else { |       } else { | ||||||
|         context.emit(event); |         context.emit(event); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     function save() { |     function save() { | ||||||
|       context.emit("save"); |       context.emit("save", localListItem.value); | ||||||
|       edit.value = false; |       edit.value = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -139,7 +152,7 @@ export default defineComponent({ | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Get's the label for the shopping list item. Either the label assign to the item |      * Gets the label for the shopping list item. Either the label assign to the item | ||||||
|      * or the label of the food applied. |      * or the label of the food applied. | ||||||
|      */ |      */ | ||||||
|     const label = computed<MultiPurposeLabelSummary | undefined>(() => { |     const label = computed<MultiPurposeLabelSummary | undefined>(() => { | ||||||
| @@ -164,7 +177,9 @@ export default defineComponent({ | |||||||
|       edit, |       edit, | ||||||
|       contextMenu, |       contextMenu, | ||||||
|       listItem, |       listItem, | ||||||
|  |       localListItem, | ||||||
|       label, |       label, | ||||||
|  |       toggleEdit, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ | |||||||
|     <!-- Viewer --> |     <!-- Viewer --> | ||||||
|     <section v-if="!edit" class="py-2"> |     <section v-if="!edit" class="py-2"> | ||||||
|       <div v-if="!byLabel"> |       <div v-if="!byLabel"> | ||||||
|         <draggable :value="shoppingList.listItems" handle=".handle" @input="updateIndex"> |         <draggable :value="shoppingList.listItems" handle=".handle" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndex"> | ||||||
|           <v-lazy v-for="(item, index) in listItems.unchecked" :key="item.id"> |           <v-lazy v-for="(item, index) in listItems.unchecked" :key="item.id"> | ||||||
|             <ShoppingListItem |             <ShoppingListItem | ||||||
|               v-model="listItems.unchecked[index]" |               v-model="listItems.unchecked[index]" | ||||||
| @@ -18,8 +18,8 @@ | |||||||
|               :labels="allLabels || []" |               :labels="allLabels || []" | ||||||
|               :units="allUnits || []" |               :units="allUnits || []" | ||||||
|               :foods="allFoods || []" |               :foods="allFoods || []" | ||||||
|               @checked="saveListItem(item)" |               @checked="saveListItem" | ||||||
|               @save="saveListItem(item)" |               @save="saveListItem" | ||||||
|               @delete="deleteListItem(item)" |               @delete="deleteListItem(item)" | ||||||
|             /> |             /> | ||||||
|           </v-lazy> |           </v-lazy> | ||||||
| @@ -43,8 +43,8 @@ | |||||||
|               :labels="allLabels || []" |               :labels="allLabels || []" | ||||||
|               :units="allUnits || []" |               :units="allUnits || []" | ||||||
|               :foods="allFoods || []" |               :foods="allFoods || []" | ||||||
|               @checked="saveListItem(item)" |               @checked="saveListItem" | ||||||
|               @save="saveListItem(item)" |               @save="saveListItem" | ||||||
|               @delete="deleteListItem(item)" |               @delete="deleteListItem(item)" | ||||||
|             /> |             /> | ||||||
|           </v-lazy> |           </v-lazy> | ||||||
| @@ -134,8 +134,8 @@ | |||||||
|                 :labels="allLabels" |                 :labels="allLabels" | ||||||
|                 :units="allUnits || []" |                 :units="allUnits || []" | ||||||
|                 :foods="allFoods || []" |                 :foods="allFoods || []" | ||||||
|                 @checked="saveListItem(item)" |                 @checked="saveListItem" | ||||||
|                 @save="saveListItem(item)" |                 @save="saveListItem" | ||||||
|                 @delete="deleteListItem(item)" |                 @delete="deleteListItem(item)" | ||||||
|               /> |               /> | ||||||
|             </div> |             </div> | ||||||
| @@ -215,7 +215,8 @@ export default defineComponent({ | |||||||
|   }, |   }, | ||||||
|   setup() { |   setup() { | ||||||
|     const { idle } = useIdle(5 * 60 * 1000) // 5 minutes |     const { idle } = useIdle(5 * 60 * 1000) // 5 minutes | ||||||
|     const loading = ref(true); |     const loadingCounter = ref(1); | ||||||
|  |     const recipeReferenceLoading = ref(false); | ||||||
|     const userApi = useUserApi(); |     const userApi = useUserApi(); | ||||||
|  |  | ||||||
|     const edit = ref(false); |     const edit = ref(false); | ||||||
| @@ -237,13 +238,20 @@ export default defineComponent({ | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function refresh() { |     async function refresh() { | ||||||
|       shoppingList.value = await fetchShoppingList(); |       loadingCounter.value += 1; | ||||||
|  |       const newListValue = await fetchShoppingList(); | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|  |       // only update the list with the new value if we're not loading, to prevent UI jitter | ||||||
|  |       if (!loadingCounter.value) { | ||||||
|  |         shoppingList.value = newListValue; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // constantly polls for changes |     // constantly polls for changes | ||||||
|     async function pollForChanges() { |     async function pollForChanges() { | ||||||
|       // pause polling if the user isn't active or we're busy |       // pause polling if the user isn't active or we're busy | ||||||
|       if (idle.value || loading.value) { |       if (idle.value || loadingCounter.value) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -270,7 +278,7 @@ export default defineComponent({ | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // start polling |     // start polling | ||||||
|     loading.value = false; |     loadingCounter.value -= 1; | ||||||
|     const pollFrequency = 5000; |     const pollFrequency = 5000; | ||||||
|  |  | ||||||
|     let attempts = 0; |     let attempts = 0; | ||||||
| @@ -340,11 +348,11 @@ export default defineComponent({ | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|       deleteListItems(checked); |       deleteListItems(checked); | ||||||
|  |  | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|       refresh(); |       refresh(); | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // ===================================== |     // ===================================== | ||||||
| @@ -458,33 +466,35 @@ export default defineComponent({ | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     async function addRecipeReferenceToList(recipeId: string) { |     async function addRecipeReferenceToList(recipeId: string) { | ||||||
|       if (!shoppingList.value || loading.value) { |       if (!shoppingList.value || recipeReferenceLoading.value) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|  |       recipeReferenceLoading.value = true; | ||||||
|       const { data } = await userApi.shopping.lists.addRecipe(shoppingList.value.id, recipeId); |       const { data } = await userApi.shopping.lists.addRecipe(shoppingList.value.id, recipeId); | ||||||
|  |       recipeReferenceLoading.value = false; | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function removeRecipeReferenceToList(recipeId: string) { |     async function removeRecipeReferenceToList(recipeId: string) { | ||||||
|       if (!shoppingList.value || loading.value) { |       if (!shoppingList.value || recipeReferenceLoading.value) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|  |       recipeReferenceLoading.value = true; | ||||||
|       const { data } = await userApi.shopping.lists.removeRecipe(shoppingList.value.id, recipeId); |       const { data } = await userApi.shopping.lists.removeRecipe(shoppingList.value.id, recipeId); | ||||||
|  |       recipeReferenceLoading.value = false; | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // ===================================== |     // ===================================== | ||||||
| @@ -500,7 +510,7 @@ export default defineComponent({ | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|       if (item.checked && shoppingList.value.listItems) { |       if (item.checked && shoppingList.value.listItems) { | ||||||
|         const lst = shoppingList.value.listItems.filter((itm) => itm.id !== item.id); |         const lst = shoppingList.value.listItems.filter((itm) => itm.id !== item.id); | ||||||
|         lst.push(item); |         lst.push(item); | ||||||
| @@ -508,12 +518,11 @@ export default defineComponent({ | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       const { data } = await userApi.shopping.items.updateOne(item.id, item); |       const { data } = await userApi.shopping.items.updateOne(item.id, item); | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function deleteListItem(item: ShoppingListItemOut) { |     async function deleteListItem(item: ShoppingListItemOut) { | ||||||
| @@ -521,14 +530,13 @@ export default defineComponent({ | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|       const { data } = await userApi.shopping.items.deleteOne(item.id); |       const { data } = await userApi.shopping.items.deleteOne(item.id); | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // ===================================== |     // ===================================== | ||||||
| @@ -556,16 +564,18 @@ export default defineComponent({ | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|  |  | ||||||
|  |       // make sure it's inserted into the end of the list, which may have been updated | ||||||
|  |       createListItemData.value.position = shoppingList.value?.listItems?.length || 1; | ||||||
|       const { data } = await userApi.shopping.items.createOne(createListItemData.value); |       const { data } = await userApi.shopping.items.createOne(createListItemData.value); | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         createListItemData.value = ingredientResetFactory(); |         createListItemData.value = ingredientResetFactory(); | ||||||
|         createEditorOpen.value = false; |         createEditorOpen.value = false; | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function updateIndex(data: ShoppingListItemOut[]) { |     function updateIndex(data: ShoppingListItemOut[]) { | ||||||
| @@ -581,14 +591,13 @@ export default defineComponent({ | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|       const { data } = await userApi.shopping.items.deleteMany(items); |       const { data } = await userApi.shopping.items.deleteMany(items); | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function updateListItems() { |     async function updateListItems() { | ||||||
| @@ -602,14 +611,13 @@ export default defineComponent({ | |||||||
|         return itm; |         return itm; | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       loading.value = true; |       loadingCounter.value += 1; | ||||||
|       const { data } = await userApi.shopping.items.updateMany(shoppingList.value.listItems); |       const { data } = await userApi.shopping.items.updateMany(shoppingList.value.listItems); | ||||||
|  |       loadingCounter.value -= 1; | ||||||
|  |  | ||||||
|       if (data) { |       if (data) { | ||||||
|         refresh(); |         refresh(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       loading.value = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
| @@ -629,6 +637,7 @@ export default defineComponent({ | |||||||
|       itemsByLabel, |       itemsByLabel, | ||||||
|       listItems, |       listItems, | ||||||
|       listRecipes, |       listRecipes, | ||||||
|  |       loadingCounter, | ||||||
|       presentLabels, |       presentLabels, | ||||||
|       removeRecipeReferenceToList, |       removeRecipeReferenceToList, | ||||||
|       saveListItem, |       saveListItem, | ||||||
|   | |||||||
| @@ -28,6 +28,10 @@ class ShoppingListService: | |||||||
|         can_merge checks if the two items can be merged together. |         can_merge checks if the two items can be merged together. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  |         # Check if items are both checked or both unchecked | ||||||
|  |         if item1.checked != item2.checked: | ||||||
|  |             return False | ||||||
|  |  | ||||||
|         # Check if foods are equal |         # Check if foods are equal | ||||||
|         foods_is_none = item1.food_id is None and item2.food_id is None |         foods_is_none = item1.food_id is None and item2.food_id is None | ||||||
|         foods_not_none = not foods_is_none |         foods_not_none = not foods_is_none | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user