feat: Improve shopping list label sections (#6345)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
miah
2025-10-24 09:43:55 -05:00
committed by GitHub
parent a242f567ad
commit 201c63d1e4
4 changed files with 134 additions and 134 deletions

View File

@@ -0,0 +1,17 @@
<template>
<v-expansion-panels v-model="open">
<slot />
</v-expansion-panels>
</template>
<script setup lang="ts">
interface Props {
startOpen?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
startOpen: false,
});
const open = ref(props.startOpen ? [0] : []);
</script>

View File

@@ -1,5 +1,5 @@
import { useToggle } from "@vueuse/core"; import { useToggle } from "@vueuse/core";
import type { ShoppingListOut, ShoppingListItemOut } from "~/lib/api/types/household"; import type { ShoppingListOut } from "~/lib/api/types/household";
/** /**
* Composable for managing shopping list label state and operations * Composable for managing shopping list label state and operations
@@ -36,14 +36,24 @@ export function useShoppingListLabels(shoppingList: Ref<ShoppingListOut | null>)
); );
}); });
const labelColorByName = computed(() => {
const map: Record<string, string | undefined> = {};
shoppingList.value?.listItems?.forEach((item) => {
if (!item.label) return;
const labelName = item.label?.name || t("shopping-list.no-label");
map[labelName] = item.label.color;
});
return map;
});
watch(labelNames, initializeLabelOpenStates, { immediate: true }); watch(labelNames, initializeLabelOpenStates, { immediate: true });
function toggleShowLabel(key: string) { function toggleShowLabel(key: string) {
labelOpenState.value[key] = !labelOpenState.value[key]; labelOpenState.value[key] = !labelOpenState.value[key];
} }
function getLabelColor(item: ShoppingListItemOut | null) { function getLabelColor(label: string) {
return item?.label?.color; return labelColorByName.value[label];
} }
const presentLabels = computed(() => { const presentLabels = computed(() => {

View File

@@ -36,6 +36,34 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<!-- Reorder Labels -->
<BaseDialog
v-model="reorderLabelsDialog"
:icon="$globals.icons.tagArrowUp"
:title="$t('shopping-list.reorder-labels')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="saveLabelOrder"
@close="cancelLabelOrder"
>
<v-card height="fit-content" max-height="70vh" style="overflow-y: auto;">
<VueDraggable
v-if="localLabels"
v-model="localLabels"
handle=".handle"
:delay="250"
:delay-on-touch-only="true"
class="my-2"
@update:model-value="updateLabelOrder"
>
<div v-for="(labelSetting, index) in localLabels" :key="labelSetting.id">
<MultiPurposeLabelSection v-model="localLabels[index]" use-color />
</div>
</VueDraggable>
</v-card>
</BaseDialog>
<BasePageTitle divider> <BasePageTitle divider>
<template #header> <template #header>
<v-container class="px-0"> <v-container class="px-0">
@@ -127,10 +155,7 @@
/> />
<!-- Viewer --> <!-- Viewer -->
<section <section v-if="!edit" class="py-2 d-flex flex-column ga-4">
v-if="!edit"
class="py-2"
>
<!-- Create Item --> <!-- Create Item -->
<div v-if="createEditorOpen"> <div v-if="createEditorOpen">
<ShoppingListItemEditor <ShoppingListItemEditor
@@ -154,27 +179,15 @@
</BaseButton> </BaseButton>
</div> </div>
<div <BaseExpansionPanels v-for="(value, key) in itemsByLabel" :key="key" :v-model="0" start-open>
v-for="(value, key) in itemsByLabel" <v-expansion-panel class="shopping-list-section">
:key="key" <v-expansion-panel-title
class="pb-4" :color="getLabelColor(key)"
> class="body-1 font-weight-bold section-title"
<v-btn >
:color="getLabelColor(value[0]) ? getLabelColor(value[0]) : '#959595'" {{ key }}
:style="{ </v-expansion-panel-title>
'color': getTextColor(getLabelColor(value[0])), <v-expansion-panel-text eager>
'letter-spacing': 'normal',
}"
@click="toggleShowLabel(key.toString())"
>
<v-icon>
{{ labelOpenState[key] ? $globals.icons.chevronDown : $globals.icons.chevronRight }}
</v-icon>
{{ key }}
</v-btn>
<v-divider />
<v-expand-transition>
<div v-if="labelOpenState[key]">
<VueDraggable <VueDraggable
:model-value="value" :model-value="value"
handle=".handle" handle=".handle"
@@ -184,107 +197,53 @@
@end="loadingCounter -= 1" @end="loadingCounter -= 1"
@update:model-value="updateIndexUncheckedByLabel(key.toString(), $event)" @update:model-value="updateIndexUncheckedByLabel(key.toString(), $event)"
> >
<v-lazy <ShoppingListItem
v-for="(item, index) in value" v-for="(item, index) in value"
:key="item.id" :key="item.id"
class="ml-2 my-2" v-model="value[index]"
> class="ml-2 my-2 w-auto"
<ShoppingListItem :labels="allLabels || []"
v-model="value[index]" :units="allUnits || []"
:labels="allLabels || []" :foods="allFoods || []"
:units="allUnits || []" :recipes="recipeMap"
:foods="allFoods || []" @checked="saveListItem"
:recipes="recipeMap" @save="saveListItem"
@checked="saveListItem" @delete="deleteListItem(item)"
@save="saveListItem"
@delete="deleteListItem(item)"
/>
</v-lazy>
</VueDraggable>
</div>
</v-expand-transition>
</div>
<!-- Reorder Labels -->
<BaseDialog
v-model="reorderLabelsDialog"
:icon="$globals.icons.tagArrowUp"
:title="$t('shopping-list.reorder-labels')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="saveLabelOrder"
@close="cancelLabelOrder"
>
<v-card
height="fit-content"
max-height="70vh"
style="overflow-y: auto;"
>
<VueDraggable
v-if="localLabels"
v-model="localLabels"
handle=".handle"
:delay="250"
:delay-on-touch-only="true"
class="my-2"
@update:model-value="updateLabelOrder"
>
<div
v-for="(labelSetting, index) in localLabels"
:key="labelSetting.id"
>
<MultiPurposeLabelSection
v-model="localLabels[index]"
use-color
/> />
</div> </VueDraggable>
</VueDraggable> </v-expansion-panel-text>
</v-card> </v-expansion-panel>
</BaseDialog> </BaseExpansionPanels>
<!-- Checked Items --> <!-- Checked Items -->
<div <v-expansion-panels flat>
v-if="listItems.checked && listItems.checked.length > 0" <v-expansion-panel v-if="listItems.checked && listItems.checked.length > 0">
class="mt-6" <v-expansion-panel-title class="border-solid border-thin py-1">
> <div class="d-flex align-center flex-0-1-100">
<div class="d-flex"> <div class="flex-1-0">
<div class="flex-grow-1"> {{ $t('shopping-list.items-checked-count', listItems.checked ? listItems.checked.length : 0) }}
<button @click="toggleShowChecked()"> </div>
<span> <div class="justify-end">
<v-icon> <BaseButtonGroup
{{ showChecked ? $globals.icons.chevronDown : $globals.icons.chevronRight }} :buttons="[
</v-icon> {
</span> icon: $globals.icons.checkboxBlankOutline,
{{ $t('shopping-list.items-checked-count', listItems.checked ? listItems.checked.length : 0) }} text: $t('shopping-list.uncheck-all-items'),
</button> event: 'uncheck',
</div> },
<div class="justify-end mt-n2"> {
<BaseButtonGroup icon: $globals.icons.delete,
:buttons="[ text: $t('shopping-list.delete-checked'),
{ event: 'delete',
icon: $globals.icons.checkboxBlankOutline, },
text: $t('shopping-list.uncheck-all-items'), ]"
event: 'uncheck', @uncheck="openUncheckAll"
}, @delete="openDeleteChecked"
{ />
icon: $globals.icons.delete, </div>
text: $t('shopping-list.delete-checked'), </div>
event: 'delete', </v-expansion-panel-title>
}, <v-expansion-panel-text eager>
]" <div v-for="(item, idx) in listItems.checked" :key="item.id">
@uncheck="openUncheckAll"
@delete="openDeleteChecked"
/>
</div>
</div>
<v-divider class="my-4" />
<v-expand-transition>
<div v-if="showChecked">
<div
v-for="(item, idx) in listItems.checked"
:key="item.id"
>
<ShoppingListItem <ShoppingListItem
v-model="listItems.checked[idx]" v-model="listItems.checked[idx]"
class="strike-through-note" class="strike-through-note"
@@ -296,14 +255,15 @@
@delete="deleteListItem(item)" @delete="deleteListItem(item)"
/> />
</div> </div>
</div> </v-expansion-panel-text>
</v-expand-transition> </v-expansion-panel>
</div> </v-expansion-panels>
</section> </section>
<!-- Recipe References --> <!-- Recipe References -->
<v-lazy <v-lazy
v-if="shoppingList.recipeReferences && shoppingList.recipeReferences.length > 0" v-if="shoppingList.recipeReferences && shoppingList.recipeReferences.length > 0"
class="mt-6"
> >
<section> <section>
<div> <div>
@@ -316,7 +276,7 @@
? shoppingList.recipeReferences.length ? shoppingList.recipeReferences.length
: 0) }} : 0) }}
</div> </div>
<v-divider class="my-4" /> <v-divider />
<RecipeList <RecipeList
:recipes="recipeList" :recipes="recipeList"
show-description show-description
@@ -367,14 +327,14 @@
<script lang="ts"> <script lang="ts">
import { VueDraggable } from "vue-draggable-plus"; import { VueDraggable } from "vue-draggable-plus";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
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 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 { useShoppingListPreferences } from "~/composables/use-users/preferences";
import { getTextColor } from "~/composables/use-text-color";
import { useShoppingListPage } from "~/composables/shopping-list-page/use-shopping-list-page"; import { useShoppingListPage } from "~/composables/shopping-list-page/use-shopping-list-page";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
import { getTextColor } from "~/composables/use-text-color";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
export default defineNuxtComponent({ export default defineNuxtComponent({
components: { components: {
@@ -417,8 +377,19 @@ export default defineNuxtComponent({
}); });
</script> </script>
<style scoped> <style>
.number-input-container { .number-input-container {
max-width: 50px; max-width: 50px;
} }
.shopping-list-section {
.section-title {
font-size: 1rem;
min-height: 48px !important;
}
.v-expansion-panel-text__wrapper {
padding: 0;
}
}
</style> </style>

View File

@@ -13,6 +13,7 @@ import type BaseButtonGroup from "@/components/global/BaseButtonGroup.vue";
import type BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue"; import type BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
import type BaseDialog from "@/components/global/BaseDialog.vue"; import type BaseDialog from "@/components/global/BaseDialog.vue";
import type BaseDivider from "@/components/global/BaseDivider.vue"; import type BaseDivider from "@/components/global/BaseDivider.vue";
import type BaseExpansionPanels from "@/components/global/BaseExpansionPanels.vue";
import type BaseOverflowButton from "@/components/global/BaseOverflowButton.vue"; import type BaseOverflowButton from "@/components/global/BaseOverflowButton.vue";
import type BasePageTitle from "@/components/global/BasePageTitle.vue"; import type BasePageTitle from "@/components/global/BasePageTitle.vue";
import type BaseStatCard from "@/components/global/BaseStatCard.vue"; import type BaseStatCard from "@/components/global/BaseStatCard.vue";
@@ -54,6 +55,7 @@ declare module "vue" {
BaseCardSectionTitle: typeof BaseCardSectionTitle; BaseCardSectionTitle: typeof BaseCardSectionTitle;
BaseDialog: typeof BaseDialog; BaseDialog: typeof BaseDialog;
BaseDivider: typeof BaseDivider; BaseDivider: typeof BaseDivider;
BaseExpansionPanels: typeof BaseExpansionPanels;
BaseOverflowButton: typeof BaseOverflowButton; BaseOverflowButton: typeof BaseOverflowButton;
BasePageTitle: typeof BasePageTitle; BasePageTitle: typeof BasePageTitle;
BaseStatCard: typeof BaseStatCard; BaseStatCard: typeof BaseStatCard;