mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 05:25:26 +02:00
feat: Migrate to Nuxt 3 framework (#5184)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
parent
89ab7fac25
commit
c24d532608
403 changed files with 23959 additions and 19557 deletions
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<v-chip v-bind="$attrs" label :color="label.color || undefined" :text-color="textColor">
|
||||
<v-chip
|
||||
v-bind="$attrs"
|
||||
label
|
||||
variant="flat"
|
||||
:color="label.color || undefined"
|
||||
:text-color="textColor"
|
||||
>
|
||||
<span style="max-width: 100%; overflow: hidden; text-overflow: ellipsis;">
|
||||
{{ label.name }}
|
||||
</span>
|
||||
|
@ -7,12 +13,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { getTextColor } from "~/composables/use-text-color";
|
||||
import { MultiPurposeLabelSummary } from "~/lib/api/types/recipe";
|
||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/recipe";
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
label: {
|
||||
type: Object as () => MultiPurposeLabelSummary,
|
||||
|
|
|
@ -6,12 +6,24 @@
|
|||
{{ $globals.icons.tags }}
|
||||
</v-icon>
|
||||
</span>
|
||||
{{ value.label.name }}
|
||||
{{ modelValue.label.name }}
|
||||
</div>
|
||||
<div style="min-width: 72px" class="ml-auto text-right">
|
||||
<v-menu offset-x left min-width="125px">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn small class="ml-2 handle" icon v-bind="attrs" v-on="on">
|
||||
<div
|
||||
style="min-width: 72px"
|
||||
class="ml-auto text-right"
|
||||
>
|
||||
<v-menu
|
||||
offset-x
|
||||
start
|
||||
min-width="125px"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
size="small"
|
||||
class="ml-2 handle"
|
||||
icon
|
||||
v-bind="props"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
|
@ -23,22 +35,21 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/household";
|
||||
import type { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/household";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object as () => ShoppingListMultiPurposeLabelOut,
|
||||
required: true,
|
||||
},
|
||||
useColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const labelColor = ref<string | undefined>(props.useColor ? props.value.label.color : undefined);
|
||||
const labelColor = ref<string | undefined>(props.useColor ? props.modelValue.label.color : undefined);
|
||||
|
||||
function contextHandler(event: string) {
|
||||
context.emit(event);
|
||||
|
|
|
@ -1,41 +1,74 @@
|
|||
<template>
|
||||
<v-container v-if="!edit" class="pa-0">
|
||||
<v-row no-gutters class="flex-nowrap align-center">
|
||||
<v-container
|
||||
v-if="!edit"
|
||||
class="pa-0"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
class="flex-nowrap align-center"
|
||||
>
|
||||
<v-col :cols="itemLabelCols">
|
||||
<v-checkbox
|
||||
v-model="listItem.checked"
|
||||
class="mt-0"
|
||||
color="null"
|
||||
hide-details
|
||||
dense
|
||||
:label="listItem.note"
|
||||
density="compact"
|
||||
:label="listItem.note!"
|
||||
@change="$emit('checked', listItem)"
|
||||
>
|
||||
<template #label>
|
||||
<div :class="listItem.checked ? 'strike-through' : ''">
|
||||
<RecipeIngredientListItem :ingredient="listItem" :disable-amount="!(listItem.isFood || listItem.quantity !== 1)" />
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="listItem"
|
||||
:disable-amount="!(listItem.isFood || listItem.quantity !== 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col v-if="label && showLabel" cols="3" class="text-right">
|
||||
<MultiPurposeLabel :label="label" small />
|
||||
<v-col
|
||||
v-if="label && showLabel"
|
||||
cols="3"
|
||||
class="text-right"
|
||||
>
|
||||
<MultiPurposeLabel
|
||||
:label="label"
|
||||
size="small"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto" class="text-right">
|
||||
<div v-if="!listItem.checked" style="min-width: 72px">
|
||||
<v-menu offset-x left min-width="125px">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-col
|
||||
cols="auto"
|
||||
class="text-right"
|
||||
>
|
||||
<div
|
||||
v-if="!listItem.checked"
|
||||
style="min-width: 72px"
|
||||
>
|
||||
<v-menu
|
||||
offset-x
|
||||
start
|
||||
min-width="125px"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-tooltip
|
||||
v-if="recipeList && recipeList.length"
|
||||
open-delay="200"
|
||||
transition="slide-x-reverse-transition"
|
||||
dense
|
||||
density="compact"
|
||||
right
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ on: onBtn, attrs: attrsBtn }">
|
||||
<v-btn small class="ml-2" icon v-bind="attrsBtn" v-on="onBtn" @click="displayRecipeRefs = !displayRecipeRefs">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
v-bind="tooltipProps"
|
||||
@click="displayRecipeRefs = !displayRecipeRefs"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.potSteam }}
|
||||
</v-icon>
|
||||
|
@ -44,43 +77,91 @@
|
|||
<span>Toggle Recipes</span>
|
||||
</v-tooltip>
|
||||
<!-- Dummy button so the spacing is consistent when labels are enabled -->
|
||||
<v-btn v-else small class="ml-2" icon disabled>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
disabled
|
||||
/>
|
||||
|
||||
<v-btn small class="ml-2 handle" icon v-bind="attrs" v-on="on">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2 handle"
|
||||
icon
|
||||
v-bind="props"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small class="ml-2" icon @click="toggleEdit(true)">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
@click="toggleEdit(true)"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="action in contextMenu" :key="action.event" dense @click="contextHandler(action.event)">
|
||||
<v-list-item-title>{{ action.text }}</v-list-item-title>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="action in contextMenu"
|
||||
:key="action.event"
|
||||
density="compact"
|
||||
@click="contextHandler(action.event)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ action.text }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!listItem.checked && recipeList && recipeList.length && displayRecipeRefs" no-gutters class="mb-2">
|
||||
<v-col cols="auto" style="width: 100%;">
|
||||
<RecipeList :recipes="recipeList" :list-item="listItem" :disabled="$nuxt.isOffline" small tile />
|
||||
<v-row
|
||||
v-if="!listItem.checked && recipeList && recipeList.length && displayRecipeRefs"
|
||||
no-gutters
|
||||
class="mb-2"
|
||||
>
|
||||
<v-col
|
||||
cols="auto"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<RecipeList
|
||||
:recipes="recipeList"
|
||||
:list-item="listItem"
|
||||
:disabled="isOffline"
|
||||
size="small"
|
||||
tile
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="listItem.checked" no-gutters class="mb-2">
|
||||
<v-row
|
||||
v-if="listItem.checked"
|
||||
no-gutters
|
||||
class="mb-2"
|
||||
>
|
||||
<v-col cols="auto">
|
||||
<div class="text-caption font-weight-light font-italic">
|
||||
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updatedAt || "").toLocaleDateString($i18n.locale)}) }}
|
||||
{{ $t("shopping-list.completed-on", {
|
||||
date: new Date(listItem.updatedAt
|
||||
|| "").toLocaleDateString($i18n.locale) })
|
||||
}}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<div v-else class="mb-1 mt-6">
|
||||
<div
|
||||
v-else
|
||||
class="mb-1 mt-6"
|
||||
>
|
||||
<ShoppingListItemEditor
|
||||
v-model="localListItem"
|
||||
:labels="labels"
|
||||
|
@ -95,13 +176,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { useOnline } from "@vueuse/core";
|
||||
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
|
||||
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
|
||||
import MultiPurposeLabel from "./MultiPurposeLabel.vue";
|
||||
import { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
||||
|
||||
interface actions {
|
||||
|
@ -109,10 +190,10 @@ interface actions {
|
|||
event: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: { ShoppingListItemEditor, MultiPurposeLabel, RecipeList, RecipeIngredientListItem },
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object as () => ShoppingListItemOut,
|
||||
required: true,
|
||||
},
|
||||
|
@ -137,10 +218,12 @@ export default defineComponent({
|
|||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ["checked", "update:modelValue", "save", "delete"],
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const displayRecipeRefs = ref(false);
|
||||
const itemLabelCols = ref<string>(props.value.checked ? "auto" : props.showLabel ? "4" : "6");
|
||||
const itemLabelCols = ref<string>(props.modelValue.checked ? "auto" : props.showLabel ? "4" : "6");
|
||||
const isOffline = computed(() => useOnline().value === false);
|
||||
|
||||
const contextMenu: actions[] = [
|
||||
{
|
||||
|
@ -154,15 +237,15 @@ export default defineComponent({
|
|||
];
|
||||
|
||||
// copy prop value so a refresh doesn't interrupt the user
|
||||
const localListItem = ref(Object.assign({}, props.value));
|
||||
const localListItem = ref(Object.assign({}, props.modelValue));
|
||||
const listItem = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
// keep local copy in sync
|
||||
localListItem.value = val;
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
const edit = ref(false);
|
||||
|
@ -173,7 +256,7 @@ export default defineComponent({
|
|||
|
||||
if (val) {
|
||||
// update local copy of item with the current value
|
||||
localListItem.value = props.value;
|
||||
localListItem.value = props.modelValue;
|
||||
}
|
||||
|
||||
edit.value = val;
|
||||
|
@ -182,7 +265,8 @@ export default defineComponent({
|
|||
function contextHandler(event: string) {
|
||||
if (event === "edit") {
|
||||
toggleEdit(true);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
context.emit(event);
|
||||
}
|
||||
}
|
||||
|
@ -205,9 +289,7 @@ export default defineComponent({
|
|||
* or the label of the food applied.
|
||||
*/
|
||||
const label = computed<MultiPurposeLabelSummary | undefined>(() => {
|
||||
// @ts-ignore - it _might_ exists
|
||||
if (listItem.value.label) {
|
||||
// @ts-ignore - it _might_ exists
|
||||
return listItem.value.label as MultiPurposeLabelSummary;
|
||||
}
|
||||
|
||||
|
@ -225,7 +307,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
listItem.value.recipeReferences.forEach((ref) => {
|
||||
const recipe = props.recipes.get(ref.recipeId)
|
||||
const recipe = props.recipes.get(ref.recipeId);
|
||||
if (recipe) {
|
||||
recipeList.push(recipe);
|
||||
}
|
||||
|
@ -247,6 +329,7 @@ export default defineComponent({
|
|||
label,
|
||||
recipeList,
|
||||
toggleEdit,
|
||||
isOffline,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card outlined>
|
||||
<v-card variant="outlined">
|
||||
<v-card-text class="pb-3 pt-1">
|
||||
<div v-if="listItem.isFood" class="d-md-flex align-center mb-2" style="gap: 20px">
|
||||
<div>
|
||||
|
@ -8,26 +8,27 @@
|
|||
</div>
|
||||
<InputLabelType
|
||||
v-model="listItem.unit"
|
||||
v-model:item-id="listItem.unitId!"
|
||||
:items="units"
|
||||
:item-id.sync="listItem.unitId"
|
||||
:label="$t('general.units')"
|
||||
:icon="$globals.icons.units"
|
||||
create
|
||||
@create="createAssignUnit"
|
||||
/>
|
||||
<InputLabelType
|
||||
v-model="listItem.food"
|
||||
v-model:item-id="listItem.foodId!"
|
||||
:items="foods"
|
||||
:item-id.sync="listItem.foodId"
|
||||
:label="$t('shopping-list.food')"
|
||||
:icon="$globals.icons.foods"
|
||||
create
|
||||
@create="createAssignFood"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div class="d-md-flex align-center" style="gap: 20px">
|
||||
<div v-if="!listItem.isFood">
|
||||
<InputQuantity v-model="listItem.quantity" />
|
||||
</div>
|
||||
<InputQuantity v-model="listItem.quantity" />
|
||||
</div>
|
||||
<v-textarea
|
||||
v-model="listItem.note"
|
||||
hide-details
|
||||
|
@ -36,17 +37,17 @@
|
|||
auto-grow
|
||||
autofocus
|
||||
@keypress="handleNoteKeyPress"
|
||||
></v-textarea>
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-end" style="gap: 20px">
|
||||
<div class="d-flex align-end">
|
||||
|
||||
<div style="max-width: 300px" class="mt-3 mr-auto">
|
||||
<InputLabelType
|
||||
v-model="listItem.label"
|
||||
v-model:item-id="listItem.labelId!"
|
||||
:items="labels"
|
||||
:item-id.sync="listItem.labelId"
|
||||
:label="$t('shopping-list.label')"
|
||||
width="250"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -54,11 +55,11 @@
|
|||
v-if="listItem.recipeReferences && listItem.recipeReferences.length > 0"
|
||||
open-on-hover
|
||||
offset-y
|
||||
left
|
||||
start
|
||||
top
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon class="mt-auto" icon v-bind="attrs" color="warning" v-on="on">
|
||||
<template #activator="{ props }">
|
||||
<v-icon class="mt-auto" :icon="$globals.icons.alert" v-bind="props" color="warning">
|
||||
{{ $globals.icons.alert }}
|
||||
</v-icon>
|
||||
</template>
|
||||
|
@ -71,10 +72,10 @@
|
|||
</div>
|
||||
<BaseButton
|
||||
v-if="listItem.labelId && listItem.food && listItem.labelId !== listItem.food.labelId"
|
||||
small
|
||||
size="small"
|
||||
color="info"
|
||||
:icon="$globals.icons.tagArrowRight"
|
||||
:text="$tc('shopping-list.save-label')"
|
||||
:text="$t('shopping-list.save-label')"
|
||||
class="mt-2 align-items-flex-start"
|
||||
@click="assignLabelToFood"
|
||||
/>
|
||||
|
@ -84,11 +85,15 @@
|
|||
<v-card-actions class="ma-0 pt-0 pb-1 justify-end">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
...(allowDelete ? [{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
}] : []),
|
||||
...(allowDelete
|
||||
? [
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: $globals.icons.close,
|
||||
text: $t('general.cancel'),
|
||||
|
@ -116,15 +121,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, watch } from "@nuxtjs/composition-api";
|
||||
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import type { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object as () => ShoppingListItemCreate | ShoppingListItemOut,
|
||||
required: true,
|
||||
},
|
||||
|
@ -146,6 +150,7 @@ export default defineComponent({
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "save", "cancel", "delete"],
|
||||
setup(props, context) {
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
|
@ -155,25 +160,25 @@ export default defineComponent({
|
|||
|
||||
const listItem = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value.food,
|
||||
() => props.modelValue.food,
|
||||
(newFood) => {
|
||||
// @ts-ignore our logic already assumes there's a label attribute, even if TS doesn't think there is
|
||||
listItem.value.label = newFood?.label || null;
|
||||
listItem.value.labelId = listItem.value.label?.id || null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function createAssignFood(val: string) {
|
||||
// keep UI reactive
|
||||
listItem.value.food ? listItem.value.food.name = val : listItem.value.food = { name: val };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
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);
|
||||
|
@ -186,7 +191,8 @@ export default defineComponent({
|
|||
|
||||
async function createAssignUnit(val: string) {
|
||||
// keep UI reactive
|
||||
listItem.value.unit ? listItem.value.unit.name = val : listItem.value.unit = { name: val };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
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);
|
||||
|
@ -203,7 +209,6 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -222,6 +227,6 @@ export default defineComponent({
|
|||
this.$emit("save");
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue