Skip to content

Commit

Permalink
feat: Data Management from Shopping List (#3603)
Browse files Browse the repository at this point in the history
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
  • Loading branch information
michael-genson and boc-the-git committed May 22, 2024
1 parent 89982f3 commit ca26639
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 35 deletions.
8 changes: 8 additions & 0 deletions frontend/components/Domain/Recipe/RecipeIngredientEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -200,23 +202,27 @@ 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();
}
// ==================================================
// Units
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({
Expand Down Expand Up @@ -269,7 +275,9 @@ export default defineComponent({
contextMenuOptions,
handleUnitEnter,
handleFoodEnter,
foodAutocomplete,
createAssignFood,
unitAutocomplete,
createAssignUnit,
foods: foodStore.foods,
foodSearch,
Expand Down
120 changes: 90 additions & 30 deletions frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
:item-id.sync="listItem.foodId"
:label="$t('shopping-list.food')"
:icon="$globals.icons.foods"
@create="createAssignFood"
/>
<InputLabelType
v-model="listItem.unit"
:items="units"
: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">
Expand All @@ -28,37 +30,49 @@
@keypress="handleNoteKeyPress"
></v-textarea>
</div>
<div class="d-flex align-end" style="gap: 20px">
<div>
<InputQuantity v-model="listItem.quantity" />
</div>
<div style="max-width: 300px" class="mt-3 mr-auto">
<InputLabelType
v-model="listItem.label"
:items="labels"
:item-id.sync="listItem.labelId"
:label="$t('shopping-list.label')"
/>
</div>
<div class="d-flex flex-wrap align-end" style="gap: 20px">
<div class="d-flex align-end">
<div>
<InputQuantity v-model="listItem.quantity" />
</div>
<div style="max-width: 300px" class="mt-3 mr-auto">
<InputLabelType
v-model="listItem.label"
:items="labels"
:item-id.sync="listItem.labelId"
:label="$t('shopping-list.label')"
/>
</div>

<v-menu
v-if="listItem.recipeReferences && listItem.recipeReferences.length > 0"
open-on-hover
offset-y
left
top
>
<template #activator="{ on, attrs }">
<v-icon class="mt-auto" icon v-bind="attrs" color="warning" v-on="on">
{{ $globals.icons.alert }}
</v-icon>
</template>
<v-card max-width="350px" class="left-warning-border">
<v-card-text>
{{ $t("shopping-list.linked-item-warning") }}
</v-card-text>
</v-card>
</v-menu>
<v-menu
v-if="listItem.recipeReferences && listItem.recipeReferences.length > 0"
open-on-hover
offset-y
left
top
>
<template #activator="{ on, attrs }">
<v-icon class="mt-auto" icon v-bind="attrs" color="warning" v-on="on">
{{ $globals.icons.alert }}
</v-icon>
</template>
<v-card max-width="350px" class="left-warning-border">
<v-card-text>
{{ $t("shopping-list.linked-item-warning") }}
</v-card-text>
</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>
Expand Down Expand Up @@ -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: {
Expand All @@ -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;
Expand All @@ -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: {
Expand Down
14 changes: 11 additions & 3 deletions frontend/components/global/BaseButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 28 additions & 2 deletions frontend/components/global/InputLabelType.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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,
};
},
});
Expand Down
1 change: 1 addition & 0 deletions frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions frontend/lib/icons/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
mdiSquareEditOutline,
mdiClose,
mdiTagArrowUpOutline,
mdiTagArrowRight,
mdiTagMultipleOutline,
mdiShapeOutline,
mdiBookOutline,
Expand Down Expand Up @@ -293,6 +294,7 @@ export const icons = {
// Organization
tags: mdiTagMultipleOutline,
tagArrowUp: mdiTagArrowUpOutline,
tagArrowRight: mdiTagArrowRight,
categories: mdiShapeOutline,
pages: mdiBookOutline,
book: mdiBookOpenPageVariant,
Expand Down

0 comments on commit ca26639

Please sign in to comment.