mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-24 23:59:45 +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:
parent
1e69577d12
commit
93c2df41c3
2 changed files with 183 additions and 169 deletions
|
@ -17,11 +17,69 @@
|
|||
<v-container>
|
||||
<v-row>
|
||||
<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 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-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-container>
|
||||
</template>
|
||||
|
@ -119,27 +177,6 @@
|
|||
</v-card>
|
||||
</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 -->
|
||||
<div v-if="createEditorOpen">
|
||||
<ShoppingListItemEditor
|
||||
|
@ -154,78 +191,41 @@
|
|||
/>
|
||||
</div>
|
||||
<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>
|
||||
</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 -->
|
||||
<div v-if="listItems.checked && listItems.checked.length > 0" class="mt-6">
|
||||
<button @click="toggleShowChecked()">
|
||||
<span>
|
||||
<v-icon>
|
||||
{{ showChecked ? $globals.icons.chevronDown : $globals.icons.chevronRight }}
|
||||
</v-icon>
|
||||
</span>
|
||||
{{ $tc('shopping-list.items-checked-count', listItems.checked ? listItems.checked.length : 0) }}
|
||||
</button>
|
||||
<div class="d-flex">
|
||||
<div class="flex-grow-1">
|
||||
<button @click="toggleShowChecked()">
|
||||
<span>
|
||||
<v-icon>
|
||||
{{ showChecked ? $globals.icons.chevronDown : $globals.icons.chevronRight }}
|
||||
</v-icon>
|
||||
</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-expand-transition>
|
||||
<div v-show="showChecked">
|
||||
|
@ -277,29 +277,6 @@
|
|||
</RecipeList>
|
||||
</section>
|
||||
</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/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
@ -314,7 +291,6 @@ import { useUserApi } from "~/composables/api";
|
|||
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue"
|
||||
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
|
||||
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 ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
|
||||
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
|
||||
|
@ -349,8 +325,8 @@ export default defineComponent({
|
|||
const userApi = useUserApi();
|
||||
|
||||
const edit = ref(false);
|
||||
const threeDot = ref(false);
|
||||
const reorderLabelsDialog = ref(false);
|
||||
const settingsDialog = ref(false);
|
||||
const preserveItemOrder = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
|
@ -678,13 +654,6 @@ export default defineComponent({
|
|||
localLabels.value = shoppingList.value?.labelSettings
|
||||
}
|
||||
|
||||
async function toggleSettingsDialog() {
|
||||
if (!settingsDialog.value) {
|
||||
await fetchAllUsers();
|
||||
}
|
||||
settingsDialog.value = !settingsDialog.value;
|
||||
}
|
||||
|
||||
function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
|
||||
if (!shoppingList.value) {
|
||||
return;
|
||||
|
@ -1064,39 +1033,6 @@ export default defineComponent({
|
|||
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 {
|
||||
...toRefs(state),
|
||||
addRecipeReferenceToList,
|
||||
|
@ -1112,6 +1048,7 @@ export default defineComponent({
|
|||
openDeleteChecked,
|
||||
deleteListItem,
|
||||
edit,
|
||||
threeDot,
|
||||
getLabelColor,
|
||||
groupSlug,
|
||||
itemsByLabel,
|
||||
|
@ -1123,8 +1060,6 @@ export default defineComponent({
|
|||
removeRecipeReferenceToList,
|
||||
reorderLabelsDialog,
|
||||
toggleReorderLabelsDialog,
|
||||
settingsDialog,
|
||||
toggleSettingsDialog,
|
||||
localLabels,
|
||||
updateLabelOrder,
|
||||
cancelLabelOrder,
|
||||
|
@ -1144,9 +1079,6 @@ export default defineComponent({
|
|||
updateIndexUncheckedByLabel,
|
||||
allUnits,
|
||||
allFoods,
|
||||
allUsers,
|
||||
currentUserId,
|
||||
updateSettings,
|
||||
getTextColor,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -6,6 +6,27 @@
|
|||
</v-card-text>
|
||||
</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">
|
||||
<v-card-text>{{ $t('shopping-list.are-you-sure-you-want-to-delete-this-item') }}</v-card-text>
|
||||
</BaseDialog>
|
||||
|
@ -38,26 +59,34 @@
|
|||
<v-icon left>
|
||||
{{ $globals.icons.cartCheck }}
|
||||
</v-icon>
|
||||
{{ list.name }}
|
||||
<v-btn class="ml-auto" icon @click.prevent="openDelete(list.id)">
|
||||
<v-icon>
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<div class="flex-grow-1">
|
||||
{{ list.name }}
|
||||
</div>
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn icon @click.prevent="toggleOwnerDialog(list)">
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
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 { useAsyncKey } from "~/composables/use-utils";
|
||||
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export default defineComponent({
|
||||
middleware: "auth",
|
||||
|
@ -77,6 +106,8 @@ export default defineComponent({
|
|||
createDialog: false,
|
||||
deleteDialog: false,
|
||||
deleteTarget: "",
|
||||
ownerDialog: false,
|
||||
ownerTarget: ref<ShoppingListOut | null>(null),
|
||||
});
|
||||
|
||||
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) {
|
||||
state.deleteDialog = true;
|
||||
state.deleteTarget = id;
|
||||
|
@ -155,6 +233,10 @@ export default defineComponent({
|
|||
preferences,
|
||||
shoppingListChoices,
|
||||
createOne,
|
||||
toggleOwnerDialog,
|
||||
allUsers,
|
||||
updateUserId,
|
||||
updateOwner,
|
||||
deleteOne,
|
||||
openDelete,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue