mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
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:
parent
89982f3e5f
commit
ca26639525
@ -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,
|
||||||
|
@ -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: {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user