mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-16 10:03:54 -07:00
feat: additional cookbook features (tags, tools, and public) (#1116)
* migration: add public, tags, and tools * generate frontend types * add help icon * start replacement for tool-tag-category selector * add help icon utility * use generator types * add support for cookbook features * add UI elements for cookbook features * fix tests * fix type error
This commit is contained in:
parent
1092e0ce7c
commit
cfaac2e060
23 changed files with 374 additions and 97 deletions
|
@ -1,32 +1,18 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { CategoryBase } from "~/types/api-types/recipe";
|
||||
import { RecipeCategory } from "~/types/api-types/user";
|
||||
import { CreateCookBook, RecipeCookBook, UpdateCookBook } from "~/types/api-types/cookbook";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
export interface CreateCookBook {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CookBook extends CreateCookBook {
|
||||
id: number;
|
||||
slug: string;
|
||||
description: string;
|
||||
position: number;
|
||||
group_id: number;
|
||||
categories: RecipeCategory[] | CategoryBase[];
|
||||
}
|
||||
|
||||
const routes = {
|
||||
cookbooks: `${prefix}/groups/cookbooks`,
|
||||
cookbooksId: (id: number) => `${prefix}/groups/cookbooks/${id}`,
|
||||
};
|
||||
|
||||
export class CookbookAPI extends BaseCRUDAPI<CookBook, CreateCookBook> {
|
||||
export class CookbookAPI extends BaseCRUDAPI<RecipeCookBook, CreateCookBook> {
|
||||
baseRoute: string = routes.cookbooks;
|
||||
itemRoute = routes.cookbooksId;
|
||||
|
||||
async updateAll(payload: CookBook[]) {
|
||||
async updateAll(payload: UpdateCookBook[]) {
|
||||
return await this.requests.put(this.baseRoute, payload);
|
||||
}
|
||||
}
|
||||
|
|
109
frontend/components/Domain/Recipe/RecipeOrganizerSelector.vue
Normal file
109
frontend/components/Domain/Recipe/RecipeOrganizerSelector.vue
Normal file
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
:value="value"
|
||||
:label="label"
|
||||
chips
|
||||
deletable-chips
|
||||
item-text="name"
|
||||
multiple
|
||||
:prepend-inner-icon="$globals.icons.tags"
|
||||
return-object
|
||||
v-bind="inputAttrs"
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
@click:close="removeByIndex(data.index)"
|
||||
>
|
||||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { computed, onMounted } from "vue-demi";
|
||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||
import { RecipeTool } from "~/types/api-types/admin";
|
||||
|
||||
type OrganizerType = "tag" | "category" | "tool";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[] | undefined,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* The type of organizer to use.
|
||||
*/
|
||||
selectorType: {
|
||||
type: String as () => OrganizerType,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* List of items that are available to be chosen from
|
||||
*/
|
||||
items: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[],
|
||||
required: true,
|
||||
},
|
||||
inputAttrs: {
|
||||
type: Object as () => Record<string, any>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.value,
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
selected.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
const { i18n } = useContext();
|
||||
|
||||
const label = computed(() => {
|
||||
switch (props.selectorType) {
|
||||
case "tag":
|
||||
return i18n.t("tag.tags");
|
||||
case "category":
|
||||
return i18n.t("category.categories");
|
||||
case "tool":
|
||||
return "Tools";
|
||||
}
|
||||
});
|
||||
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
selected,
|
||||
removeByIndex,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
28
frontend/components/global/HelpIcon.vue
Normal file
28
frontend/components/global/HelpIcon.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu top offset-y left open-on-hover>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on" @click.stop>
|
||||
<v-icon> {{ $globals.icons.help }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card max-width="300px">
|
||||
<v-card-text>
|
||||
<slot></slot>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,9 +1,9 @@
|
|||
import { useAsync, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { CookBook } from "~/api/class-interfaces/group-cookbooks";
|
||||
import { ReadCookBook, RecipeCookBook, UpdateCookBook } from "~/types/api-types/cookbook";
|
||||
|
||||
let cookbookStore: Ref<CookBook[] | null> | null = null;
|
||||
let cookbookStore: Ref<ReadCookBook[] | null> | null = null;
|
||||
|
||||
export const useCookbook = function () {
|
||||
function getOne(id: string | number) {
|
||||
|
@ -60,13 +60,13 @@ export const useCookbooks = function () {
|
|||
|
||||
loading.value = false;
|
||||
},
|
||||
async updateOne(updateData: CookBook) {
|
||||
async updateOne(updateData: UpdateCookBook) {
|
||||
if (!updateData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.updateOne(updateData.id, updateData);
|
||||
const { data } = await api.cookbooks.updateOne(updateData.id, updateData as RecipeCookBook);
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<v-icon>{{ $globals.icons.translate }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t('sidebar.language') }}</v-list-item-title>
|
||||
<v-list-item-title>{{ $t("sidebar.language") }}</v-list-item-title>
|
||||
<LanguageDialog v-model="languageDialog" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
@ -103,7 +103,7 @@ export default defineComponent({
|
|||
return {
|
||||
icon: $globals.icons.pages,
|
||||
title: cookbook.name,
|
||||
to: `/cookbooks/${cookbook.slug}`,
|
||||
to: `/cookbooks/${cookbook.slug as string}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,16 +9,10 @@
|
|||
{{ book.description }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-tabs v-model="tab" show-arrows>
|
||||
<v-tab v-for="(cat, index) in book.categories" :key="index">
|
||||
{{ cat.name }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item v-for="(cat, idx) in book.categories" :key="`tabs` + idx">
|
||||
<RecipeCardSection class="mb-5 mx-1" :recipes="cat.recipes" />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
|
||||
<v-container class="pa-0">
|
||||
<RecipeCardSection class="mb-5 mx-1" :recipes="book.recipes" />
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
@ -26,6 +20,7 @@
|
|||
import { defineComponent, useRoute, ref, useMeta } from "@nuxtjs/composition-api";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
setup() {
|
||||
|
@ -51,6 +46,3 @@ export default defineComponent({
|
|||
head: {}, // Must include for useMeta
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -5,7 +5,9 @@
|
|||
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Cookbooks </template>
|
||||
Arrange and edit your cookbooks here.
|
||||
Cookbooks are another way to organize recipes by creating cross sections of recipes and tags. Creating a cookbook
|
||||
will add an entry to the side-bar and all the recipes with the tags and categories chosen will be displayed in the
|
||||
cookbook.
|
||||
</BasePageTitle>
|
||||
|
||||
<BaseButton create @click="actions.createOne()" />
|
||||
|
@ -31,10 +33,24 @@
|
|||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-card-text>
|
||||
<v-card-text v-if="cookbooks">
|
||||
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
|
||||
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea>
|
||||
<DomainRecipeCategoryTagSelector v-model="cookbooks[index].categories" />
|
||||
<RecipeOrganizerSelector
|
||||
v-model="cookbooks[index].categories"
|
||||
:items="allCategories || []"
|
||||
selector-type="category"
|
||||
/>
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tags" :items="allTags || []" selector-type="tag" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tools" :items="tools || []" selector-type="tool" />
|
||||
<v-switch v-model="cookbooks[index].public">
|
||||
<template #label>
|
||||
Public Cookbook
|
||||
<HelpIcon class="ml-4">
|
||||
Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.
|
||||
</HelpIcon>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
@ -42,12 +58,12 @@
|
|||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
text: $tc('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $t('general.save'),
|
||||
text: $tc('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
|
@ -66,15 +82,27 @@
|
|||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import draggable from "vuedraggable";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useCategories, useTags, useTools } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable },
|
||||
components: { draggable, RecipeOrganizerSelector },
|
||||
setup() {
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
const { tools } = useTools();
|
||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||
|
||||
getAllCategories();
|
||||
getAllTags();
|
||||
|
||||
return {
|
||||
allCategories,
|
||||
allTags,
|
||||
cookbooks,
|
||||
actions,
|
||||
tools,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
|
|
|
@ -124,6 +124,7 @@ export interface RecipeIngredient {
|
|||
food?: IngredientFood | CreateIngredientFood;
|
||||
disableAmount?: boolean;
|
||||
quantity?: number;
|
||||
originalText?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface IngredientUnit {
|
||||
|
|
|
@ -15,22 +15,46 @@ export interface CreateCookBook {
|
|||
description?: string;
|
||||
slug?: string;
|
||||
position?: number;
|
||||
public?: boolean;
|
||||
categories?: CategoryBase[];
|
||||
tags?: TagBase[];
|
||||
tools?: RecipeTool[];
|
||||
}
|
||||
export interface TagBase {
|
||||
name: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface RecipeTool {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
onHand?: boolean;
|
||||
}
|
||||
export interface ReadCookBook {
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
position?: number;
|
||||
public?: boolean;
|
||||
categories?: CategoryBase[];
|
||||
tags?: TagBase[];
|
||||
tools?: RecipeTool[];
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
export interface RecipeCategoryResponse {
|
||||
export interface RecipeCookBook {
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
position?: number;
|
||||
public?: boolean;
|
||||
categories?: CategoryBase[];
|
||||
tags?: TagBase[];
|
||||
tools?: RecipeTool[];
|
||||
groupId: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
recipes?: RecipeSummary[];
|
||||
recipes: RecipeSummary[];
|
||||
}
|
||||
export interface RecipeSummary {
|
||||
id?: string;
|
||||
|
@ -64,12 +88,6 @@ export interface RecipeTag {
|
|||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface RecipeTool {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
onHand?: boolean;
|
||||
}
|
||||
export interface RecipeIngredient {
|
||||
title?: string;
|
||||
note?: string;
|
||||
|
@ -77,6 +95,7 @@ export interface RecipeIngredient {
|
|||
food?: IngredientFood | CreateIngredientFood;
|
||||
disableAmount?: boolean;
|
||||
quantity?: number;
|
||||
originalText?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface IngredientUnit {
|
||||
|
@ -110,21 +129,15 @@ export interface CreateIngredientFood {
|
|||
description?: string;
|
||||
labelId?: string;
|
||||
}
|
||||
export interface RecipeCookBook {
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
position?: number;
|
||||
categories: RecipeCategoryResponse[];
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
export interface SaveCookBook {
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
position?: number;
|
||||
public?: boolean;
|
||||
categories?: CategoryBase[];
|
||||
tags?: TagBase[];
|
||||
tools?: RecipeTool[];
|
||||
groupId: string;
|
||||
}
|
||||
export interface UpdateCookBook {
|
||||
|
@ -132,7 +145,10 @@ export interface UpdateCookBook {
|
|||
description?: string;
|
||||
slug?: string;
|
||||
position?: number;
|
||||
public?: boolean;
|
||||
categories?: CategoryBase[];
|
||||
tags?: TagBase[];
|
||||
tools?: RecipeTool[];
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
|
@ -279,6 +279,7 @@ export interface RecipeIngredient {
|
|||
food?: IngredientFood | CreateIngredientFood;
|
||||
disableAmount?: boolean;
|
||||
quantity?: number;
|
||||
originalText?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface CreateIngredientUnit {
|
||||
|
|
|
@ -140,6 +140,7 @@ export interface RecipeIngredient {
|
|||
food?: IngredientFood | CreateIngredientFood;
|
||||
disableAmount?: boolean;
|
||||
quantity?: number;
|
||||
originalText?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface IngredientUnit {
|
||||
|
|
|
@ -153,6 +153,7 @@ export interface RecipeIngredient {
|
|||
food?: IngredientFood | CreateIngredientFood;
|
||||
disableAmount?: boolean;
|
||||
quantity?: number;
|
||||
originalText?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface IngredientUnit {
|
||||
|
|
|
@ -5,6 +5,7 @@ export interface Icon {
|
|||
// General
|
||||
chart: string;
|
||||
wrench: string;
|
||||
help: string;
|
||||
bowlMixOutline: string;
|
||||
foods: string;
|
||||
units: string;
|
||||
|
|
|
@ -107,6 +107,7 @@ import {
|
|||
mdiBowlMixOutline,
|
||||
mdiWrench,
|
||||
mdiChartLine,
|
||||
mdiHelpCircleOutline,
|
||||
} from "@mdi/js";
|
||||
|
||||
export const icons = {
|
||||
|
@ -118,6 +119,7 @@ export const icons = {
|
|||
|
||||
// General
|
||||
bowlMixOutline: mdiBowlMixOutline,
|
||||
help: mdiHelpCircleOutline,
|
||||
foods: mdiFoodApple,
|
||||
units: mdiBeakerOutline,
|
||||
alert: mdiAlert,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue