1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-28 09:39:41 +02:00

feat: Shopping list UI overhaul - three dot menu (#4415)

Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Cody 2025-01-26 08:04:40 -06:00 committed by GitHub
parent 1e69577d12
commit 93c2df41c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 183 additions and 169 deletions

View file

@ -17,11 +17,69 @@
<v-container> <v-container>
<v-row> <v-row>
<v-col cols="3" class="text-left"> <v-col cols="3" class="text-left">
<ButtonLink :to="`/shopping-lists?disableRedirect=true`" :text="$tc('general.back')" :icon="$globals.icons.backArrow" /> <ButtonLink :to="`/shopping-lists?disableRedirect=true`" :text="$tc('shopping-list.all-lists')" :icon="$globals.icons.backArrow" />
</v-col> </v-col>
<v-col cols="6" class="d-flex justify-center"> <v-col cols="6" class="d-none d-lg-flex justify-center">
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img> <v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img>
</v-col> </v-col>
<v-col class="d-flex justify-end">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.contentCopy,
text: '',
event: 'edit',
children: [
{
icon: $globals.icons.contentCopy,
text: $tc('shopping-list.copy-as-text'),
event: 'copy-plain',
},
{
icon: $globals.icons.contentCopy,
text: $tc('shopping-list.copy-as-markdown'),
event: 'copy-markdown',
},
],
},
{
icon: $globals.icons.checkboxOutline,
text: $tc('shopping-list.check-all-items'),
event: 'check',
},
{
icon: $globals.icons.dotsVertical,
text: '',
event: 'three-dot',
children: [
{
icon: $globals.icons.tags,
text: $tc('shopping-list.toggle-label-sort'),
event: 'sort-by-labels',
},
{
icon: $globals.icons.tags,
text: $tc('shopping-list.reorder-labels'),
event: 'reorder-labels',
},
{
icon: $globals.icons.tags,
text: $tc('shopping-list.manage-labels'),
event: 'manage-labels',
},
],
},
]"
@edit="edit = true"
@three-dot="threeDot = true"
@check="openCheckAll"
@sort-by-labels="sortByLabels"
@copy-plain="copyListItems('plain')"
@copy-markdown="copyListItems('markdown')"
@reorder-labels="toggleReorderLabelsDialog()"
@manage-labels="$router.push(`/group/data/labels`)"
/>
</v-col>
</v-row> </v-row>
</v-container> </v-container>
</template> </template>
@ -119,27 +177,6 @@
</v-card> </v-card>
</BaseDialog> </BaseDialog>
<!-- Settings -->
<BaseDialog
v-model="settingsDialog"
:icon="$globals.icons.cog"
:title="$t('general.settings')"
@confirm="updateSettings"
>
<v-container>
<v-form>
<v-select
v-model="currentUserId"
:items="allUsers"
item-text="fullName"
item-value="id"
:label="$t('general.owner')"
:prepend-icon="$globals.icons.user"
/>
</v-form>
</v-container>
</BaseDialog>
<!-- Create Item --> <!-- Create Item -->
<div v-if="createEditorOpen"> <div v-if="createEditorOpen">
<ShoppingListItemEditor <ShoppingListItemEditor
@ -154,78 +191,41 @@
/> />
</div> </div>
<div v-else class="mt-4 d-flex justify-end"> <div v-else class="mt-4 d-flex justify-end">
<BaseButton
v-if="preferences.viewByLabel" edit class="mr-2"
:disabled="$nuxt.isOffline"
@click="toggleReorderLabelsDialog">
<template #icon> {{ $globals.icons.tags }} </template>
{{ $t('shopping-list.reorder-labels') }}
</BaseButton>
<BaseButton create @click="createEditorOpen = true" > {{ $t('general.add') }} </BaseButton> <BaseButton create @click="createEditorOpen = true" > {{ $t('general.add') }} </BaseButton>
</div> </div>
<!-- Action Bar -->
<div class="d-flex justify-end mb-4 mt-2">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.contentCopy,
text: '',
event: 'edit',
children: [
{
icon: $globals.icons.contentCopy,
text: $tc('shopping-list.copy-as-text'),
event: 'copy-plain',
},
{
icon: $globals.icons.contentCopy,
text: $tc('shopping-list.copy-as-markdown'),
event: 'copy-markdown',
},
],
},
{
icon: $globals.icons.delete,
text: $tc('shopping-list.delete-checked'),
event: 'delete',
},
{
icon: $globals.icons.tags,
text: $tc('shopping-list.toggle-label-sort'),
event: 'sort-by-labels',
},
{
icon: $globals.icons.checkboxBlankOutline,
text: $tc('shopping-list.uncheck-all-items'),
event: 'uncheck',
},
{
icon: $globals.icons.checkboxOutline,
text: $tc('shopping-list.check-all-items'),
event: 'check',
},
]"
@edit="edit = true"
@delete="openDeleteChecked"
@uncheck="openUncheckAll"
@check="openCheckAll"
@sort-by-labels="sortByLabels"
@copy-plain="copyListItems('plain')"
@copy-markdown="copyListItems('markdown')"
/>
</div>
<!-- Checked Items --> <!-- Checked Items -->
<div v-if="listItems.checked && listItems.checked.length > 0" class="mt-6"> <div v-if="listItems.checked && listItems.checked.length > 0" class="mt-6">
<button @click="toggleShowChecked()"> <div class="d-flex">
<span> <div class="flex-grow-1">
<v-icon> <button @click="toggleShowChecked()">
{{ showChecked ? $globals.icons.chevronDown : $globals.icons.chevronRight }} <span>
</v-icon> <v-icon>
</span> {{ showChecked ? $globals.icons.chevronDown : $globals.icons.chevronRight }}
{{ $tc('shopping-list.items-checked-count', listItems.checked ? listItems.checked.length : 0) }} </v-icon>
</button> </span>
{{ $tc('shopping-list.items-checked-count', listItems.checked ? listItems.checked.length : 0) }}
</button>
</div>
<div class="justify-end mt-n2">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.checkboxBlankOutline,
text: $tc('shopping-list.uncheck-all-items'),
event: 'uncheck',
},
{
icon: $globals.icons.delete,
text: $tc('shopping-list.delete-checked'),
event: 'delete',
},
]"
@uncheck="openUncheckAll"
@delete="openDeleteChecked"
/>
</div>
</div>
<v-divider class="my-4"></v-divider> <v-divider class="my-4"></v-divider>
<v-expand-transition> <v-expand-transition>
<div v-show="showChecked"> <div v-show="showChecked">
@ -277,29 +277,6 @@
</RecipeList> </RecipeList>
</section> </section>
</v-lazy> </v-lazy>
<v-lazy>
<div class="d-flex justify-end">
<BaseButton
edit
:disabled="$nuxt.isOffline"
@click="toggleSettingsDialog"
>
<template #icon> {{ $globals.icons.cog }} </template>
{{ $t('general.settings') }}
</BaseButton>
</div>
</v-lazy>
<v-lazy>
<div v-if="$nuxt.isOnline" class="d-flex justify-end mt-10">
<ButtonLink
:to="`/group/data/labels`"
:text="$tc('shopping-list.manage-labels')"
:icon="$globals.icons.tags"
/>
</div>
</v-lazy>
<WakelockSwitch/> <WakelockSwitch/>
</v-container> </v-container>
</template> </template>
@ -314,7 +291,6 @@ import { useUserApi } from "~/composables/api";
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue" import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue"
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue"; import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
import { ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household"; import { ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
import { UserOut } from "~/lib/api/types/user";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue"; import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue"; import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store"; import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
@ -349,8 +325,8 @@ export default defineComponent({
const userApi = useUserApi(); const userApi = useUserApi();
const edit = ref(false); const edit = ref(false);
const threeDot = ref(false);
const reorderLabelsDialog = ref(false); const reorderLabelsDialog = ref(false);
const settingsDialog = ref(false);
const preserveItemOrder = ref(false); const preserveItemOrder = ref(false);
const route = useRoute(); const route = useRoute();
@ -678,13 +654,6 @@ export default defineComponent({
localLabels.value = shoppingList.value?.labelSettings localLabels.value = shoppingList.value?.labelSettings
} }
async function toggleSettingsDialog() {
if (!settingsDialog.value) {
await fetchAllUsers();
}
settingsDialog.value = !settingsDialog.value;
}
function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) { function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
if (!shoppingList.value) { if (!shoppingList.value) {
return; return;
@ -1064,39 +1033,6 @@ export default defineComponent({
refresh(); refresh();
} }
// ===============================================================
// Shopping List Settings
const allUsers = ref<UserOut[]>([]);
const currentUserId = ref<string | undefined>();
async function fetchAllUsers() {
const { data } = await userApi.households.fetchMembers();
if (!data) {
return;
}
// update current user
allUsers.value = data.items.sort((a, b) => ((a.fullName || "") < (b.fullName || "") ? -1 : 1));
currentUserId.value = shoppingList.value?.userId;
}
async function updateSettings() {
if (!shoppingList.value || !currentUserId.value) {
return;
}
loadingCounter.value += 1;
const { data } = await userApi.shopping.lists.updateOne(
shoppingList.value.id,
{...shoppingList.value, userId: currentUserId.value},
);
loadingCounter.value -= 1;
if (data) {
refresh();
}
}
return { return {
...toRefs(state), ...toRefs(state),
addRecipeReferenceToList, addRecipeReferenceToList,
@ -1112,6 +1048,7 @@ export default defineComponent({
openDeleteChecked, openDeleteChecked,
deleteListItem, deleteListItem,
edit, edit,
threeDot,
getLabelColor, getLabelColor,
groupSlug, groupSlug,
itemsByLabel, itemsByLabel,
@ -1123,8 +1060,6 @@ export default defineComponent({
removeRecipeReferenceToList, removeRecipeReferenceToList,
reorderLabelsDialog, reorderLabelsDialog,
toggleReorderLabelsDialog, toggleReorderLabelsDialog,
settingsDialog,
toggleSettingsDialog,
localLabels, localLabels,
updateLabelOrder, updateLabelOrder,
cancelLabelOrder, cancelLabelOrder,
@ -1144,9 +1079,6 @@ export default defineComponent({
updateIndexUncheckedByLabel, updateIndexUncheckedByLabel,
allUnits, allUnits,
allFoods, allFoods,
allUsers,
currentUserId,
updateSettings,
getTextColor, getTextColor,
}; };
}, },

View file

@ -6,6 +6,27 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Settings -->
<BaseDialog
v-model="ownerDialog"
:icon="$globals.icons.admin"
:title="$t('user.edit-user')"
@confirm="updateOwner"
>
<v-container>
<v-form>
<v-select
v-model="updateUserId"
:items="allUsers"
item-text="fullName"
item-value="id"
:label="$t('general.owner')"
:prepend-icon="$globals.icons.user"
/>
</v-form>
</v-container>
</BaseDialog>
<BaseDialog v-model="deleteDialog" :title="$tc('general.confirm')" color="error" @confirm="deleteOne"> <BaseDialog v-model="deleteDialog" :title="$tc('general.confirm')" color="error" @confirm="deleteOne">
<v-card-text>{{ $t('shopping-list.are-you-sure-you-want-to-delete-this-item') }}</v-card-text> <v-card-text>{{ $t('shopping-list.are-you-sure-you-want-to-delete-this-item') }}</v-card-text>
</BaseDialog> </BaseDialog>
@ -38,26 +59,34 @@
<v-icon left> <v-icon left>
{{ $globals.icons.cartCheck }} {{ $globals.icons.cartCheck }}
</v-icon> </v-icon>
{{ list.name }} <div class="flex-grow-1">
<v-btn class="ml-auto" icon @click.prevent="openDelete(list.id)"> {{ list.name }}
<v-icon> </div>
{{ $globals.icons.delete }} <div class="d-flex justify-end">
</v-icon> <v-btn icon @click.prevent="toggleOwnerDialog(list)">
</v-btn> <v-icon>
{{ $globals.icons.user }}
</v-icon>
</v-btn>
<v-btn icon @click.prevent="openDelete(list.id)">
<v-icon>
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
</div>
</v-card-title> </v-card-title>
</v-card> </v-card>
</section> </section>
<div class="d-flex justify-end mt-10">
<ButtonLink :to="`/group/data/labels`" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
</div>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useAsync, useContext, reactive, ref, toRefs, useRoute, useRouter, watch } from "@nuxtjs/composition-api"; import { computed, defineComponent, useAsync, useContext, reactive, ref, toRefs, useRoute, useRouter, watch } from "@nuxtjs/composition-api";
import { ShoppingListOut } from "~/lib/api/types/household";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
import { useShoppingListPreferences } from "~/composables/use-users/preferences"; import { useShoppingListPreferences } from "~/composables/use-users/preferences";
import { UserOut } from "~/lib/api/types/user";
export default defineComponent({ export default defineComponent({
middleware: "auth", middleware: "auth",
@ -77,6 +106,8 @@ export default defineComponent({
createDialog: false, createDialog: false,
deleteDialog: false, deleteDialog: false,
deleteTarget: "", deleteTarget: "",
ownerDialog: false,
ownerTarget: ref<ShoppingListOut | null>(null),
}); });
const shoppingLists = useAsync(async () => { const shoppingLists = useAsync(async () => {
@ -136,6 +167,53 @@ export default defineComponent({
} }
} }
async function toggleOwnerDialog(list: ShoppingListOut) {
if (!state.ownerDialog) {
state.ownerTarget = list;
await fetchAllUsers();
}
state.ownerDialog = !state.ownerDialog;
}
// ===============================================================
// Shopping List Edit User/Owner
const allUsers = ref<UserOut[]>([]);
const updateUserId = ref<string | undefined>();
async function fetchAllUsers() {
const { data } = await userApi.households.fetchMembers();
if (!data) {
return;
}
// update current user
allUsers.value = data.items.sort((a, b) => ((a.fullName || "") < (b.fullName || "") ? -1 : 1));
updateUserId.value = state.ownerTarget?.userId;
}
async function updateOwner() {
if (!state.ownerTarget || !updateUserId.value) {
return;
}
// user has not changed, so we should not update
if (state.ownerTarget.userId === updateUserId.value) {
return;
}
// get full list, so the move does not delete shopping list items
const { data: fullList } = await userApi.shopping.lists.getOne(state.ownerTarget.id);
if (!fullList) {
return;
}
const { data } = await userApi.shopping.lists.updateOne(
state.ownerTarget.id,
{...fullList, userId: updateUserId.value},
);
if (data) {
refresh();
}
}
function openDelete(id: string) { function openDelete(id: string) {
state.deleteDialog = true; state.deleteDialog = true;
state.deleteTarget = id; state.deleteTarget = id;
@ -155,6 +233,10 @@ export default defineComponent({
preferences, preferences,
shoppingListChoices, shoppingListChoices,
createOne, createOne,
toggleOwnerDialog,
allUsers,
updateUserId,
updateOwner,
deleteOne, deleteOne,
openDelete, openDelete,
}; };