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:
Hayden 2022-04-01 09:50:31 -08:00 committed by GitHub
parent 1092e0ce7c
commit cfaac2e060
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 374 additions and 97 deletions

View file

@ -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);
}
}

View 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>

View 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>

View file

@ -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();
}

View file

@ -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}`,
};
});
});

View file

@ -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>

View file

@ -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() {

View file

@ -124,6 +124,7 @@ export interface RecipeIngredient {
food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean;
quantity?: number;
originalText?: string;
referenceId?: string;
}
export interface IngredientUnit {

View file

@ -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;
}

View file

@ -279,6 +279,7 @@ export interface RecipeIngredient {
food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean;
quantity?: number;
originalText?: string;
referenceId?: string;
}
export interface CreateIngredientUnit {

View file

@ -140,6 +140,7 @@ export interface RecipeIngredient {
food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean;
quantity?: number;
originalText?: string;
referenceId?: string;
}
export interface IngredientUnit {

View file

@ -153,6 +153,7 @@ export interface RecipeIngredient {
food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean;
quantity?: number;
originalText?: string;
referenceId?: string;
}
export interface IngredientUnit {

View file

@ -5,6 +5,7 @@ export interface Icon {
// General
chart: string;
wrench: string;
help: string;
bowlMixOutline: string;
foods: string;
units: string;

View file

@ -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,