mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-23 15:31:37 -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:
parent
7d94209f3e
commit
856a009dd8
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user