mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-04 14:15:24 -04:00
feat(backend): ✨ add rename tag, tool, category support (#875)
This commit is contained in:
parent
8d77f4b31e
commit
e109ac0f47
@ -1,4 +1,5 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
import { BaseCRUDAPI } from "../_base";
|
||||||
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
|
||||||
@ -9,14 +10,24 @@ export interface CreateTool {
|
|||||||
|
|
||||||
export interface Tool extends CreateTool {
|
export interface Tool extends CreateTool {
|
||||||
id: number;
|
id: number;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeToolResponse extends Tool {
|
||||||
|
recipes: Recipe[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
tools: `${prefix}/tools`,
|
tools: `${prefix}/tools`,
|
||||||
toolsId: (id: string) => `${prefix}/tools/${id}`,
|
toolsId: (id: string) => `${prefix}/tools/${id}`,
|
||||||
|
toolsSlug: (id: string) => `${prefix}/tools/slug/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ToolsApi extends BaseCRUDAPI<Tool, CreateTool> {
|
export class ToolsApi extends BaseCRUDAPI<Tool, CreateTool> {
|
||||||
baseRoute: string = routes.tools;
|
baseRoute: string = routes.tools;
|
||||||
itemRoute = routes.toolsId;
|
itemRoute = routes.toolsId;
|
||||||
|
|
||||||
|
async byslug(slug: string) {
|
||||||
|
return await this.requests.get<Tool>(routes.toolsSlug(slug));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-app-bar v-if="!disableToolbar" color="transparent" flat class="mt-n1 flex-sm-wrap rounded">
|
<v-app-bar v-if="!disableToolbar" color="transparent" flat class="mt-n1 flex-sm-wrap rounded">
|
||||||
<v-icon v-if="title" large left>
|
<slot name="title">
|
||||||
{{ displayTitleIcon }}
|
<v-icon v-if="title" large left>
|
||||||
</v-icon>
|
{{ displayTitleIcon }}
|
||||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
</v-icon>
|
||||||
|
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||||
|
</slot>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn :icon="$vuetify.breakpoint.xsOnly" text :disabled="recipes.length === 0" @click="navigateRandom">
|
<v-btn :icon="$vuetify.breakpoint.xsOnly" text :disabled="recipes.length === 0" @click="navigateRandom">
|
||||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||||
|
106
frontend/components/Domain/Recipe/RecipeCategoryTagToolPage.vue
Normal file
106
frontend/components/Domain/Recipe/RecipeCategoryTagToolPage.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="items">
|
||||||
|
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
||||||
|
<v-icon large left>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
<v-toolbar-title class="headline"> {{ headline }} </v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
|
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
||||||
|
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
||||||
|
<v-row>
|
||||||
|
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
||||||
|
<v-card hover :to="`/recipes/${itemType}/${item.slug}`">
|
||||||
|
<v-card-actions>
|
||||||
|
<v-icon>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
<v-card-title class="py-1">{{ item.name }}</v-card-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, useContext, computed } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
|
type ItemType = "tags" | "categories" | "tools";
|
||||||
|
|
||||||
|
const ItemTypes = {
|
||||||
|
tag: "tags",
|
||||||
|
category: "categories",
|
||||||
|
tool: "tools",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GenericItem {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
itemType: {
|
||||||
|
type: String as () => ItemType,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as () => GenericItem[],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
// @ts-ignore
|
||||||
|
const { i18n, $globals } = useContext();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
headline: "tags",
|
||||||
|
icon: $globals.icons.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (props.itemType) {
|
||||||
|
case ItemTypes.tag:
|
||||||
|
state.headline = i18n.t("tag.tags") as string;
|
||||||
|
break;
|
||||||
|
case ItemTypes.category:
|
||||||
|
state.headline = i18n.t("recipe.categories") as string;
|
||||||
|
break;
|
||||||
|
case ItemTypes.tool:
|
||||||
|
state.headline = "Tools";
|
||||||
|
state.icon = $globals.icons.potSteam;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsSorted = computed(() => {
|
||||||
|
const byLetter: { [key: string]: Array<any> } = {};
|
||||||
|
|
||||||
|
if (!props.items) return byLetter;
|
||||||
|
|
||||||
|
props.items.forEach((item) => {
|
||||||
|
const letter = item.name[0].toUpperCase();
|
||||||
|
if (!byLetter[letter]) {
|
||||||
|
byLetter[letter] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
byLetter[letter].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return byLetter;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
itemsSorted,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
title: "vbase-nuxt",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -181,6 +181,11 @@ export default defineComponent({
|
|||||||
to: "/recipes/tags",
|
to: "/recipes/tags",
|
||||||
title: this.$t("sidebar.tags"),
|
title: this.$t("sidebar.tags"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: this.$globals.icons.potSteam,
|
||||||
|
to: "/recipes/tools",
|
||||||
|
title: "Tools",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -317,7 +317,10 @@ export default {
|
|||||||
// https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-build
|
// https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-build
|
||||||
analyze: process.env.NODE_ENV !== "production",
|
analyze: process.env.NODE_ENV !== "production",
|
||||||
babel: {
|
babel: {
|
||||||
plugins: [["@babel/plugin-proposal-private-property-in-object", { loose: true }]],
|
plugins: [
|
||||||
|
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
||||||
|
["@nuxtjs/composition-api/dist/babel-plugin"],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
transpile: process.env.NODE_ENV !== "production" ? [/@vue[\\/]composition-api/] : null,
|
transpile: process.env.NODE_ENV !== "production" ? [/@vue[\\/]composition-api/] : null,
|
||||||
},
|
},
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<BaseCardSectionTitle class="pt-2" :icon="$globals.icons.email" title="Email Configuration" />
|
<BaseCardSectionTitle class="pt-2" :icon="$globals.icons.email" title="Email Configuration" />
|
||||||
<v-alert :key="idx" border="left" colored-border :type="getColor(appConfig.emailReady)" elevation="2">
|
<v-alert border="left" colored-border :type="getColor(appConfig.emailReady)" elevation="2">
|
||||||
<div class="font-weight-medium">Email Configuration Status</div>
|
<div class="font-weight-medium">Email Configuration Status</div>
|
||||||
<div>
|
<div>
|
||||||
{{ appConfig.emailReady ? "Ready" : "Not Ready - Check Environmental Variables" }}
|
{{ appConfig.emailReady ? "Ready" : "Not Ready - Check Environmental Variables" }}
|
||||||
|
@ -6,12 +6,54 @@
|
|||||||
:title="category.name"
|
:title="category.name"
|
||||||
:recipes="category.recipes"
|
:recipes="category.recipes"
|
||||||
@sort="assignSorted"
|
@sort="assignSorted"
|
||||||
></RecipeCardSection>
|
>
|
||||||
|
<template #title>
|
||||||
|
<v-btn icon class="mr-1">
|
||||||
|
<v-icon dark large @click="reset">
|
||||||
|
{{ $globals.icons.tags }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<template v-if="edit">
|
||||||
|
<v-text-field
|
||||||
|
v-model="category.name"
|
||||||
|
autofocus
|
||||||
|
single-line
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="headline"
|
||||||
|
@keyup.enter="updateCategory"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn icon @click="updateCategory">
|
||||||
|
<v-icon size="28">
|
||||||
|
{{ $globals.icons.save }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon class="mr-1" @click="reset">
|
||||||
|
<v-icon size="28">
|
||||||
|
{{ $globals.icons.close }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template #activator="{ on, attrs }">
|
||||||
|
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
||||||
|
{{ category.name }}
|
||||||
|
</v-toolbar-title>
|
||||||
|
</template>
|
||||||
|
<span> Click to Edit </span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</RecipeCardSection>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
@ -21,13 +63,49 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const slug = route.value.params.slug;
|
const slug = route.value.params.slug;
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
initialValue: "",
|
||||||
|
edit: false,
|
||||||
|
});
|
||||||
|
|
||||||
const category = useAsync(async () => {
|
const category = useAsync(async () => {
|
||||||
const { data } = await api.categories.getOne(slug);
|
const { data } = await api.categories.getOne(slug);
|
||||||
|
if (data) {
|
||||||
|
state.initialValue = data.name;
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}, slug);
|
}, slug);
|
||||||
return { category };
|
|
||||||
|
function reset() {
|
||||||
|
state.edit = false;
|
||||||
|
|
||||||
|
if (category.value) {
|
||||||
|
category.value.name = state.initialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCategory() {
|
||||||
|
state.edit = false;
|
||||||
|
|
||||||
|
if (!category.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await api.categories.updateOne(category.value.slug, category.value);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
router.push("/recipes/categories/" + data.slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
category,
|
||||||
|
reset,
|
||||||
|
...toRefs(state),
|
||||||
|
updateCategory,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
return {
|
return {
|
||||||
@ -44,6 +122,4 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,71 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container v-if="categories">
|
<v-container>
|
||||||
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
<RecipeCategoryTagToolPage v-if="categories" :items="categories" item-type="categories" />
|
||||||
<v-icon large left>
|
|
||||||
{{ $globals.icons.tags }}
|
|
||||||
</v-icon>
|
|
||||||
<v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
<section v-for="(items, key, idx) in categoriesByLetter" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
|
||||||
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
|
||||||
<v-row>
|
|
||||||
<v-col v-for="(item, index) in items" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
|
||||||
<v-card hover :to="`/recipes/categories/${item.slug}`">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-icon>
|
|
||||||
{{ $globals.icons.tags }}
|
|
||||||
</v-icon>
|
|
||||||
<v-card-title class="py-1">{{ item.name }}</v-card-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</section>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, useAsync } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||||
|
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
RecipeCategoryTagToolPage,
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const api = useUserApi();
|
const userApi = useUserApi();
|
||||||
|
|
||||||
const categories = useAsync(async () => {
|
const categories = useAsync(async () => {
|
||||||
const { data } = await api.categories.getAll();
|
const { data } = await userApi.categories.getAll();
|
||||||
return data;
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
const categoriesByLetter: any = computed(() => {
|
if (data) {
|
||||||
const catsByLetter: { [key: string]: Array<any> } = {};
|
return data;
|
||||||
|
}
|
||||||
if (!categories.value) return catsByLetter;
|
|
||||||
|
|
||||||
categories.value.forEach((item) => {
|
|
||||||
const letter = item.name[0].toUpperCase();
|
|
||||||
if (!catsByLetter[letter]) {
|
|
||||||
catsByLetter[letter] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
catsByLetter[letter].push(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
return catsByLetter;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { categories, api, categoriesByLetter };
|
|
||||||
},
|
|
||||||
head() {
|
|
||||||
return {
|
return {
|
||||||
title: this.$t("sidebar.categories") as string,
|
categories,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// head: {
|
||||||
|
// // @ts-ignore
|
||||||
|
// title: this.$t("sidebar.categories") as string,
|
||||||
|
// },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,49 +1,125 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCardSection
|
<RecipeCardSection
|
||||||
v-if="tag"
|
v-if="tags"
|
||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
:title="tag.name"
|
:title="tags.name"
|
||||||
:recipes="tag.recipes"
|
:recipes="tags.recipes"
|
||||||
@sort="assignSorted"
|
@sort="assignSorted"
|
||||||
></RecipeCardSection>
|
>
|
||||||
|
<template #title>
|
||||||
|
<v-btn icon class="mr-1">
|
||||||
|
<v-icon dark large @click="reset">
|
||||||
|
{{ $globals.icons.tags }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<template v-if="edit">
|
||||||
|
<v-text-field
|
||||||
|
v-model="tags.name"
|
||||||
|
autofocus
|
||||||
|
single-line
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="headline"
|
||||||
|
@keyup.enter="updateTags"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn icon @click="updateTags">
|
||||||
|
<v-icon size="28">
|
||||||
|
{{ $globals.icons.save }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon class="mr-1" @click="reset">
|
||||||
|
<v-icon size="28">
|
||||||
|
{{ $globals.icons.close }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template #activator="{ on, attrs }">
|
||||||
|
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
||||||
|
{{ tags.name }}
|
||||||
|
</v-toolbar-title>
|
||||||
|
</template>
|
||||||
|
<span> Click to Edit </span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</RecipeCardSection>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { Recipe } from "~/types/api-types/admin";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const slug = route.value.params.slug;
|
const slug = route.value.params.slug;
|
||||||
|
|
||||||
const tag = useAsync(async () => {
|
const state = reactive({
|
||||||
|
initialValue: "",
|
||||||
|
edit: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tags = useAsync(async () => {
|
||||||
const { data } = await api.tags.getOne(slug);
|
const { data } = await api.tags.getOne(slug);
|
||||||
|
if (data) {
|
||||||
|
state.initialValue = data.name;
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}, slug);
|
}, slug);
|
||||||
return { tag };
|
|
||||||
|
function reset() {
|
||||||
|
state.edit = false;
|
||||||
|
|
||||||
|
if (tags.value) {
|
||||||
|
tags.value.name = state.initialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTags() {
|
||||||
|
state.edit = false;
|
||||||
|
|
||||||
|
if (!tags.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await api.tags.updateOne(tags.value.slug, tags.value);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
router.push("/recipes/tags/" + data.slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags,
|
||||||
|
reset,
|
||||||
|
...toRefs(state),
|
||||||
|
updateTags,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
return {
|
return {
|
||||||
title: this.$t("sidebar.tags") as string,
|
title: this.$t("sidebar.categories") as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
assignSorted(val: Array<Recipe>) {
|
assignSorted(val: Array<Recipe>) {
|
||||||
if (this.tag) {
|
if (this.tags) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.tag.recipes = val;
|
this.tags.recipes = val;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,71 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container v-if="tags">
|
<v-container>
|
||||||
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tags" />
|
||||||
<v-icon large left>
|
|
||||||
{{ $globals.icons.tags }}
|
|
||||||
</v-icon>
|
|
||||||
<v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
<section v-for="(items, key, idx) in tagsByLetter" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
|
||||||
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
|
||||||
<v-row>
|
|
||||||
<v-col v-for="(item, index) in items" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
|
||||||
<v-card hover :to="`/recipes/tags/${item.slug}`">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-icon>
|
|
||||||
{{ $globals.icons.tags }}
|
|
||||||
</v-icon>
|
|
||||||
<v-card-title class="py-1">{{ item.name }}</v-card-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</section>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, computed } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||||
|
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
components: {
|
||||||
const api = useUserApi();
|
RecipeCategoryTagToolPage,
|
||||||
|
|
||||||
const tags = useAsync(async () => {
|
|
||||||
const { data } = await api.tags.getAll();
|
|
||||||
return data;
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
const tagsByLetter: any = computed(() => {
|
|
||||||
const tagsByLetter: { [key: string]: Array<any> } = {};
|
|
||||||
|
|
||||||
if (!tags.value) return tagsByLetter;
|
|
||||||
|
|
||||||
tags.value.forEach((item) => {
|
|
||||||
const letter = item.name[0].toUpperCase();
|
|
||||||
if (!tagsByLetter[letter]) {
|
|
||||||
tagsByLetter[letter] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
tagsByLetter[letter].push(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
return tagsByLetter;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { tags, api, tagsByLetter };
|
|
||||||
},
|
},
|
||||||
head() {
|
setup() {
|
||||||
|
const userApi = useUserApi();
|
||||||
|
const tools = useAsync(async () => {
|
||||||
|
const { data } = await userApi.tags.getAll();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
title: this.$t("sidebar.tags") as string,
|
tools,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// head: {
|
||||||
|
// // @ts-ignore
|
||||||
|
// title: this.$t("sidebar.tags") as string,
|
||||||
|
// },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
119
frontend/pages/recipes/tools/_slug.vue
Normal file
119
frontend/pages/recipes/tools/_slug.vue
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<RecipeCardSection v-if="tools" :title="tools.name" :recipes="tools.recipes" @sort="assignSorted">
|
||||||
|
<template #title>
|
||||||
|
<v-btn icon class="mr-1">
|
||||||
|
<v-icon dark large @click="reset">
|
||||||
|
{{ $globals.icons.potSteam }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<template v-if="edit">
|
||||||
|
<v-text-field
|
||||||
|
v-model="tools.name"
|
||||||
|
autofocus
|
||||||
|
single-line
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="headline"
|
||||||
|
@keyup.enter="updateTools"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn icon @click="updateTools">
|
||||||
|
<v-icon size="28">
|
||||||
|
{{ $globals.icons.save }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon class="mr-1" @click="reset">
|
||||||
|
<v-icon size="28">
|
||||||
|
{{ $globals.icons.close }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template #activator="{ on, attrs }">
|
||||||
|
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
||||||
|
{{ tools.name }}
|
||||||
|
</v-toolbar-title>
|
||||||
|
</template>
|
||||||
|
<span> Click to Edit </span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</RecipeCardSection>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { RecipeCardSection },
|
||||||
|
setup() {
|
||||||
|
const api = useUserApi();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const slug = route.value.params.slug;
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
initialValue: "",
|
||||||
|
edit: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tools = useAsync(async () => {
|
||||||
|
const { data } = await api.tools.byslug(slug);
|
||||||
|
if (data) {
|
||||||
|
state.initialValue = data.name;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}, slug);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
state.edit = false;
|
||||||
|
|
||||||
|
if (tools.value) {
|
||||||
|
tools.value.name = state.initialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTools() {
|
||||||
|
state.edit = false;
|
||||||
|
|
||||||
|
if (!tools.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await api.tools.updateOne(tools.value.id, tools.value);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
router.push("/recipes/tools/" + data.slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
reset,
|
||||||
|
...toRefs(state),
|
||||||
|
updateTools,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.categories") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
assignSorted(val: Array<Recipe>) {
|
||||||
|
if (this.tools) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.tools.recipes = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
33
frontend/pages/recipes/tools/index.vue
Normal file
33
frontend/pages/recipes/tools/index.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tools" />
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||||
|
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
RecipeCategoryTagToolPage,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const userApi = useUserApi();
|
||||||
|
const tools = useAsync(async () => {
|
||||||
|
const { data } = await userApi.tools.getAll();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
title: "Tools",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -2,7 +2,6 @@ import { useDark } from "@vueuse/core";
|
|||||||
|
|
||||||
export default ({ $vuetify }: any) => {
|
export default ({ $vuetify }: any) => {
|
||||||
const isDark = useDark();
|
const isDark = useDark();
|
||||||
console.log("isDark Plugin", isDark);
|
|
||||||
|
|
||||||
if (isDark.value) {
|
if (isDark.value) {
|
||||||
$vuetify.theme.dark = true;
|
$vuetify.theme.dark = true;
|
||||||
|
@ -28,10 +28,9 @@ from mealie.schema.group.group_preferences import ReadGroupPreferences
|
|||||||
from mealie.schema.group.invite_token import ReadInviteToken
|
from mealie.schema.group.invite_token import ReadInviteToken
|
||||||
from mealie.schema.group.webhook import ReadWebhook
|
from mealie.schema.group.webhook import ReadWebhook
|
||||||
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
||||||
from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse
|
from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse, RecipeTool
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
||||||
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
|
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
|
||||||
from mealie.schema.recipe.recipe_tool import RecipeTool
|
|
||||||
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
|
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
|
||||||
from mealie.schema.server import ServerTask
|
from mealie.schema.server import ServerTask
|
||||||
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
|
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from slugify import slugify
|
||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm
|
||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
@ -14,10 +15,10 @@ recipes_to_tools = Table(
|
|||||||
class Tool(SqlAlchemyBase, BaseMixins):
|
class Tool(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "tools"
|
__tablename__ = "tools"
|
||||||
name = Column(String, index=True, unique=True, nullable=False)
|
name = Column(String, index=True, unique=True, nullable=False)
|
||||||
|
slug = Column(String, index=True, unique=True, nullable=False)
|
||||||
on_hand = Column(Boolean, default=False)
|
on_hand = Column(Boolean, default=False)
|
||||||
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tools, back_populates="tools")
|
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tools, back_populates="tools")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, name, on_hand, **_) -> None:
|
def __init__(self, name, **_) -> None:
|
||||||
self.on_hand = on_hand
|
self.slug = slugify(name)
|
||||||
self.name = name
|
|
||||||
|
@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.dependencies import is_logged_in
|
from mealie.core.dependencies import is_logged_in
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||||
@ -10,6 +11,7 @@ from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
|||||||
public_router = APIRouter()
|
public_router = APIRouter()
|
||||||
user_router = UserAPIRouter()
|
user_router = UserAPIRouter()
|
||||||
admin_router = AdminAPIRouter()
|
admin_router = AdminAPIRouter()
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@public_router.get("")
|
@public_router.get("")
|
||||||
@ -61,6 +63,7 @@ async def update_recipe_category(category: str, new_category: CategoryIn, sessio
|
|||||||
try:
|
try:
|
||||||
return db.categories.update(category, new_category.dict())
|
return db.categories.update(category, new_category.dict())
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception("Failed to update category")
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from mealie.schema.recipe.recipe_tool import RecipeToolResponse
|
||||||
from mealie.services._base_http_service.router_factory import RouterFactory
|
from mealie.services._base_http_service.router_factory import RouterFactory
|
||||||
from mealie.services.recipe.recipe_tool_service import RecipeToolService
|
from mealie.services.recipe.recipe_tool_service import RecipeToolService
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
router.include_router(RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"]))
|
tools_router = RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"])
|
||||||
|
|
||||||
|
|
||||||
|
@tools_router.get("/slug/{slug}")
|
||||||
|
async def Func(slug: str, tools_service: RecipeToolService = Depends(RecipeToolService.private)):
|
||||||
|
"""Returns a recipe by slug."""
|
||||||
|
return tools_service.db.tools.get_one(slug, "slug", override_schema=RecipeToolResponse)
|
||||||
|
|
||||||
|
|
||||||
|
router.include_router(tools_router)
|
||||||
|
@ -3,4 +3,5 @@ from .recipe_category import *
|
|||||||
from .recipe_comments import *
|
from .recipe_comments import *
|
||||||
from .recipe_image_types import *
|
from .recipe_image_types import *
|
||||||
from .recipe_ingredient import *
|
from .recipe_ingredient import *
|
||||||
|
from .recipe_tool import *
|
||||||
from .request_helpers import *
|
from .request_helpers import *
|
||||||
|
@ -18,7 +18,6 @@ from .recipe_notes import RecipeNote
|
|||||||
from .recipe_nutrition import Nutrition
|
from .recipe_nutrition import Nutrition
|
||||||
from .recipe_settings import RecipeSettings
|
from .recipe_settings import RecipeSettings
|
||||||
from .recipe_step import RecipeStep
|
from .recipe_step import RecipeStep
|
||||||
from .recipe_tool import RecipeTool
|
|
||||||
|
|
||||||
app_dirs = get_app_dirs()
|
app_dirs = get_app_dirs()
|
||||||
|
|
||||||
@ -35,6 +34,11 @@ class RecipeCategory(RecipeTag):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeTool(RecipeTag):
|
||||||
|
id: int = 0
|
||||||
|
on_hand: bool = False
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipeByUrl(BaseModel):
|
class CreateRecipeByUrl(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class RecipeTagResponse(RecipeCategoryResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from .recipe import Recipe
|
from . import Recipe
|
||||||
|
|
||||||
RecipeCategoryResponse.update_forward_refs()
|
RecipeCategoryResponse.update_forward_refs()
|
||||||
RecipeTagResponse.update_forward_refs()
|
RecipeTagResponse.update_forward_refs()
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
|
||||||
@ -8,6 +10,19 @@ class RecipeToolCreate(CamelModel):
|
|||||||
|
|
||||||
class RecipeTool(RecipeToolCreate):
|
class RecipeTool(RecipeToolCreate):
|
||||||
id: int
|
id: int
|
||||||
|
slug: str
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeToolResponse(RecipeTool):
|
||||||
|
recipes: List["Recipe"] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
from .recipe import Recipe
|
||||||
|
|
||||||
|
RecipeToolResponse.update_forward_refs()
|
||||||
|
@ -63,7 +63,7 @@ class MealieAlphaMigrator(BaseMigrator):
|
|||||||
recipe_lookup: dict[str, Path] = {}
|
recipe_lookup: dict[str, Path] = {}
|
||||||
recipes_as_dicts = []
|
recipes_as_dicts = []
|
||||||
|
|
||||||
for x in temp_path.rglob("**/[!.]*.json"):
|
for x in temp_path.rglob("**/recipes/**/[!.]*.json"):
|
||||||
if (y := MigrationReaders.json(x)) is not None:
|
if (y := MigrationReaders.json(x)) is not None:
|
||||||
recipes_as_dicts.append(y)
|
recipes_as_dicts.append(y)
|
||||||
slug = y["slug"]
|
slug = y["slug"]
|
||||||
@ -76,12 +76,16 @@ class MealieAlphaMigrator(BaseMigrator):
|
|||||||
recipe_model_lookup = {x.slug: x for x in recipes}
|
recipe_model_lookup = {x.slug: x for x in recipes}
|
||||||
|
|
||||||
for slug, status in results:
|
for slug, status in results:
|
||||||
if status:
|
if not status:
|
||||||
model = recipe_model_lookup.get(slug)
|
continue
|
||||||
dest_dir = model.directory
|
|
||||||
source_dir = recipe_lookup.get(slug)
|
|
||||||
|
|
||||||
if dest_dir.exists():
|
model = recipe_model_lookup.get(slug)
|
||||||
shutil.rmtree(dest_dir)
|
dest_dir = model.directory
|
||||||
|
source_dir = recipe_lookup.get(slug)
|
||||||
|
|
||||||
shutil.copytree(source_dir, dest_dir)
|
if dest_dir.exists():
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
|
|
||||||
|
for dir in source_dir.iterdir():
|
||||||
|
if dir.is_dir():
|
||||||
|
shutil.copytree(dir, dest_dir / dir.name)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import base64
|
import base64
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from gzip import GzipFile
|
from gzip import GzipFile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import regex as re
|
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
from mealie.schema.recipe import RecipeNote
|
from mealie.schema.recipe import RecipeNote
|
||||||
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from mealie.schema.recipe.recipe_tool import RecipeTool, RecipeToolCreate
|
from mealie.schema.recipe import RecipeTool, RecipeToolCreate
|
||||||
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
||||||
from mealie.services._base_http_service.http_services import UserHttpService
|
from mealie.services._base_http_service.http_services import UserHttpService
|
||||||
from mealie.services.events import create_recipe_event
|
from mealie.services.events import create_recipe_event
|
||||||
|
@ -22,6 +22,9 @@ class Routes:
|
|||||||
class TestRecipeTool:
|
class TestRecipeTool:
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
slug: str
|
||||||
|
on_hand: bool
|
||||||
|
recipes: list
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -32,7 +35,15 @@ def tool(api_client: TestClient, unique_user: TestUser) -> TestRecipeTool:
|
|||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
yield TestRecipeTool(id=response.json()["id"], name=data["name"])
|
as_json = response.json()
|
||||||
|
|
||||||
|
yield TestRecipeTool(
|
||||||
|
id=as_json["id"],
|
||||||
|
name=data["name"],
|
||||||
|
slug=as_json["slug"],
|
||||||
|
on_hand=as_json["onHand"],
|
||||||
|
recipes=[],
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token)
|
response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token)
|
||||||
@ -58,7 +69,12 @@ def test_read_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: Te
|
|||||||
|
|
||||||
|
|
||||||
def test_update_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
|
def test_update_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
|
||||||
update_data = {"id": tool.id, "name": random_string(10)}
|
update_data = {
|
||||||
|
"id": tool.id,
|
||||||
|
"name": random_string(10),
|
||||||
|
"slug": tool.slug,
|
||||||
|
"on_hand": True,
|
||||||
|
}
|
||||||
|
|
||||||
response = api_client.put(Routes.item(tool.id), json=update_data, headers=unique_user.token)
|
response = api_client.put(Routes.item(tool.id), json=update_data, headers=unique_user.token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -89,7 +105,7 @@ def test_recipe_tool_association(api_client: TestClient, tool: TestRecipeTool, u
|
|||||||
|
|
||||||
as_json = response.json()
|
as_json = response.json()
|
||||||
|
|
||||||
as_json["tools"] = [{"id": tool.id, "name": tool.name}]
|
as_json["tools"] = [{"id": tool.id, "name": tool.name, "slug": tool.slug}]
|
||||||
|
|
||||||
# Update Recipe
|
# Update Recipe
|
||||||
response = api_client.put(Routes.recipe(slug), json=as_json, headers=unique_user.token)
|
response = api_client.put(Routes.recipe(slug), json=as_json, headers=unique_user.token)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user