feat: Data Management from Shopping List (#3603)

Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
This commit is contained in:
Michael Genson 2024-05-22 16:58:16 -05:00 committed by GitHub
parent 89982f3e5f
commit ca26639525
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 140 additions and 35 deletions

View File

@ -29,6 +29,7 @@
</v-col> </v-col>
<v-col v-if="!disableAmount" sm="12" md="3" cols="12"> <v-col v-if="!disableAmount" sm="12" md="3" cols="12">
<v-autocomplete <v-autocomplete
ref="unitAutocomplete"
v-model="value.unit" v-model="value.unit"
:search-input.sync="unitSearch" :search-input.sync="unitSearch"
auto-select-first auto-select-first
@ -57,6 +58,7 @@
<!-- Foods Input --> <!-- Foods Input -->
<v-col v-if="!disableAmount" m="12" md="3" cols="12" class=""> <v-col v-if="!disableAmount" m="12" md="3" cols="12" class="">
<v-autocomplete <v-autocomplete
ref="foodAutocomplete"
v-model="value.food" v-model="value.food"
:search-input.sync="foodSearch" :search-input.sync="foodSearch"
auto-select-first auto-select-first
@ -200,11 +202,13 @@ export default defineComponent({
const foodStore = useFoodStore(); const foodStore = useFoodStore();
const foodData = useFoodData(); const foodData = useFoodData();
const foodSearch = ref(""); const foodSearch = ref("");
const foodAutocomplete = ref<HTMLInputElement>();
async function createAssignFood() { async function createAssignFood() {
foodData.data.name = foodSearch.value; foodData.data.name = foodSearch.value;
props.value.food = await foodStore.actions.createOne(foodData.data) || undefined; props.value.food = await foodStore.actions.createOne(foodData.data) || undefined;
foodData.reset(); foodData.reset();
foodAutocomplete.value?.blur();
} }
// ================================================== // ==================================================
@ -212,11 +216,13 @@ export default defineComponent({
const unitStore = useUnitStore(); const unitStore = useUnitStore();
const unitsData = useUnitData(); const unitsData = useUnitData();
const unitSearch = ref(""); const unitSearch = ref("");
const unitAutocomplete = ref<HTMLInputElement>();
async function createAssignUnit() { async function createAssignUnit() {
unitsData.data.name = unitSearch.value; unitsData.data.name = unitSearch.value;
props.value.unit = await unitStore.actions.createOne(unitsData.data) || undefined; props.value.unit = await unitStore.actions.createOne(unitsData.data) || undefined;
unitsData.reset(); unitsData.reset();
unitAutocomplete.value?.blur();
} }
const state = reactive({ const state = reactive({
@ -269,7 +275,9 @@ export default defineComponent({
contextMenuOptions, contextMenuOptions,
handleUnitEnter, handleUnitEnter,
handleFoodEnter, handleFoodEnter,
foodAutocomplete,
createAssignFood, createAssignFood,
unitAutocomplete,
createAssignUnit, createAssignUnit,
foods: foodStore.foods, foods: foodStore.foods,
foodSearch, foodSearch,

View File

@ -9,6 +9,7 @@
:item-id.sync="listItem.foodId" :item-id.sync="listItem.foodId"
:label="$t('shopping-list.food')" :label="$t('shopping-list.food')"
:icon="$globals.icons.foods" :icon="$globals.icons.foods"
@create="createAssignFood"
/> />
<InputLabelType <InputLabelType
v-model="listItem.unit" v-model="listItem.unit"
@ -16,6 +17,7 @@
:item-id.sync="listItem.unitId" :item-id.sync="listItem.unitId"
:label="$t('general.units')" :label="$t('general.units')"
:icon="$globals.icons.units" :icon="$globals.icons.units"
@create="createAssignUnit"
/> />
</div> </div>
<div class="d-md-flex align-center" style="gap: 20px"> <div class="d-md-flex align-center" style="gap: 20px">
@ -28,7 +30,8 @@
@keypress="handleNoteKeyPress" @keypress="handleNoteKeyPress"
></v-textarea> ></v-textarea>
</div> </div>
<div class="d-flex align-end" style="gap: 20px"> <div class="d-flex flex-wrap align-end" style="gap: 20px">
<div class="d-flex align-end">
<div> <div>
<InputQuantity v-model="listItem.quantity" /> <InputQuantity v-model="listItem.quantity" />
</div> </div>
@ -60,6 +63,17 @@
</v-card> </v-card>
</v-menu> </v-menu>
</div> </div>
<BaseButton
v-if="listItem.labelId && listItem.food && listItem.labelId !== listItem.food.labelId"
small
color="info"
:icon="$globals.icons.tagArrowRight"
:text="$tc('shopping-list.save-label')"
class="mt-2 align-items-flex-start"
@click="assignLabelToFood"
/>
<v-spacer />
</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
<v-card-actions class="ma-0 pt-0 pb-1 justify-end"> <v-card-actions class="ma-0 pt-0 pb-1 justify-end">
@ -100,6 +114,7 @@ import { defineComponent, computed, watch } from "@nuxtjs/composition-api";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/group"; import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/group";
import { MultiPurposeLabelOut } from "~/lib/api/types/labels"; import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe"; import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -121,6 +136,12 @@ export default defineComponent({
}, },
}, },
setup(props, context) { setup(props, context) {
const foodStore = useFoodStore();
const foodData = useFoodData();
const unitStore = useUnitStore();
const unitData = useUnitData();
const listItem = computed({ const listItem = computed({
get: () => { get: () => {
return props.value; return props.value;
@ -139,8 +160,47 @@ export default defineComponent({
} }
); );
async function createAssignFood(val: string) {
// keep UI reactive
listItem.value.food ? listItem.value.food.name = val : listItem.value.food = { name: val };
foodData.data.name = val;
const newFood = await foodStore.actions.createOne(foodData.data);
if (newFood) {
listItem.value.food = newFood;
listItem.value.foodId = newFood.id;
}
foodData.reset();
}
async function createAssignUnit(val: string) {
// keep UI reactive
listItem.value.unit ? listItem.value.unit.name = val : listItem.value.unit = { name: val };
unitData.data.name = val;
const newUnit = await unitStore.actions.createOne(unitData.data);
if (newUnit) {
listItem.value.unit = newUnit;
listItem.value.unitId = newUnit.id;
}
unitData.reset();
}
async function assignLabelToFood() {
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
return;
}
listItem.value.food.labelId = listItem.value.labelId;
// @ts-ignore the food will have an id, even though TS says it might not
await foodStore.actions.updateOne(listItem.value.food);
}
return { return {
listItem, listItem,
createAssignFood,
createAssignUnit,
assignLabelToFood,
}; };
}, },
methods: { methods: {

View File

@ -14,15 +14,15 @@
> >
<v-icon v-if="!iconRight" left> <v-icon v-if="!iconRight" left>
<slot name="icon"> <slot name="icon">
{{ btnAttrs.icon }} {{ icon || btnAttrs.icon }}
</slot> </slot>
</v-icon> </v-icon>
<slot name="default"> <slot name="default">
{{ btnAttrs.text }} {{ text || btnAttrs.text }}
</slot> </slot>
<v-icon v-if="iconRight" right> <v-icon v-if="iconRight" right>
<slot name="icon"> <slot name="icon">
{{ btnAttrs.icon }} {{ icon || btnAttrs.icon }}
</slot> </slot>
</v-icon> </v-icon>
</v-btn> </v-btn>
@ -103,6 +103,14 @@ export default defineComponent({
type: String, type: String,
default: null, default: null,
}, },
text: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
iconRight: { iconRight: {
type: Boolean, type: Boolean,
default: false, default: false,

View File

@ -1,14 +1,27 @@
<template> <template>
<v-autocomplete <v-autocomplete
ref="autocompleteRef"
v-model="itemVal" v-model="itemVal"
v-bind="$attrs" v-bind="$attrs"
:search-input.sync="searchInput"
item-text="name" item-text="name"
return-object return-object
:items="items" :items="items"
:prepend-icon="icon || $globals.icons.tags" :prepend-icon="icon || $globals.icons.tags"
auto-select-first
clearable clearable
hide-details hide-details
/> @keyup.enter="emitCreate"
>
<template v-if="$listeners.create" #no-data>
<div class="caption text-center pb-2">{{ $t("recipe.press-enter-to-create") }}</div>
</template>
<template v-if="$listeners.create" #append-item>
<div class="px-2">
<BaseButton block small @click="emitCreate"></BaseButton>
</div>
</template>
</v-autocomplete>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -31,7 +44,7 @@
* Both the ID and Item can be synced. The item can be synced using the v-model syntax and the itemId can be synced * Both the ID and Item can be synced. The item can be synced using the v-model syntax and the itemId can be synced
* using the .sync syntax `item-id.sync="item.labelId"` * using the .sync syntax `item-id.sync="item.labelId"`
*/ */
import { defineComponent, computed } from "@nuxtjs/composition-api"; import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
import { MultiPurposeLabelSummary } from "~/lib/api/types/labels"; import { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe"; import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
@ -59,6 +72,8 @@ export default defineComponent({
}, },
}, },
setup(props, context) { setup(props, context) {
const autocompleteRef = ref<HTMLInputElement>();
const searchInput = ref("");
const itemIdVal = computed({ const itemIdVal = computed({
get: () => { get: () => {
return props.itemId || undefined; return props.itemId || undefined;
@ -78,9 +93,20 @@ export default defineComponent({
}, },
}); });
function emitCreate() {
if (props.items.some(item => item.name === searchInput.value)) {
return;
}
context.emit("create", searchInput.value);
autocompleteRef.value?.blur();
}
return { return {
autocompleteRef,
itemVal, itemVal,
itemIdVal, itemIdVal,
searchInput,
emitCreate,
}; };
}, },
}); });

View File

@ -782,6 +782,7 @@
"food": "Food", "food": "Food",
"note": "Note", "note": "Note",
"label": "Label", "label": "Label",
"save-label": "Save Label",
"linked-item-warning": "This item is linked to one or more recipe. Adjusting the units or foods will yield unexpected results when adding or removing the recipe from this list.", "linked-item-warning": "This item is linked to one or more recipe. Adjusting the units or foods will yield unexpected results when adding or removing the recipe from this list.",
"toggle-food": "Toggle Food", "toggle-food": "Toggle Food",
"manage-labels": "Manage Labels", "manage-labels": "Manage Labels",

View File

@ -9,6 +9,7 @@ import {
mdiSquareEditOutline, mdiSquareEditOutline,
mdiClose, mdiClose,
mdiTagArrowUpOutline, mdiTagArrowUpOutline,
mdiTagArrowRight,
mdiTagMultipleOutline, mdiTagMultipleOutline,
mdiShapeOutline, mdiShapeOutline,
mdiBookOutline, mdiBookOutline,
@ -293,6 +294,7 @@ export const icons = {
// Organization // Organization
tags: mdiTagMultipleOutline, tags: mdiTagMultipleOutline,
tagArrowUp: mdiTagArrowUpOutline, tagArrowUp: mdiTagArrowUpOutline,
tagArrowRight: mdiTagArrowRight,
categories: mdiShapeOutline, categories: mdiShapeOutline,
pages: mdiBookOutline, pages: mdiBookOutline,
book: mdiBookOpenPageVariant, book: mdiBookOpenPageVariant,