1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-24 15:49:42 +02:00

Feature: Shopping List Label Section Improvements (#2090)

* added backend for shopping list label config

* updated codegen

* refactored shopping list ops to service
removed unique contraint
removed label settings from main route/schema
added new route for label settings

* codegen

* made sure label settings output in position order

* implemented submenu for label order drag and drop

* removed redundant label and tweaked formatting

* added view by label to user preferences

* made items draggable within each label section

* moved reorder labels to its own button

* made dialog scrollable

* fixed broken model

* refactored labels to use a service
moved shopping list label logic to service
modified label seeder to use service

* added tests

* fix for first label missing the tag icon

* fixed wrong mapped type

* added statement to create existing relationships

* fix restore test, maybe
This commit is contained in:
Michael Genson 2023-02-21 21:58:41 -06:00 committed by GitHub
parent e14851531d
commit a6c46a7420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 715 additions and 61 deletions

View file

@ -9,12 +9,13 @@
<!-- Viewer -->
<section v-if="!edit" class="py-2">
<div v-if="!byLabel">
<div v-if="!preferences.viewByLabel">
<draggable :value="listItems.unchecked" handle=".handle" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndexUnchecked">
<v-lazy v-for="(item, index) in listItems.unchecked" :key="item.id">
<v-lazy v-for="(item, index) in listItems.unchecked" :key="item.id" class="my-2">
<ShoppingListItem
v-model="listItems.unchecked[index]"
class="my-2 my-sm-0"
:show-label=true
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
@ -28,29 +29,43 @@
<!-- View By Label -->
<div v-else>
<div v-for="(value, key) in itemsByLabel" :key="key" class="mb-6">
<div v-for="(value, key, idx) in itemsByLabel" :key="key" class="mb-6">
<div @click="toggleShowChecked()">
<span>
<span v-if="idx || key !== $tc('shopping-list.no-label')">
<v-icon>
{{ $globals.icons.tags }}
</v-icon>
</span>
{{ key }}
</div>
<v-lazy v-for="(item, index) in value" :key="item.id">
<ShoppingListItem
v-model="value[index]"
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
@checked="saveListItem"
@save="saveListItem"
@delete="deleteListItem(item)"
/>
</v-lazy>
<draggable :value="value" handle=".handle" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndexUncheckedByLabel(key, $event)">
<v-lazy v-for="(item, index) in value" :key="item.id" class="ml-2 my-2">
<ShoppingListItem
v-model="value[index]"
:show-label=false
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
@checked="saveListItem"
@save="saveListItem"
@delete="deleteListItem(item)"
/>
</v-lazy>
</draggable>
</div>
</div>
<!-- Reorder Labels -->
<BaseDialog v-model="reorderLabelsDialog" :icon="$globals.icons.tagArrowUp" :title="$t('shopping-list.reorder-labels')">
<v-card height="fit-content" max-height="70vh" style="overflow-y: auto;">
<draggable :value="shoppingList.labelSettings" handle=".handle" class="my-2" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateLabelOrder">
<div v-for="(labelSetting, index) in shoppingList.labelSettings" :key="labelSetting.id">
<MultiPurposeLabelSection v-model="shoppingList.labelSettings[index]" />
</div>
</draggable>
</v-card>
</BaseDialog>
<!-- Create Item -->
<div v-if="createEditorOpen">
<ShoppingListItemEditor
@ -65,6 +80,10 @@
/>
</div>
<div v-else class="mt-4 d-flex justify-end">
<BaseButton v-if="preferences.viewByLabel" color="info" class="mr-2" @click="reorderLabelsDialog = true">
<template #icon> {{ $globals.icons.tags }} </template>
{{ $t('shopping-list.reorder-labels') }}
</BaseButton>
<BaseButton create @click="createEditorOpen = true" />
</div>
@ -192,11 +211,13 @@ import { useIdle, useToggle } from "@vueuse/core";
import { useCopyList } from "~/composables/use-copy";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue"
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/group";
import { ShoppingListItemCreate, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/group";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
type CopyTypes = "plain" | "markdown";
@ -208,18 +229,21 @@ interface PresentLabel {
export default defineComponent({
components: {
draggable,
MultiPurposeLabelSection,
ShoppingListItem,
RecipeList,
ShoppingListItemEditor,
},
setup() {
const preferences = useShoppingListPreferences();
const { idle } = useIdle(5 * 60 * 1000) // 5 minutes
const loadingCounter = ref(1);
const recipeReferenceLoading = ref(false);
const userApi = useUserApi();
const edit = ref(false);
const byLabel = ref(false);
const reorderLabelsDialog = ref(false);
const route = useRoute();
const id = route.value.params.id;
@ -395,7 +419,33 @@ export default defineComponent({
const { foods: allFoods } = useFoodStore();
function sortByLabels() {
byLabel.value = !byLabel.value;
preferences.value.viewByLabel = !preferences.value.viewByLabel;
}
function toggleReorderLabelsDialog() {
reorderLabelsDialog.value = !reorderLabelsDialog.value
}
async function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
if (!shoppingList.value) {
return;
}
labelSettings.forEach((labelSetting, index) => {
labelSetting.position = index;
return labelSetting;
});
// setting this doesn't have any effect on the data since it's refreshed automatically, but it makes the ux feel smoother
shoppingList.value.labelSettings = labelSettings;
loadingCounter.value += 1;
const { data } = await userApi.shopping.lists.updateLabelSettings(shoppingList.value.id, labelSettings);
loadingCounter.value -= 1;
if (data) {
refresh();
}
}
const presentLabels = computed(() => {
@ -442,7 +492,25 @@ export default defineComponent({
items[noLabelText] = noLabel;
}
itemsByLabel.value = items;
// sort the map by label order
const orderedLabelNames = shoppingList.value?.labelSettings?.map((labelSetting) => { return labelSetting.label.name; })
if (!orderedLabelNames) {
itemsByLabel.value = items;
return;
}
const itemsSorted: { [prop: string]: ShoppingListItemOut[] } = {};
if (noLabelText in items) {
itemsSorted[noLabelText] = items[noLabelText];
}
orderedLabelNames.forEach(labelName => {
if (labelName in items) {
itemsSorted[labelName] = items[labelName];
}
});
itemsByLabel.value = itemsSorted;
}
watch(shoppingList, () => {
@ -588,6 +656,24 @@ export default defineComponent({
updateListItems();
}
function updateIndexUncheckedByLabel(labelName: string, labeledUncheckedItems: ShoppingListItemOut[]) {
if (!itemsByLabel.value[labelName]) {
return;
}
// update this label's item order
itemsByLabel.value[labelName] = labeledUncheckedItems;
// reset list order of all items
const allUncheckedItems: ShoppingListItemOut[] = [];
for (labelName in itemsByLabel.value) {
allUncheckedItems.push(...itemsByLabel.value[labelName]);
}
// save changes
return updateIndexUnchecked(allUncheckedItems);
}
async function deleteListItems(items: ShoppingListItemOut[]) {
if (!shoppingList.value) {
return;
@ -626,7 +712,6 @@ export default defineComponent({
addRecipeReferenceToList,
updateListItems,
allLabels,
byLabel,
contextMenu,
contextMenuAction,
copyListItems,
@ -640,8 +725,12 @@ export default defineComponent({
listItems,
listRecipes,
loadingCounter,
preferences,
presentLabels,
removeRecipeReferenceToList,
reorderLabelsDialog,
toggleReorderLabelsDialog,
updateLabelOrder,
saveListItem,
shoppingList,
showChecked,
@ -649,6 +738,7 @@ export default defineComponent({
toggleShowChecked,
uncheckAll,
updateIndexUnchecked,
updateIndexUncheckedByLabel,
allUnits,
allFoods,
};