mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-27 16:24:31 -04:00
feat: Simplify Default Layout Logic and Add Household.name To Cookbooks API (#6243)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -100,9 +100,7 @@ import type { SideBarLink } from "~/types/application-types";
|
|||||||
import { useAppInfo } from "~/composables/api";
|
import { useAppInfo } from "~/composables/api";
|
||||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup() {
|
setup() {
|
||||||
@@ -116,12 +114,8 @@ export default defineNuxtComponent({
|
|||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const cookbookPreferences = useCookbookPreferences();
|
const cookbookPreferences = useCookbookPreferences();
|
||||||
|
|
||||||
const ownCookbookStore = useCookbookStore(i18n);
|
const ownCookbookStore = useCookbookStore(i18n);
|
||||||
const ownHouseholdStore = useHouseholdStore(i18n);
|
|
||||||
|
|
||||||
const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({});
|
const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({});
|
||||||
const publicHouseholdStoreCache = ref<Record<string, ReturnType<typeof usePublicHouseholdStore>>>({});
|
|
||||||
|
|
||||||
function getPublicCookbookStore(slug: string) {
|
function getPublicCookbookStore(slug: string) {
|
||||||
if (!publicCookbookStoreCache.value[slug]) {
|
if (!publicCookbookStoreCache.value[slug]) {
|
||||||
@@ -130,13 +124,6 @@ export default defineNuxtComponent({
|
|||||||
return publicCookbookStoreCache.value[slug];
|
return publicCookbookStoreCache.value[slug];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPublicHouseholdStore(slug: string) {
|
|
||||||
if (!publicHouseholdStoreCache.value[slug]) {
|
|
||||||
publicHouseholdStoreCache.value[slug] = usePublicHouseholdStore(slug, i18n);
|
|
||||||
}
|
|
||||||
return publicHouseholdStoreCache.value[slug];
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookbooks = computed(() => {
|
const cookbooks = computed(() => {
|
||||||
if (isOwnGroup.value) {
|
if (isOwnGroup.value) {
|
||||||
return ownCookbookStore.store.value;
|
return ownCookbookStore.store.value;
|
||||||
@@ -148,24 +135,6 @@ export default defineNuxtComponent({
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const households = computed(() => {
|
|
||||||
if (isOwnGroup.value) {
|
|
||||||
return ownHouseholdStore.store.value;
|
|
||||||
}
|
|
||||||
else if (groupSlug.value) {
|
|
||||||
const publicStore = getPublicHouseholdStore(groupSlug.value);
|
|
||||||
return unref(publicStore.store);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const householdsById = computed(() => {
|
|
||||||
return households.value.reduce((acc, household) => {
|
|
||||||
acc[household.id] = household;
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: string]: HouseholdSummary });
|
|
||||||
});
|
|
||||||
|
|
||||||
const appInfo = useAppInfo();
|
const appInfo = useAppInfo();
|
||||||
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
||||||
|
|
||||||
@@ -197,11 +166,8 @@ export default defineNuxtComponent({
|
|||||||
const ownLinks: SideBarLink[] = [];
|
const ownLinks: SideBarLink[] = [];
|
||||||
const links: SideBarLink[] = [];
|
const links: SideBarLink[] = [];
|
||||||
const cookbooksByHousehold = sortedCookbooks.reduce((acc, cookbook) => {
|
const cookbooksByHousehold = sortedCookbooks.reduce((acc, cookbook) => {
|
||||||
const householdName = householdsById.value[cookbook.householdId]?.name || "";
|
const householdName = cookbook.household?.name || "";
|
||||||
if (!acc[householdName]) {
|
(acc[householdName] ||= []).push(cookbook);
|
||||||
acc[householdName] = [];
|
|
||||||
}
|
|
||||||
acc[householdName].push(cookbook);
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, ReadCookBook[]>);
|
}, {} as Record<string, ReadCookBook[]>);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export type LogicalOperator = "AND" | "OR";
|
|||||||
export type RelationalKeyword = "IS" | "IS NOT" | "IN" | "NOT IN" | "CONTAINS ALL" | "LIKE" | "NOT LIKE";
|
export type RelationalKeyword = "IS" | "IS NOT" | "IN" | "NOT IN" | "CONTAINS ALL" | "LIKE" | "NOT LIKE";
|
||||||
export type RelationalOperator = "=" | "<>" | ">" | "<" | ">=" | "<=";
|
export type RelationalOperator = "=" | "<>" | ">" | "<" | ">=" | "<=";
|
||||||
|
|
||||||
|
export interface CookbookHousehold {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
export interface CreateCookBook {
|
export interface CreateCookBook {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -28,6 +32,7 @@ export interface ReadCookBook {
|
|||||||
householdId: string;
|
householdId: string;
|
||||||
id: string;
|
id: string;
|
||||||
queryFilter?: QueryFilterJSON;
|
queryFilter?: QueryFilterJSON;
|
||||||
|
household?: CookbookHousehold | null;
|
||||||
}
|
}
|
||||||
export interface QueryFilterJSON {
|
export interface QueryFilterJSON {
|
||||||
parts?: QueryFilterJSONPart[];
|
parts?: QueryFilterJSONPart[];
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface OpenAIIngredient {
|
export interface OpenAIIngredient {
|
||||||
input: string;
|
|
||||||
confidence?: number | null;
|
|
||||||
quantity?: number | null;
|
quantity?: number | null;
|
||||||
unit?: string | null;
|
unit?: string | null;
|
||||||
food?: string | null;
|
food?: string | null;
|
||||||
|
|||||||
2
frontend/types/components.d.ts
vendored
2
frontend/types/components.d.ts
vendored
@@ -3,6 +3,7 @@ import type AdvancedOnly from "@/components/global/AdvancedOnly.vue";
|
|||||||
import type AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
import type AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
||||||
import type AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
import type AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
||||||
import type AppLoader from "@/components/global/AppLoader.vue";
|
import type AppLoader from "@/components/global/AppLoader.vue";
|
||||||
|
import type AppLogo from "@/components/global/AppLogo.vue";
|
||||||
import type AppToolbar from "@/components/global/AppToolbar.vue";
|
import type AppToolbar from "@/components/global/AppToolbar.vue";
|
||||||
import type AutoForm from "@/components/global/AutoForm.vue";
|
import type AutoForm from "@/components/global/AutoForm.vue";
|
||||||
import type BannerExperimental from "@/components/global/BannerExperimental.vue";
|
import type BannerExperimental from "@/components/global/BannerExperimental.vue";
|
||||||
@@ -43,6 +44,7 @@ declare module "vue" {
|
|||||||
AppButtonCopy: typeof AppButtonCopy;
|
AppButtonCopy: typeof AppButtonCopy;
|
||||||
AppButtonUpload: typeof AppButtonUpload;
|
AppButtonUpload: typeof AppButtonUpload;
|
||||||
AppLoader: typeof AppLoader;
|
AppLoader: typeof AppLoader;
|
||||||
|
AppLogo: typeof AppLogo;
|
||||||
AppToolbar: typeof AppToolbar;
|
AppToolbar: typeof AppToolbar;
|
||||||
AutoForm: typeof AutoForm;
|
AutoForm: typeof AutoForm;
|
||||||
BannerExperimental: typeof BannerExperimental;
|
BannerExperimental: typeof BannerExperimental;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# This file is auto-generated by gen_schema_exports.py
|
# This file is auto-generated by gen_schema_exports.py
|
||||||
from .cookbook import CookBookPagination, CreateCookBook, ReadCookBook, SaveCookBook, UpdateCookBook
|
from .cookbook import CookbookHousehold, CookBookPagination, CreateCookBook, ReadCookBook, SaveCookBook, UpdateCookBook
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CookBookPagination",
|
"CookBookPagination",
|
||||||
|
"CookbookHousehold",
|
||||||
"CreateCookBook",
|
"CreateCookBook",
|
||||||
"ReadCookBook",
|
"ReadCookBook",
|
||||||
"SaveCookBook",
|
"SaveCookBook",
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ from typing import Annotated
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from pydantic import UUID4, ConfigDict, Field, ValidationInfo, field_validator
|
from pydantic import UUID4, ConfigDict, Field, ValidationInfo, field_validator
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
from sqlalchemy.orm.interfaces import LoaderOption
|
||||||
|
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.db.models.household.cookbook import CookBook
|
||||||
from mealie.db.models.recipe import RecipeModel
|
from mealie.db.models.recipe import RecipeModel
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema.response.pagination import PaginationBase
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
@@ -13,6 +16,12 @@ from mealie.schema.response.query_filter import QueryFilterBuilder, QueryFilterJ
|
|||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class CookbookHousehold(MealieModel):
|
||||||
|
id: UUID4
|
||||||
|
name: str
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
class CreateCookBook(MealieModel):
|
class CreateCookBook(MealieModel):
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
@@ -62,6 +71,7 @@ class UpdateCookBook(SaveCookBook):
|
|||||||
|
|
||||||
class ReadCookBook(UpdateCookBook):
|
class ReadCookBook(UpdateCookBook):
|
||||||
query_filter: Annotated[QueryFilterJSON, Field(validate_default=True)] = None # type: ignore
|
query_filter: Annotated[QueryFilterJSON, Field(validate_default=True)] = None # type: ignore
|
||||||
|
household: CookbookHousehold | None = None
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@@ -80,6 +90,10 @@ class ReadCookBook(UpdateCookBook):
|
|||||||
logger.exception(f"Invalid query filter string: {query_filter_string}")
|
logger.exception(f"Invalid query filter string: {query_filter_string}")
|
||||||
return QueryFilterJSON()
|
return QueryFilterJSON()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def loader_options(cls) -> list[LoaderOption]:
|
||||||
|
return [joinedload(CookBook.household)]
|
||||||
|
|
||||||
|
|
||||||
class CookBookPagination(PaginationBase):
|
class CookBookPagination(PaginationBase):
|
||||||
items: list[ReadCookBook]
|
items: list[ReadCookBook]
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ def test_get_one_cookbook(
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
cookbook_data = response.json()
|
cookbook_data = response.json()
|
||||||
assert cookbook_data["id"] == str(cookbook.id)
|
assert cookbook_data["id"] == str(cookbook.id)
|
||||||
|
assert cookbook_data["household"]["name"] == household.name
|
||||||
|
|
||||||
|
|
||||||
def test_get_cookbooks_with_recipes(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
|
def test_get_cookbooks_with_recipes(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ def test_read_cookbook(
|
|||||||
use_other_household: bool,
|
use_other_household: bool,
|
||||||
):
|
):
|
||||||
sample = random.choice(cookbooks)
|
sample = random.choice(cookbooks)
|
||||||
|
household = unique_user.repos.households.get_one(sample.data["household_id"])
|
||||||
|
assert household
|
||||||
|
|
||||||
if use_other_household:
|
if use_other_household:
|
||||||
headers = h2_user.token
|
headers = h2_user.token
|
||||||
else:
|
else:
|
||||||
@@ -104,6 +107,8 @@ def test_read_cookbook(
|
|||||||
assert page_data["slug"] == sample.slug
|
assert page_data["slug"] == sample.slug
|
||||||
assert page_data["name"] == sample.name
|
assert page_data["name"] == sample.name
|
||||||
assert page_data["groupId"] == str(unique_user.group_id)
|
assert page_data["groupId"] == str(unique_user.group_id)
|
||||||
|
assert page_data["householdId"] == str(unique_user.household_id)
|
||||||
|
assert page_data["household"]["name"] == household.name
|
||||||
|
|
||||||
|
|
||||||
def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
|
def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
|
||||||
|
|||||||
Reference in New Issue
Block a user