mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
unified category/tag selector
This commit is contained in:
parent
ac362b5692
commit
e17a42900a
9 changed files with 71 additions and 138 deletions
|
@ -44,6 +44,11 @@ export const tagAPI = {
|
||||||
let response = await apiReq.get(tagURLs.getAll);
|
let response = await apiReq.get(tagURLs.getAll);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
async create(name) {
|
||||||
|
let response = await apiReq.post(tagURLs.getAll, { name: name });
|
||||||
|
store.dispatch("requestTags");
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
async getRecipesInTag(tag) {
|
async getRecipesInTag(tag) {
|
||||||
let response = await apiReq.get(tagURLs.getTag(tag));
|
let response = await apiReq.get(tagURLs.getTag(tag));
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import NewCategoryDialog from "./NewCategoryDialog.vue";
|
import NewCategoryDialog from "@/components/UI/Dialogs/NewCategoryDialog.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-btn icon @click="dialog = true">
|
|
||||||
<v-icon color="white">mdi-plus</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-dialog v-model="dialog" width="500">
|
|
||||||
<v-card>
|
|
||||||
<v-app-bar dense dark color="primary mb-2">
|
|
||||||
<v-icon large left class="mt-1">
|
|
||||||
mdi-tag
|
|
||||||
</v-icon>
|
|
||||||
|
|
||||||
<v-toolbar-title class="headline">
|
|
||||||
Create a Category
|
|
||||||
</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
<v-card-title> </v-card-title>
|
|
||||||
<v-form @submit.prevent="select">
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
dense
|
|
||||||
label="Category Name"
|
|
||||||
v-model="categoryName"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="grey" text @click="dialog = false">
|
|
||||||
{{ $t("general.cancel") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn color="success" text type="submit" :disabled="!categoryName">
|
|
||||||
{{ $t("general.create") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-form>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { api } from "@/api";
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
buttonText: String,
|
|
||||||
value: String,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dialog: false,
|
|
||||||
categoryName: "",
|
|
||||||
rules: {
|
|
||||||
required: val =>
|
|
||||||
!!val || this.$t("settings.theme.theme-name-is-required"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
dialog(val) {
|
|
||||||
if (!val) this.categoryName = "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async select() {
|
|
||||||
await api.categories.create(this.categoryName);
|
|
||||||
this.$emit("new-category", this.categoryName);
|
|
||||||
this.dialog = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<v-select
|
<v-autocomplete
|
||||||
:items="activeItems"
|
:items="activeItems"
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
|
:value="value"
|
||||||
:label="inputLabel"
|
:label="inputLabel"
|
||||||
chips
|
chips
|
||||||
deletable-chips
|
deletable-chips
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
:solo="solo"
|
:solo="solo"
|
||||||
:return-object="returnObject"
|
:return-object="returnObject"
|
||||||
:flat="flat"
|
:flat="flat"
|
||||||
@change="emitChange"
|
@input="emitChange"
|
||||||
>
|
>
|
||||||
<template v-slot:selection="data">
|
<template v-slot:selection="data">
|
||||||
<v-chip
|
<v-chip
|
||||||
|
@ -24,16 +25,28 @@
|
||||||
label
|
label
|
||||||
color="accent"
|
color="accent"
|
||||||
dark
|
dark
|
||||||
|
:key="data.index"
|
||||||
>
|
>
|
||||||
{{ data.item.name }}
|
{{ data.item.name || data.item }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
<template v-slot:append-outer="">
|
||||||
|
<NewCategoryDialog
|
||||||
|
v-if="showAdd"
|
||||||
|
:tag-dialog="tagSelector"
|
||||||
|
@created-item="pushToItem"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-autocomplete>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import NewCategoryDialog from "@/components/UI/Dialogs/NewCategoryDialog";
|
||||||
const MOUNTED_EVENT = "mounted";
|
const MOUNTED_EVENT = "mounted";
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
NewCategoryDialog,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: Array,
|
value: Array,
|
||||||
solo: {
|
solo: {
|
||||||
|
@ -51,6 +64,12 @@ export default {
|
||||||
hint: {
|
hint: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
showAdd: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showLabel: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -59,6 +78,7 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$emit(MOUNTED_EVENT);
|
this.$emit(MOUNTED_EVENT);
|
||||||
|
this.setInit(this.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -69,12 +89,18 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
inputLabel() {
|
inputLabel() {
|
||||||
|
if (!this.showLabel) return null;
|
||||||
return this.tagSelector ? "Tags" : "Categories";
|
return this.tagSelector ? "Tags" : "Categories";
|
||||||
},
|
},
|
||||||
activeItems() {
|
activeItems() {
|
||||||
if (this.tagSelector) return this.$store.getters.getAllTags;
|
let ItemObjects = [];
|
||||||
|
if (this.tagSelector) ItemObjects = this.$store.getters.getAllTags;
|
||||||
else {
|
else {
|
||||||
return this.$store.getters.getAllCategories;
|
ItemObjects = this.$store.getters.getAllCategories;
|
||||||
|
}
|
||||||
|
if (this.returnObject) return ItemObjects;
|
||||||
|
else {
|
||||||
|
return ItemObjects.map(x => x.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
flat() {
|
flat() {
|
||||||
|
@ -91,6 +117,10 @@ export default {
|
||||||
removeByIndex(index) {
|
removeByIndex(index) {
|
||||||
this.selected.splice(index, 1);
|
this.selected.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
pushToItem(createdItem) {
|
||||||
|
createdItem = this.returnObject ? createdItem : createdItem.name;
|
||||||
|
this.selected.push(createdItem);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -114,60 +114,21 @@
|
||||||
<BulkAdd @bulk-data="appendIngredients" />
|
<BulkAdd @bulk-data="appendIngredients" />
|
||||||
|
|
||||||
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
|
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
|
||||||
<v-combobox
|
<CategoryTagSelector
|
||||||
dense
|
:return-object="false"
|
||||||
multiple
|
|
||||||
chips
|
|
||||||
item-color="secondary"
|
|
||||||
deletable-chips
|
|
||||||
v-model="value.recipeCategory"
|
v-model="value.recipeCategory"
|
||||||
hide-selected
|
:show-add="true"
|
||||||
:items="allCategories"
|
:show-label="false"
|
||||||
text="name"
|
/>
|
||||||
:search-input.sync="categoriesSearchInput"
|
|
||||||
@change="categoriesSearchInput = ''"
|
|
||||||
>
|
|
||||||
<template v-slot:selection="data">
|
|
||||||
<v-chip
|
|
||||||
class="ma-1"
|
|
||||||
:input-value="data.selected"
|
|
||||||
close
|
|
||||||
@click:close="removeCategory(data.index)"
|
|
||||||
label
|
|
||||||
color="accent"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
{{ data.item }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-combobox>
|
|
||||||
|
|
||||||
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2>
|
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2>
|
||||||
<v-combobox
|
<CategoryTagSelector
|
||||||
dense
|
:return-object="false"
|
||||||
multiple
|
|
||||||
chips
|
|
||||||
deletable-chips
|
|
||||||
v-model="value.tags"
|
v-model="value.tags"
|
||||||
hide-selected
|
:show-add="true"
|
||||||
:items="allTags"
|
:tag-selector="true"
|
||||||
:search-input.sync="tagsSearchInput"
|
:show-label="false"
|
||||||
@change="tagssSearchInput = ''"
|
/>
|
||||||
>
|
|
||||||
<template v-slot:selection="data">
|
|
||||||
<v-chip
|
|
||||||
class="ma-1"
|
|
||||||
:input-value="data.selected"
|
|
||||||
close
|
|
||||||
label
|
|
||||||
@click:close="removeTags(data.index)"
|
|
||||||
color="accent"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
{{ data.item }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-combobox>
|
|
||||||
|
|
||||||
<h2 class="my-4">{{ $t("recipe.notes") }}</h2>
|
<h2 class="my-4">{{ $t("recipe.notes") }}</h2>
|
||||||
<v-card
|
<v-card
|
||||||
|
@ -265,11 +226,13 @@ import { api } from "@/api";
|
||||||
import utils from "@/utils";
|
import utils from "@/utils";
|
||||||
import BulkAdd from "./BulkAdd";
|
import BulkAdd from "./BulkAdd";
|
||||||
import ExtrasEditor from "./ExtrasEditor";
|
import ExtrasEditor from "./ExtrasEditor";
|
||||||
|
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
BulkAdd,
|
BulkAdd,
|
||||||
ExtrasEditor,
|
ExtrasEditor,
|
||||||
draggable,
|
draggable,
|
||||||
|
CategoryTagSelector,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: Object,
|
value: Object,
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
:dense="false"
|
:dense="false"
|
||||||
v-model="groupSettings.categories"
|
v-model="groupSettings.categories"
|
||||||
:return-object="true"
|
:return-object="true"
|
||||||
|
:show-add="true"
|
||||||
:hint="
|
:hint="
|
||||||
$t(
|
$t(
|
||||||
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
|
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from mealie.db.models.model_base import SqlAlchemyBase
|
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
|
from mealie.db.models.model_base import SqlAlchemyBase
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy.orm import validates
|
from sqlalchemy.orm import validates
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class Tag(SqlAlchemyBase):
|
||||||
assert name != ""
|
assert name != ""
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def __init__(self, name) -> None:
|
def __init__(self, name, session=None) -> None:
|
||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.category import RecipeTagResponse
|
from mealie.schema.category import RecipeTagResponse, TagIn
|
||||||
from mealie.schema.snackbar import SnackResponse
|
from mealie.schema.snackbar import SnackResponse
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
@ -19,6 +19,14 @@ async def get_all_recipe_tags(session: Session = Depends(generate_session)):
|
||||||
""" Returns a list of available tags in the database """
|
""" Returns a list of available tags in the database """
|
||||||
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
async def create_recipe_tag(
|
||||||
|
tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
|
||||||
|
):
|
||||||
|
""" Creates a Tag in the database """
|
||||||
|
|
||||||
|
return db.tags.create(session, tag.dict())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tag}", response_model=RecipeTagResponse)
|
@router.get("/{tag}", response_model=RecipeTagResponse)
|
||||||
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
|
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
|
||||||
|
|
|
@ -23,6 +23,10 @@ class RecipeCategoryResponse(CategoryBase):
|
||||||
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
|
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
|
||||||
|
|
||||||
|
|
||||||
|
class TagIn(CategoryIn):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TagBase(CategoryBase):
|
class TagBase(CategoryBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue