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

View File

@ -9,6 +9,7 @@
:item-id.sync="listItem.foodId"
:label="$t('shopping-list.food')"
:icon="$globals.icons.foods"
@create="createAssignFood"
/>
<InputLabelType
v-model="listItem.unit"
@ -16,6 +17,7 @@
:item-id.sync="listItem.unitId"
:label="$t('general.units')"
:icon="$globals.icons.units"
@create="createAssignUnit"
/>
</div>
<div class="d-md-flex align-center" style="gap: 20px">
@ -28,7 +30,8 @@
@keypress="handleNoteKeyPress"
></v-textarea>
</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>
<InputQuantity v-model="listItem.quantity" />
</div>
@ -60,6 +63,17 @@
</v-card>
</v-menu>
</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>
<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 { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
export default defineComponent({
props: {
@ -121,6 +136,12 @@ export default defineComponent({
},
},
setup(props, context) {
const foodStore = useFoodStore();
const foodData = useFoodData();
const unitStore = useUnitStore();
const unitData = useUnitData();
const listItem = computed({
get: () => {
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 {
listItem,
createAssignFood,
createAssignUnit,
assignLabelToFood,
};
},
methods: {

View File

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

View File

@ -1,14 +1,27 @@
<template>
<v-autocomplete
ref="autocompleteRef"
v-model="itemVal"
v-bind="$attrs"
:search-input.sync="searchInput"
item-text="name"
return-object
:items="items"
:prepend-icon="icon || $globals.icons.tags"
auto-select-first
clearable
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>
<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
* 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 { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
@ -59,6 +72,8 @@ export default defineComponent({
},
},
setup(props, context) {
const autocompleteRef = ref<HTMLInputElement>();
const searchInput = ref("");
const itemIdVal = computed({
get: () => {
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 {
autocompleteRef,
itemVal,
itemIdVal,
searchInput,
emitCreate,
};
},
});

View File

@ -782,6 +782,7 @@
"food": "Food",
"note": "Note",
"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.",
"toggle-food": "Toggle Food",
"manage-labels": "Manage Labels",

View File

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