mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: Filter Recipes By Household (and a ton of bug fixes) (#4207)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
parent
2a6922a85c
commit
7c274de778
@ -69,50 +69,52 @@
|
|||||||
@toggle-dense-view="toggleMobileCards()"
|
@toggle-dense-view="toggleMobileCards()"
|
||||||
/>
|
/>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<div v-if="recipes" class="mt-2">
|
<div v-if="recipes && ready">
|
||||||
<v-row v-if="!useMobileCards">
|
<div class="mt-2">
|
||||||
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
<v-row v-if="!useMobileCards">
|
||||||
<v-lazy>
|
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
||||||
<RecipeCard
|
<v-lazy>
|
||||||
:name="recipe.name"
|
<RecipeCard
|
||||||
:description="recipe.description"
|
:name="recipe.name"
|
||||||
:slug="recipe.slug"
|
:description="recipe.description"
|
||||||
:rating="recipe.rating"
|
:slug="recipe.slug"
|
||||||
:image="recipe.image"
|
:rating="recipe.rating"
|
||||||
:tags="recipe.tags"
|
:image="recipe.image"
|
||||||
:recipe-id="recipe.id"
|
:tags="recipe.tags"
|
||||||
/>
|
:recipe-id="recipe.id"
|
||||||
</v-lazy>
|
/>
|
||||||
</v-col>
|
</v-lazy>
|
||||||
</v-row>
|
</v-col>
|
||||||
<v-row v-else dense>
|
</v-row>
|
||||||
<v-col
|
<v-row v-else dense>
|
||||||
v-for="recipe in recipes"
|
<v-col
|
||||||
:key="recipe.name"
|
v-for="recipe in recipes"
|
||||||
cols="12"
|
:key="recipe.name"
|
||||||
:sm="singleColumn ? '12' : '12'"
|
cols="12"
|
||||||
:md="singleColumn ? '12' : '6'"
|
:sm="singleColumn ? '12' : '12'"
|
||||||
:lg="singleColumn ? '12' : '4'"
|
:md="singleColumn ? '12' : '6'"
|
||||||
:xl="singleColumn ? '12' : '3'"
|
:lg="singleColumn ? '12' : '4'"
|
||||||
>
|
:xl="singleColumn ? '12' : '3'"
|
||||||
<v-lazy>
|
>
|
||||||
<RecipeCardMobile
|
<v-lazy>
|
||||||
:name="recipe.name"
|
<RecipeCardMobile
|
||||||
:description="recipe.description"
|
:name="recipe.name"
|
||||||
:slug="recipe.slug"
|
:description="recipe.description"
|
||||||
:rating="recipe.rating"
|
:slug="recipe.slug"
|
||||||
:image="recipe.image"
|
:rating="recipe.rating"
|
||||||
:tags="recipe.tags"
|
:image="recipe.image"
|
||||||
:recipe-id="recipe.id"
|
:tags="recipe.tags"
|
||||||
/>
|
:recipe-id="recipe.id"
|
||||||
</v-lazy>
|
/>
|
||||||
</v-col>
|
</v-lazy>
|
||||||
</v-row>
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
<v-card v-intersect="infiniteScroll"></v-card>
|
||||||
|
<v-fade-transition>
|
||||||
|
<AppLoader v-if="loading" :loading="loading" />
|
||||||
|
</v-fade-transition>
|
||||||
</div>
|
</div>
|
||||||
<v-card v-intersect="infiniteScroll"></v-card>
|
|
||||||
<v-fade-transition>
|
|
||||||
<AppLoader v-if="loading" :loading="loading" />
|
|
||||||
</v-fade-transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -223,36 +225,42 @@ export default defineComponent({
|
|||||||
|
|
||||||
const queryFilter = computed(() => {
|
const queryFilter = computed(() => {
|
||||||
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
||||||
return preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
|
const orderByFilter = preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
|
||||||
|
|
||||||
|
if (props.query.queryFilter && orderByFilter) {
|
||||||
|
return `(${props.query.queryFilter}) AND ${orderByFilter}`;
|
||||||
|
} else if (props.query.queryFilter) {
|
||||||
|
return props.query.queryFilter;
|
||||||
|
} else {
|
||||||
|
return orderByFilter;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchRecipes(pageCount = 1) {
|
async function fetchRecipes(pageCount = 1) {
|
||||||
return await fetchMore(
|
return await fetchMore(
|
||||||
page.value,
|
page.value,
|
||||||
// we double-up the first call to avoid a bug with large screens that render the entire first page without scrolling, preventing additional loading
|
|
||||||
perPage * pageCount,
|
perPage * pageCount,
|
||||||
props.query?.orderBy || preferences.value.orderBy,
|
props.query?.orderBy || preferences.value.orderBy,
|
||||||
props.query?.orderDirection || preferences.value.orderDirection,
|
props.query?.orderDirection || preferences.value.orderDirection,
|
||||||
props.query,
|
props.query,
|
||||||
// filter out recipes that have a null value for the property we're sorting by
|
// we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
|
||||||
queryFilter.value
|
queryFilter.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.query) {
|
await initRecipes();
|
||||||
await initRecipes();
|
ready.value = true;
|
||||||
ready.value = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastQuery: string | undefined;
|
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||||
watch(
|
watch(
|
||||||
() => props.query,
|
() => props.query,
|
||||||
async (newValue: RecipeSearchQuery | undefined) => {
|
async (newValue: RecipeSearchQuery | undefined) => {
|
||||||
const newValueString = JSON.stringify(newValue)
|
const newValueString = JSON.stringify(newValue)
|
||||||
if (newValue && (!ready.value || lastQuery !== newValueString)) {
|
if (lastQuery !== newValueString) {
|
||||||
lastQuery = newValueString;
|
lastQuery = newValueString;
|
||||||
|
ready.value = false;
|
||||||
await initRecipes();
|
await initRecipes();
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
@ -261,8 +269,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function initRecipes() {
|
async function initRecipes() {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
const newRecipes = await fetchRecipes(2);
|
hasMore.value = true;
|
||||||
if (!newRecipes.length) {
|
|
||||||
|
// we double-up the first call to avoid a bug with large screens that render
|
||||||
|
// the entire first page without scrolling, preventing additional loading
|
||||||
|
const newRecipes = await fetchRecipes(page.value + 1);
|
||||||
|
if (newRecipes.length < perPage) {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +286,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const infiniteScroll = useThrottleFn(() => {
|
const infiniteScroll = useThrottleFn(() => {
|
||||||
useAsync(async () => {
|
useAsync(async () => {
|
||||||
if (!ready.value || !hasMore.value || loading.value) {
|
if (!hasMore.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,9 +294,10 @@ export default defineComponent({
|
|||||||
page.value = page.value + 1;
|
page.value = page.value + 1;
|
||||||
|
|
||||||
const newRecipes = await fetchRecipes();
|
const newRecipes = await fetchRecipes();
|
||||||
if (!newRecipes.length) {
|
if (newRecipes.length < perPage) {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
} else {
|
}
|
||||||
|
if (newRecipes.length) {
|
||||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,6 +392,7 @@ export default defineComponent({
|
|||||||
displayTitleIcon,
|
displayTitleIcon,
|
||||||
EVENTS,
|
EVENTS,
|
||||||
infiniteScroll,
|
infiniteScroll,
|
||||||
|
ready,
|
||||||
loading,
|
loading,
|
||||||
navigateRandom,
|
navigateRandom,
|
||||||
preferences,
|
preferences,
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
v-model="selected"
|
v-model="selected"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
show-select
|
show-select
|
||||||
|
sort-by="dateAdded"
|
||||||
|
sort-desc
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:items="recipes"
|
:items="recipes"
|
||||||
:items-per-page="15"
|
:items-per-page="15"
|
||||||
@ -39,6 +41,9 @@
|
|||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
<template #item.dateAdded="{ item }">
|
||||||
|
{{ formatDate(item.dateAdded) }}
|
||||||
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -132,6 +137,14 @@ export default defineComponent({
|
|||||||
return hdrs;
|
return hdrs;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function formatDate(date: string) {
|
||||||
|
try {
|
||||||
|
return i18n.d(Date.parse(date), "medium");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Group Members
|
// Group Members
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
@ -160,6 +173,7 @@ export default defineComponent({
|
|||||||
groupSlug,
|
groupSlug,
|
||||||
setValue,
|
setValue,
|
||||||
headers,
|
headers,
|
||||||
|
formatDate,
|
||||||
members,
|
members,
|
||||||
getMember,
|
getMember,
|
||||||
};
|
};
|
||||||
|
@ -53,6 +53,14 @@
|
|||||||
{{ $t("general.foods") }}
|
{{ $t("general.foods") }}
|
||||||
</SearchFilter>
|
</SearchFilter>
|
||||||
|
|
||||||
|
<!-- Household Filter -->
|
||||||
|
<SearchFilter v-if="households.length > 1" v-model="selectedHouseholds" :items="households" radio>
|
||||||
|
<v-icon left>
|
||||||
|
{{ $globals.icons.household }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("household.households") }}
|
||||||
|
</SearchFilter>
|
||||||
|
|
||||||
<!-- Sort Options -->
|
<!-- Sort Options -->
|
||||||
<v-menu offset-y nudge-bottom="3">
|
<v-menu offset-y nudge-bottom="3">
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
@ -142,17 +150,25 @@ import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref,
|
|||||||
import { watchDebounced } from "@vueuse/shared";
|
import { watchDebounced } from "@vueuse/shared";
|
||||||
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
|
import {
|
||||||
|
useCategoryStore,
|
||||||
|
usePublicCategoryStore,
|
||||||
|
useFoodStore,
|
||||||
|
usePublicFoodStore,
|
||||||
|
useHouseholdStore,
|
||||||
|
usePublicHouseholdStore,
|
||||||
|
useTagStore,
|
||||||
|
usePublicTagStore,
|
||||||
|
useToolStore,
|
||||||
|
usePublicToolStore,
|
||||||
|
} from "~/composables/store";
|
||||||
import { useUserSearchQuerySession } from "~/composables/use-users/preferences";
|
import { useUserSearchQuerySession } from "~/composables/use-users/preferences";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||||
import { usePublicCategoryStore } from "~/composables/store/use-category-store";
|
import { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
import { usePublicFoodStore } from "~/composables/store/use-food-store";
|
|
||||||
import { usePublicTagStore } from "~/composables/store/use-tag-store";
|
|
||||||
import { usePublicToolStore } from "~/composables/store/use-tool-store";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { SearchFilter, RecipeCardSection },
|
components: { SearchFilter, RecipeCardSection },
|
||||||
@ -186,6 +202,9 @@ export default defineComponent({
|
|||||||
const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||||
const selectedFoods = ref<IngredientFood[]>([]);
|
const selectedFoods = ref<IngredientFood[]>([]);
|
||||||
|
|
||||||
|
const households = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
|
||||||
|
const selectedHouseholds = ref([] as NoUndefinedField<HouseholdSummary>[]);
|
||||||
|
|
||||||
const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||||
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
||||||
|
|
||||||
@ -199,6 +218,7 @@ export default defineComponent({
|
|||||||
search: state.value.search ? state.value.search : "",
|
search: state.value.search ? state.value.search : "",
|
||||||
categories: toIDArray(selectedCategories.value),
|
categories: toIDArray(selectedCategories.value),
|
||||||
foods: toIDArray(selectedFoods.value),
|
foods: toIDArray(selectedFoods.value),
|
||||||
|
households: toIDArray(selectedHouseholds.value),
|
||||||
tags: toIDArray(selectedTags.value),
|
tags: toIDArray(selectedTags.value),
|
||||||
tools: toIDArray(selectedTools.value),
|
tools: toIDArray(selectedTools.value),
|
||||||
requireAllCategories: state.value.requireAllCategories,
|
requireAllCategories: state.value.requireAllCategories,
|
||||||
@ -239,10 +259,9 @@ export default defineComponent({
|
|||||||
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
||||||
selectedCategories.value = [];
|
selectedCategories.value = [];
|
||||||
selectedFoods.value = [];
|
selectedFoods.value = [];
|
||||||
|
selectedHouseholds.value = [];
|
||||||
selectedTags.value = [];
|
selectedTags.value = [];
|
||||||
selectedTools.value = [];
|
selectedTools.value = [];
|
||||||
|
|
||||||
search();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleOrderDirection() {
|
function toggleOrderDirection() {
|
||||||
@ -280,6 +299,7 @@ export default defineComponent({
|
|||||||
search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search,
|
search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search,
|
||||||
orderBy: passedQuery.value.orderBy === queryDefaults.orderBy ? undefined : passedQuery.value.orderBy,
|
orderBy: passedQuery.value.orderBy === queryDefaults.orderBy ? undefined : passedQuery.value.orderBy,
|
||||||
orderDirection: passedQuery.value.orderDirection === queryDefaults.orderDirection ? undefined : passedQuery.value.orderDirection,
|
orderDirection: passedQuery.value.orderDirection === queryDefaults.orderDirection ? undefined : passedQuery.value.orderDirection,
|
||||||
|
households: !passedQuery.value.households?.length || passedQuery.value.households?.length === households.store.value.length ? undefined : passedQuery.value.households,
|
||||||
requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined,
|
requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined,
|
||||||
requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
|
requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
|
||||||
requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
|
requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
|
||||||
@ -361,13 +381,10 @@ export default defineComponent({
|
|||||||
watch(
|
watch(
|
||||||
() => route.value.query,
|
() => route.value.query,
|
||||||
() => {
|
() => {
|
||||||
if (state.value.ready) {
|
if (!Object.keys(route.value.query).length) {
|
||||||
hydrateSearch();
|
reset();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async function hydrateSearch() {
|
async function hydrateSearch() {
|
||||||
@ -423,9 +440,9 @@ export default defineComponent({
|
|||||||
if (query.categories?.length) {
|
if (query.categories?.length) {
|
||||||
promises.push(
|
promises.push(
|
||||||
waitUntilAndExecute(
|
waitUntilAndExecute(
|
||||||
() => categories.items.value.length > 0,
|
() => categories.store.value.length > 0,
|
||||||
() => {
|
() => {
|
||||||
const result = categories.items.value.filter((item) =>
|
const result = categories.store.value.filter((item) =>
|
||||||
(query.categories as string[]).includes(item.id as string)
|
(query.categories as string[]).includes(item.id as string)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -440,9 +457,9 @@ export default defineComponent({
|
|||||||
if (query.tags?.length) {
|
if (query.tags?.length) {
|
||||||
promises.push(
|
promises.push(
|
||||||
waitUntilAndExecute(
|
waitUntilAndExecute(
|
||||||
() => tags.items.value.length > 0,
|
() => tags.store.value.length > 0,
|
||||||
() => {
|
() => {
|
||||||
const result = tags.items.value.filter((item) => (query.tags as string[]).includes(item.id as string));
|
const result = tags.store.value.filter((item) => (query.tags as string[]).includes(item.id as string));
|
||||||
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -454,9 +471,9 @@ export default defineComponent({
|
|||||||
if (query.tools?.length) {
|
if (query.tools?.length) {
|
||||||
promises.push(
|
promises.push(
|
||||||
waitUntilAndExecute(
|
waitUntilAndExecute(
|
||||||
() => tools.items.value.length > 0,
|
() => tools.store.value.length > 0,
|
||||||
() => {
|
() => {
|
||||||
const result = tools.items.value.filter((item) => (query.tools as string[]).includes(item.id));
|
const result = tools.store.value.filter((item) => (query.tools as string[]).includes(item.id));
|
||||||
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
|
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -469,13 +486,13 @@ export default defineComponent({
|
|||||||
promises.push(
|
promises.push(
|
||||||
waitUntilAndExecute(
|
waitUntilAndExecute(
|
||||||
() => {
|
() => {
|
||||||
if (foods.foods.value) {
|
if (foods.store.value) {
|
||||||
return foods.foods.value.length > 0;
|
return foods.store.value.length > 0;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
const result = foods.foods.value?.filter((item) => (query.foods as string[]).includes(item.id));
|
const result = foods.store.value?.filter((item) => (query.foods as string[]).includes(item.id));
|
||||||
selectedFoods.value = result ?? [];
|
selectedFoods.value = result ?? [];
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -484,6 +501,25 @@ export default defineComponent({
|
|||||||
selectedFoods.value = [];
|
selectedFoods.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.households?.length) {
|
||||||
|
promises.push(
|
||||||
|
waitUntilAndExecute(
|
||||||
|
() => {
|
||||||
|
if (households.store.value) {
|
||||||
|
return households.store.value.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const result = households.store.value?.filter((item) => (query.households as string[]).includes(item.id));
|
||||||
|
selectedHouseholds.value = result as NoUndefinedField<HouseholdSummary>[] ?? [];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedHouseholds.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -515,6 +551,7 @@ export default defineComponent({
|
|||||||
() => state.value.orderDirection,
|
() => state.value.orderDirection,
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
selectedFoods,
|
selectedFoods,
|
||||||
|
selectedHouseholds,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
],
|
],
|
||||||
@ -533,10 +570,11 @@ export default defineComponent({
|
|||||||
search,
|
search,
|
||||||
reset,
|
reset,
|
||||||
state,
|
state,
|
||||||
categories: categories.items as unknown as NoUndefinedField<RecipeCategory>[],
|
categories: categories.store as unknown as NoUndefinedField<RecipeCategory>[],
|
||||||
tags: tags.items as unknown as NoUndefinedField<RecipeTag>[],
|
tags: tags.store as unknown as NoUndefinedField<RecipeTag>[],
|
||||||
foods: foods.foods,
|
foods: foods.store,
|
||||||
tools: tools.items as unknown as NoUndefinedField<RecipeTool>[],
|
tools: tools.store as unknown as NoUndefinedField<RecipeTool>[],
|
||||||
|
households: households.store as unknown as NoUndefinedField<HouseholdSummary>[],
|
||||||
|
|
||||||
sortable,
|
sortable,
|
||||||
toggleOrderDirection,
|
toggleOrderDirection,
|
||||||
@ -545,6 +583,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
selectedFoods,
|
selectedFoods,
|
||||||
|
selectedHouseholds,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
appendRecipes,
|
appendRecipes,
|
||||||
|
@ -289,11 +289,11 @@ export default defineComponent({
|
|||||||
createAssignFood,
|
createAssignFood,
|
||||||
unitAutocomplete,
|
unitAutocomplete,
|
||||||
createAssignUnit,
|
createAssignUnit,
|
||||||
foods: foodStore.foods,
|
foods: foodStore.store,
|
||||||
foodSearch,
|
foodSearch,
|
||||||
toggleTitle,
|
toggleTitle,
|
||||||
unitActions: unitStore.actions,
|
unitActions: unitStore.actions,
|
||||||
units: unitStore.units,
|
units: unitStore.store,
|
||||||
unitSearch,
|
unitSearch,
|
||||||
validators,
|
validators,
|
||||||
workingUnitData: unitsData.data,
|
workingUnitData: unitsData.data,
|
||||||
|
@ -135,7 +135,7 @@ export default defineComponent({
|
|||||||
await store.actions.createOne({ ...state });
|
await store.actions.createOne({ ...state });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = store.items.value.find((item) => item.name === state.name);
|
const newItem = store.store.value.find((item) => item.name === state.name);
|
||||||
|
|
||||||
context.emit(CREATED_ITEM_EVENT, newItem);
|
context.emit(CREATED_ITEM_EVENT, newItem);
|
||||||
dialog.value = false;
|
dialog.value = false;
|
||||||
|
@ -127,9 +127,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
const items = computed(() => {
|
const items = computed(() => {
|
||||||
if (!props.returnObject) {
|
if (!props.returnObject) {
|
||||||
return store.items.value.map((item) => item.name);
|
return store.store.value.map((item) => item.name);
|
||||||
}
|
}
|
||||||
return store.items.value;
|
return store.store.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
function removeByIndex(index: number) {
|
function removeByIndex(index: number) {
|
||||||
|
@ -105,7 +105,7 @@ export default defineComponent({
|
|||||||
const recipeHousehold = ref<HouseholdSummary>();
|
const recipeHousehold = ref<HouseholdSummary>();
|
||||||
if (user) {
|
if (user) {
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
userApi.groups.fetchHousehold(props.recipe.householdId).then(({ data }) => {
|
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
||||||
recipeHousehold.value = data || undefined;
|
recipeHousehold.value = data || undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -11,28 +11,43 @@
|
|||||||
<v-card width="400">
|
<v-card width="400">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field v-model="state.search" class="mb-2" hide-details dense :label="$tc('search.search')" clearable />
|
<v-text-field v-model="state.search" class="mb-2" hide-details dense :label="$tc('search.search')" clearable />
|
||||||
<v-switch
|
<div class="d-flex py-4">
|
||||||
v-if="requireAll != undefined"
|
<v-switch
|
||||||
v-model="requireAllValue"
|
v-if="requireAll != undefined"
|
||||||
dense
|
v-model="requireAllValue"
|
||||||
small
|
dense
|
||||||
:label="`${requireAll ? $tc('search.has-all') : $tc('search.has-any')}`"
|
small
|
||||||
>
|
hide-details
|
||||||
</v-switch>
|
class="my-auto"
|
||||||
|
:label="`${requireAll ? $tc('search.has-all') : $tc('search.has-any')}`"
|
||||||
|
/>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="accent"
|
||||||
|
class="mr-2 my-auto"
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
|
{{ $tc("search.clear-selection") }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
<v-card v-if="filtered.length > 0" flat outlined>
|
<v-card v-if="filtered.length > 0" flat outlined>
|
||||||
|
<v-radio-group v-model="selectedRadio" class="ma-0 pa-0">
|
||||||
<v-virtual-scroll :items="filtered" height="300" item-height="51">
|
<v-virtual-scroll :items="filtered" height="300" item-height="51">
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<v-list-item :key="item.id" dense :value="item">
|
<v-list-item :key="item.id" dense :value="item">
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-checkbox v-model="selected" :value="item"></v-checkbox>
|
<v-radio v-if="radio" :value="item" @click="handleRadioClick(item)" />
|
||||||
</v-list-item-action>
|
<v-checkbox v-else v-model="selected" :value="item" />
|
||||||
<v-list-item-content>
|
</v-list-item-action>
|
||||||
<v-list-item-title> {{ item.name }}</v-list-item-title>
|
<v-list-item-content>
|
||||||
</v-list-item-content>
|
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
</template>
|
</template>
|
||||||
</v-virtual-scroll>
|
</v-virtual-scroll>
|
||||||
|
</v-radio-group>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<v-alert type="info" text> {{ $tc('search.no-results') }} </v-alert>
|
<v-alert type="info" text> {{ $tc('search.no-results') }} </v-alert>
|
||||||
@ -65,6 +80,10 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
radio: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@ -86,6 +105,13 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedRadio = computed({
|
||||||
|
get: () => (selected.value.length > 0 ? selected.value[0] : null),
|
||||||
|
set: (value) => {
|
||||||
|
context.emit("input", value ? [value] : []);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
if (!state.search) {
|
if (!state.search) {
|
||||||
return props.items;
|
return props.items;
|
||||||
@ -94,11 +120,26 @@ export default defineComponent({
|
|||||||
return props.items.filter((item) => item.name.toLowerCase().includes(state.search.toLowerCase()));
|
return props.items.filter((item) => item.name.toLowerCase().includes(state.search.toLowerCase()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleRadioClick = (item: SelectableItem) => {
|
||||||
|
if (selectedRadio.value === item) {
|
||||||
|
selectedRadio.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selected.value = [];
|
||||||
|
selectedRadio.value = null;
|
||||||
|
state.search = "";
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requireAllValue,
|
requireAllValue,
|
||||||
state,
|
state,
|
||||||
selected,
|
selected,
|
||||||
|
selectedRadio,
|
||||||
filtered,
|
filtered,
|
||||||
|
handleRadioClick,
|
||||||
|
clearSelection,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -44,6 +44,8 @@
|
|||||||
item-key="id"
|
item-key="id"
|
||||||
:show-select="bulkActions.length > 0"
|
:show-select="bulkActions.length > 0"
|
||||||
:headers="activeHeaders"
|
:headers="activeHeaders"
|
||||||
|
:sort-by="initialSort"
|
||||||
|
:sort-desc="initialSortDesc"
|
||||||
:items="data || []"
|
:items="data || []"
|
||||||
:items-per-page="15"
|
:items-per-page="15"
|
||||||
:search="search"
|
:search="search"
|
||||||
@ -126,6 +128,14 @@ export default defineComponent({
|
|||||||
type: Array as () => BulkAction[],
|
type: Array as () => BulkAction[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
initialSort: {
|
||||||
|
type: String,
|
||||||
|
default: "id",
|
||||||
|
},
|
||||||
|
initialSortDesc: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
// ===========================================================
|
// ===========================================================
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export { useAppInfo } from "./use-app-info";
|
export { useAppInfo } from "./use-app-info";
|
||||||
export { useStaticRoutes } from "./static-routes";
|
export { useStaticRoutes } from "./static-routes";
|
||||||
export { useAdminApi, useUserApi } from "./api-client";
|
export { useAdminApi, usePublicApi, usePublicExploreApi, useUserApi } from "./api-client";
|
||||||
|
3
frontend/composables/partials/types.ts
Normal file
3
frontend/composables/partials/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type BoundT = {
|
||||||
|
id?: string | number | null;
|
||||||
|
};
|
@ -1,18 +1,15 @@
|
|||||||
import { Ref, useAsync } from "@nuxtjs/composition-api";
|
import { Ref, useAsync } from "@nuxtjs/composition-api";
|
||||||
import { useAsyncKey } from "../use-utils";
|
import { useAsyncKey } from "../use-utils";
|
||||||
|
import { BoundT } from "./types";
|
||||||
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||||
import { QueryValue } from "~/lib/api/base/route";
|
import { QueryValue } from "~/lib/api/base/route";
|
||||||
|
|
||||||
type BoundT = {
|
interface ReadOnlyStoreActions<T extends BoundT> {
|
||||||
id?: string | number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PublicStoreActions<T extends BoundT> {
|
|
||||||
getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>;
|
getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>;
|
||||||
refresh(): Promise<void>;
|
refresh(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoreActions<T extends BoundT> extends PublicStoreActions<T> {
|
interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
|
||||||
createOne(createData: T): Promise<T | null>;
|
createOne(createData: T): Promise<T | null>;
|
||||||
updateOne(updateData: T): Promise<T | null>;
|
updateOne(updateData: T): Promise<T | null>;
|
||||||
deleteOne(id: string | number): Promise<T | null>;
|
deleteOne(id: string | number): Promise<T | null>;
|
||||||
@ -20,16 +17,16 @@ interface StoreActions<T extends BoundT> extends PublicStoreActions<T> {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* usePublicStoreActions is a factory function that returns a set of methods
|
* useReadOnlyActions is a factory function that returns a set of methods
|
||||||
* that can be reused to manage the state of a data store without using
|
* that can be reused to manage the state of a data store without using
|
||||||
* Vuex. This is primarily used for basic GET/GETALL operations that required
|
* Vuex. This is primarily used for basic GET/GETALL operations that required
|
||||||
* a lot of refreshing hooks to be called on operations
|
* a lot of refreshing hooks to be called on operations
|
||||||
*/
|
*/
|
||||||
export function usePublicStoreActions<T extends BoundT>(
|
export function useReadOnlyActions<T extends BoundT>(
|
||||||
api: BaseCRUDAPIReadOnly<T>,
|
api: BaseCRUDAPIReadOnly<T>,
|
||||||
allRef: Ref<T[] | null> | null,
|
allRef: Ref<T[] | null> | null,
|
||||||
loading: Ref<boolean>
|
loading: Ref<boolean>
|
||||||
): PublicStoreActions<T> {
|
): ReadOnlyStoreActions<T> {
|
||||||
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||||
params.orderBy ??= "name";
|
params.orderBy ??= "name";
|
||||||
params.orderDirection ??= "asc";
|
params.orderDirection ??= "asc";
|
||||||
|
53
frontend/composables/partials/use-store-factory.ts
Normal file
53
frontend/composables/partials/use-store-factory.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useReadOnlyActions, useStoreActions } from "./use-actions-factory";
|
||||||
|
import { BoundT } from "./types";
|
||||||
|
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||||
|
|
||||||
|
export const useData = function<T extends BoundT>(defaultObject: T) {
|
||||||
|
const data = reactive({ ...defaultObject });
|
||||||
|
function reset() {
|
||||||
|
Object.assign(data, defaultObject);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data, reset };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReadOnlyStore = function<T extends BoundT>(
|
||||||
|
store: Ref<T[]>,
|
||||||
|
loading: Ref<boolean>,
|
||||||
|
api: BaseCRUDAPIReadOnly<T>,
|
||||||
|
) {
|
||||||
|
const actions = {
|
||||||
|
...useReadOnlyActions(api, store, loading),
|
||||||
|
flushStore() {
|
||||||
|
store.value = [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!loading.value && (!store.value || store.value.length === 0)) {
|
||||||
|
const result = actions.getAll();
|
||||||
|
store.value = result.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { store, actions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = function<T extends BoundT>(
|
||||||
|
store: Ref<T[]>,
|
||||||
|
loading: Ref<boolean>,
|
||||||
|
api: BaseCRUDAPI<unknown, T, unknown>,
|
||||||
|
) {
|
||||||
|
const actions = {
|
||||||
|
...useStoreActions(api, store, loading),
|
||||||
|
flushStore() {
|
||||||
|
store = ref([]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!loading.value && (!store.value || store.value.length === 0)) {
|
||||||
|
const result = actions.getAll();
|
||||||
|
store.value = result.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { store, actions };
|
||||||
|
}
|
@ -32,6 +32,7 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
|
|||||||
searchSeed: query?._searchSeed, // unused, but pass it along for completeness of data
|
searchSeed: query?._searchSeed, // unused, but pass it along for completeness of data
|
||||||
search: query?.search,
|
search: query?.search,
|
||||||
cookbook: query?.cookbook,
|
cookbook: query?.cookbook,
|
||||||
|
households: query?.households,
|
||||||
categories: query?.categories,
|
categories: query?.categories,
|
||||||
requireAllCategories: query?.requireAllCategories,
|
requireAllCategories: query?.requireAllCategories,
|
||||||
tags: query?.tags,
|
tags: query?.tags,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export { useFoodStore, useFoodData } from "./use-food-store";
|
export { useCategoryStore, usePublicCategoryStore, useCategoryData } from "./use-category-store";
|
||||||
export { useUnitStore, useUnitData } from "./use-unit-store";
|
export { useFoodStore, usePublicFoodStore, useFoodData } from "./use-food-store";
|
||||||
|
export { useHouseholdStore, usePublicHouseholdStore } from "./use-household-store";
|
||||||
export { useLabelStore, useLabelData } from "./use-label-store";
|
export { useLabelStore, useLabelData } from "./use-label-store";
|
||||||
export { useToolStore, useToolData } from "./use-tool-store";
|
export { useTagStore, usePublicTagStore, useTagData } from "./use-tag-store";
|
||||||
export { useCategoryStore, useCategoryData } from "./use-category-store";
|
export { useToolStore, usePublicToolStore, useToolData } from "./use-tool-store";
|
||||||
export { useTagStore, useTagData } from "./use-tag-store";
|
export { useUnitStore, useUnitData } from "./use-unit-store";
|
||||||
|
@ -1,73 +1,26 @@
|
|||||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
import { usePublicExploreApi } from "../api/api-client";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { RecipeCategory } from "~/lib/api/types/recipe";
|
import { RecipeCategory } from "~/lib/api/types/recipe";
|
||||||
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
const categoryStore: Ref<RecipeCategory[]> = ref([]);
|
const store: Ref<RecipeCategory[]> = ref([]);
|
||||||
const publicStoreLoading = ref(false);
|
const loading = ref(false);
|
||||||
const storeLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
export function useCategoryData() {
|
export const useCategoryData = function () {
|
||||||
const data = reactive({
|
return useData<RecipeCategory>({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
slug: undefined,
|
slug: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset() {
|
|
||||||
data.id = "";
|
|
||||||
data.name = "";
|
|
||||||
data.slug = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePublicCategoryStore(groupSlug: string) {
|
export const useCategoryStore = function () {
|
||||||
const api = usePublicExploreApi(groupSlug).explore;
|
|
||||||
const loading = publicStoreLoading;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
...usePublicStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
|
|
||||||
flushStore() {
|
|
||||||
categoryStore.value = [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!categoryStore.value || categoryStore.value?.length === 0)) {
|
|
||||||
actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: categoryStore,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCategoryStore() {
|
|
||||||
// passing the group slug switches to using the public API
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = storeLoading;
|
return useStore<RecipeCategory>(store, loading, api.categories);
|
||||||
|
}
|
||||||
const actions = {
|
|
||||||
...useStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
|
export const usePublicCategoryStore = function (groupSlug: string) {
|
||||||
flushStore() {
|
const api = usePublicExploreApi(groupSlug).explore;
|
||||||
categoryStore.value = [];
|
return useReadOnlyStore<RecipeCategory>(store, publicLoading, api.categories);
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!categoryStore.value || categoryStore.value?.length === 0)) {
|
|
||||||
actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: categoryStore,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,73 +1,28 @@
|
|||||||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
import { usePublicExploreApi } from "../api/api-client";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { IngredientFood } from "~/lib/api/types/recipe";
|
import { IngredientFood } from "~/lib/api/types/recipe";
|
||||||
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
let foodStore: Ref<IngredientFood[] | null> = ref([]);
|
const store: Ref<IngredientFood[]> = ref([]);
|
||||||
const publicStoreLoading = ref(false);
|
const loading = ref(false);
|
||||||
const storeLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
/**
|
|
||||||
* useFoodData returns a template reactive object
|
|
||||||
* for managing the creation of foods. It also provides a
|
|
||||||
* function to reset the data back to the initial state.
|
|
||||||
*/
|
|
||||||
export const useFoodData = function () {
|
export const useFoodData = function () {
|
||||||
const data: IngredientFood = reactive({
|
return useData<IngredientFood>({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
labelId: undefined,
|
labelId: undefined,
|
||||||
onHand: false,
|
onHand: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
function reset() {
|
|
||||||
data.id = "";
|
|
||||||
data.name = "";
|
|
||||||
data.description = "";
|
|
||||||
data.labelId = undefined;
|
|
||||||
data.onHand = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usePublicFoodStore = function (groupSlug: string) {
|
|
||||||
const api = usePublicExploreApi(groupSlug).explore;
|
|
||||||
const loading = publicStoreLoading;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
...usePublicStoreActions(api.foods, foodStore, loading),
|
|
||||||
flushStore() {
|
|
||||||
foodStore = ref([]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!foodStore.value || foodStore.value.length === 0)) {
|
|
||||||
foodStore = actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { foods: foodStore, actions };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFoodStore = function () {
|
export const useFoodStore = function () {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = storeLoading;
|
return useStore<IngredientFood>(store, loading, api.foods);
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
export const usePublicFoodStore = function (groupSlug: string) {
|
||||||
...useStoreActions(api.foods, foodStore, loading),
|
const api = usePublicExploreApi(groupSlug).explore;
|
||||||
flushStore() {
|
return useReadOnlyStore<IngredientFood>(store, publicLoading, api.foods);
|
||||||
foodStore.value = [];
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!foodStore.value || foodStore.value.length === 0)) {
|
|
||||||
foodStore = actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { foods: foodStore, actions };
|
|
||||||
};
|
|
||||||
|
18
frontend/composables/store/use-household-store.ts
Normal file
18
frontend/composables/store/use-household-store.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useReadOnlyStore } from "../partials/use-store-factory";
|
||||||
|
import { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
|
const store: Ref<HouseholdSummary[]> = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export const useHouseholdStore = function () {
|
||||||
|
const api = useUserApi();
|
||||||
|
return useReadOnlyStore<HouseholdSummary>(store, loading, api.households);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePublicHouseholdStore = function (groupSlug: string) {
|
||||||
|
const api = usePublicExploreApi(groupSlug).explore;
|
||||||
|
return useReadOnlyStore<HouseholdSummary>(store, publicLoading, api.households);
|
||||||
|
}
|
@ -1,50 +1,21 @@
|
|||||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
import { useStoreActions } from "../partials/use-actions-factory";
|
import { useData, useStore } from "../partials/use-store-factory";
|
||||||
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
let labelStore: Ref<MultiPurposeLabelOut[] | null> = ref([]);
|
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
|
||||||
const storeLoading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
export function useLabelData() {
|
export const useLabelData = function () {
|
||||||
const data = reactive({
|
return useData<MultiPurposeLabelOut>({
|
||||||
groupId: "",
|
groupId: "",
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
color: "",
|
color: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset() {
|
|
||||||
data.groupId = "";
|
|
||||||
data.id = "";
|
|
||||||
data.name = "";
|
|
||||||
data.color = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLabelStore() {
|
export const useLabelStore = function () {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = storeLoading;
|
return useStore<MultiPurposeLabelOut>(store, loading, api.multiPurposeLabels);
|
||||||
|
|
||||||
const actions = {
|
|
||||||
...useStoreActions<MultiPurposeLabelOut>(api.multiPurposeLabels, labelStore, loading),
|
|
||||||
flushStore() {
|
|
||||||
labelStore.value = [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!labelStore.value || labelStore.value?.length === 0)) {
|
|
||||||
labelStore = actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: labelStore,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,72 +1,26 @@
|
|||||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
import { usePublicExploreApi } from "../api/api-client";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { RecipeTag } from "~/lib/api/types/recipe";
|
import { RecipeTag } from "~/lib/api/types/recipe";
|
||||||
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
const items: Ref<RecipeTag[]> = ref([]);
|
const store: Ref<RecipeTag[]> = ref([]);
|
||||||
const publicStoreLoading = ref(false);
|
const loading = ref(false);
|
||||||
const storeLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
export function useTagData() {
|
export const useTagData = function () {
|
||||||
const data = reactive({
|
return useData<RecipeTag>({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
slug: undefined,
|
slug: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset() {
|
|
||||||
data.id = "";
|
|
||||||
data.name = "";
|
|
||||||
data.slug = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePublicTagStore(groupSlug: string) {
|
export const useTagStore = function () {
|
||||||
const api = usePublicExploreApi(groupSlug).explore;
|
|
||||||
const loading = publicStoreLoading;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
...usePublicStoreActions<RecipeTag>(api.tags, items, loading),
|
|
||||||
flushStore() {
|
|
||||||
items.value = [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!items.value || items.value?.length === 0)) {
|
|
||||||
actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTagStore() {
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = storeLoading;
|
return useStore<RecipeTag>(store, loading, api.tags);
|
||||||
|
}
|
||||||
const actions = {
|
|
||||||
...useStoreActions<RecipeTag>(api.tags, items, loading),
|
export const usePublicTagStore = function (groupSlug: string) {
|
||||||
flushStore() {
|
const api = usePublicExploreApi(groupSlug).explore;
|
||||||
items.value = [];
|
return useReadOnlyStore<RecipeTag>(store, publicLoading, api.tags);
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!items.value || items.value?.length === 0)) {
|
|
||||||
actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,74 +1,27 @@
|
|||||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
import { usePublicExploreApi } from "../api/api-client";
|
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { RecipeTool } from "~/lib/api/types/recipe";
|
import { RecipeTool } from "~/lib/api/types/recipe";
|
||||||
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
const toolStore: Ref<RecipeTool[]> = ref([]);
|
const store: Ref<RecipeTool[]> = ref([]);
|
||||||
const publicStoreLoading = ref(false);
|
const loading = ref(false);
|
||||||
const storeLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
export function useToolData() {
|
export const useToolData = function () {
|
||||||
const data = reactive({
|
return useData<RecipeTool>({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
slug: undefined,
|
slug: "",
|
||||||
onHand: false,
|
onHand: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset() {
|
|
||||||
data.id = "";
|
|
||||||
data.name = "";
|
|
||||||
data.slug = undefined;
|
|
||||||
data.onHand = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePublicToolStore(groupSlug: string) {
|
export const useToolStore = function () {
|
||||||
const api = usePublicExploreApi(groupSlug).explore;
|
|
||||||
const loading = publicStoreLoading;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
...usePublicStoreActions<RecipeTool>(api.tools, toolStore, loading),
|
|
||||||
flushStore() {
|
|
||||||
toolStore.value = [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!toolStore.value || toolStore.value?.length === 0)) {
|
|
||||||
actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: toolStore,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useToolStore() {
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = storeLoading;
|
return useStore<RecipeTool>(store, loading, api.tools);
|
||||||
|
}
|
||||||
const actions = {
|
|
||||||
...useStoreActions<RecipeTool>(api.tools, toolStore, loading),
|
export const usePublicToolStore = function (groupSlug: string) {
|
||||||
flushStore() {
|
const api = usePublicExploreApi(groupSlug).explore;
|
||||||
toolStore.value = [];
|
return useReadOnlyStore<RecipeTool>(store, publicLoading, api.tools);
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!toolStore.value || toolStore.value?.length === 0)) {
|
|
||||||
actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: toolStore,
|
|
||||||
actions,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,22 @@
|
|||||||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||||
import { useStoreActions } from "../partials/use-actions-factory";
|
import { useData, useStore } from "../partials/use-store-factory";
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { IngredientUnit } from "~/lib/api/types/recipe";
|
import { IngredientUnit } from "~/lib/api/types/recipe";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
let unitStore: Ref<IngredientUnit[] | null> = ref([]);
|
const store: Ref<IngredientUnit[]> = ref([]);
|
||||||
const storeLoading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
/**
|
|
||||||
* useUnitData returns a template reactive object
|
|
||||||
* for managing the creation of units. It also provides a
|
|
||||||
* function to reset the data back to the initial state.
|
|
||||||
*/
|
|
||||||
export const useUnitData = function () {
|
export const useUnitData = function () {
|
||||||
const data: IngredientUnit = reactive({
|
return useData<IngredientUnit>({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
fraction: true,
|
fraction: true,
|
||||||
abbreviation: "",
|
abbreviation: "",
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
function reset() {
|
|
||||||
data.id = "";
|
|
||||||
data.name = "";
|
|
||||||
data.fraction = true;
|
|
||||||
data.abbreviation = "";
|
|
||||||
data.description = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
reset,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUnitStore = function () {
|
export const useUnitStore = function () {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = storeLoading;
|
return useStore<IngredientUnit>(store, loading, api.units);
|
||||||
|
}
|
||||||
const actions = {
|
|
||||||
...useStoreActions<IngredientUnit>(api.units, unitStore, loading),
|
|
||||||
flushStore() {
|
|
||||||
unitStore.value = [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loading.value && (!unitStore.value || unitStore.value.length === 0)) {
|
|
||||||
unitStore = actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { units: unitStore, actions };
|
|
||||||
};
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { computed, ref, Ref, useAsync } from "@nuxtjs/composition-api";
|
import { computed, ref, Ref, useAsync } from "@nuxtjs/composition-api";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||||
import { HouseholdCreate, HouseholdInDB } from "~/lib/api/types/household";
|
import { HouseholdCreate, HouseholdInDB } from "~/lib/api/types/household";
|
||||||
|
|
||||||
const householdSelfRef = ref<HouseholdInDB | null>(null);
|
const householdSelfRef = ref<HouseholdInDB | null>(null);
|
||||||
@ -46,8 +46,8 @@ export const useHouseholdSelf = function () {
|
|||||||
return { actions, household };
|
return { actions, household };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHouseholds = function () {
|
export const useAdminHouseholds = function () {
|
||||||
const api = useUserApi();
|
const api = useAdminApi();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
function getAllHouseholds() {
|
function getAllHouseholds() {
|
||||||
|
@ -7,34 +7,37 @@ const loading = ref(false);
|
|||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
|
||||||
export const useUserSelfRatings = function () {
|
export const useUserSelfRatings = function () {
|
||||||
const { $auth } = useContext();
|
const { $auth } = useContext();
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
async function refreshUserRatings() {
|
async function refreshUserRatings() {
|
||||||
if (loading.value) {
|
if (!$auth.user || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.users.getSelfRatings();
|
|
||||||
userRatings.value = data?.ratings || [];
|
|
||||||
loading.value = false;
|
|
||||||
ready.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
|
loading.value = true;
|
||||||
loading.value = true;
|
const { data } = await api.users.getSelfRatings();
|
||||||
const userId = $auth.user?.id || "";
|
userRatings.value = data?.ratings || [];
|
||||||
await api.users.setRating(userId, slug, rating, isFavorite);
|
loading.value = false;
|
||||||
loading.value = false;
|
ready.value = true;
|
||||||
await refreshUserRatings();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
|
||||||
|
loading.value = true;
|
||||||
|
const userId = $auth.user?.id || "";
|
||||||
|
await api.users.setRating(userId, slug, rating, isFavorite);
|
||||||
|
loading.value = false;
|
||||||
|
await refreshUserRatings();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready.value) {
|
||||||
refreshUserRatings();
|
refreshUserRatings();
|
||||||
return {
|
}
|
||||||
userRatings,
|
|
||||||
refreshUserRatings,
|
return {
|
||||||
setRating,
|
userRatings,
|
||||||
ready,
|
refreshUserRatings,
|
||||||
}
|
setRating,
|
||||||
|
ready,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -652,6 +652,7 @@
|
|||||||
"or": "Or",
|
"or": "Or",
|
||||||
"has-any": "Has Any",
|
"has-any": "Has Any",
|
||||||
"has-all": "Has All",
|
"has-all": "Has All",
|
||||||
|
"clear-selection": "Clear Selection",
|
||||||
"results": "Results",
|
"results": "Results",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search-mealie": "Search Mealie (press /)",
|
"search-mealie": "Search Mealie (press /)",
|
||||||
|
13
frontend/lib/api/admin/admin-households.ts
Normal file
13
frontend/lib/api/admin/admin-households.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { BaseCRUDAPI } from "../base/base-clients";
|
||||||
|
import { HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin } from "~/lib/api/types/household";
|
||||||
|
const prefix = "/api";
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
adminHouseholds: `${prefix}/admin/households`,
|
||||||
|
adminHouseholdsId: (id: string) => `${prefix}/admin/households/${id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AdminHouseholdsApi extends BaseCRUDAPI<HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin> {
|
||||||
|
baseRoute: string = routes.adminHouseholds;
|
||||||
|
itemRoute = routes.adminHouseholdsId;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { AdminAboutAPI } from "./admin/admin-about";
|
import { AdminAboutAPI } from "./admin/admin-about";
|
||||||
import { AdminUsersApi } from "./admin/admin-users";
|
import { AdminUsersApi } from "./admin/admin-users";
|
||||||
|
import { AdminHouseholdsApi } from "./admin/admin-households";
|
||||||
import { AdminGroupsApi } from "./admin/admin-groups";
|
import { AdminGroupsApi } from "./admin/admin-groups";
|
||||||
import { AdminBackupsApi } from "./admin/admin-backups";
|
import { AdminBackupsApi } from "./admin/admin-backups";
|
||||||
import { AdminMaintenanceApi } from "./admin/admin-maintenance";
|
import { AdminMaintenanceApi } from "./admin/admin-maintenance";
|
||||||
@ -9,6 +10,7 @@ import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
|||||||
export class AdminAPI {
|
export class AdminAPI {
|
||||||
public about: AdminAboutAPI;
|
public about: AdminAboutAPI;
|
||||||
public users: AdminUsersApi;
|
public users: AdminUsersApi;
|
||||||
|
public households: AdminHouseholdsApi;
|
||||||
public groups: AdminGroupsApi;
|
public groups: AdminGroupsApi;
|
||||||
public backups: AdminBackupsApi;
|
public backups: AdminBackupsApi;
|
||||||
public maintenance: AdminMaintenanceApi;
|
public maintenance: AdminMaintenanceApi;
|
||||||
@ -17,6 +19,7 @@ export class AdminAPI {
|
|||||||
constructor(requests: ApiRequestInstance) {
|
constructor(requests: ApiRequestInstance) {
|
||||||
this.about = new AdminAboutAPI(requests);
|
this.about = new AdminAboutAPI(requests);
|
||||||
this.users = new AdminUsersApi(requests);
|
this.users = new AdminUsersApi(requests);
|
||||||
|
this.households = new AdminHouseholdsApi(requests);
|
||||||
this.groups = new AdminGroupsApi(requests);
|
this.groups = new AdminGroupsApi(requests);
|
||||||
this.backups = new AdminBackupsApi(requests);
|
this.backups = new AdminBackupsApi(requests);
|
||||||
this.maintenance = new AdminMaintenanceApi(requests);
|
this.maintenance = new AdminMaintenanceApi(requests);
|
||||||
|
@ -4,6 +4,7 @@ import { PublicRecipeApi } from "./explore/recipes";
|
|||||||
import { PublicFoodsApi } from "./explore/foods";
|
import { PublicFoodsApi } from "./explore/foods";
|
||||||
import { PublicCategoriesApi, PublicTagsApi, PublicToolsApi } from "./explore/organizers";
|
import { PublicCategoriesApi, PublicTagsApi, PublicToolsApi } from "./explore/organizers";
|
||||||
import { PublicCookbooksApi } from "./explore/cookbooks";
|
import { PublicCookbooksApi } from "./explore/cookbooks";
|
||||||
|
import { PublicHouseholdApi } from "./explore/households";
|
||||||
|
|
||||||
export class ExploreApi extends BaseAPI {
|
export class ExploreApi extends BaseAPI {
|
||||||
public recipes: PublicRecipeApi;
|
public recipes: PublicRecipeApi;
|
||||||
@ -12,6 +13,7 @@ export class ExploreApi extends BaseAPI {
|
|||||||
public categories: PublicCategoriesApi;
|
public categories: PublicCategoriesApi;
|
||||||
public tags: PublicTagsApi;
|
public tags: PublicTagsApi;
|
||||||
public tools: PublicToolsApi;
|
public tools: PublicToolsApi;
|
||||||
|
public households: PublicHouseholdApi
|
||||||
|
|
||||||
constructor(requests: ApiRequestInstance, groupSlug: string) {
|
constructor(requests: ApiRequestInstance, groupSlug: string) {
|
||||||
super(requests);
|
super(requests);
|
||||||
@ -21,5 +23,6 @@ export class ExploreApi extends BaseAPI {
|
|||||||
this.categories = new PublicCategoriesApi(requests, groupSlug);
|
this.categories = new PublicCategoriesApi(requests, groupSlug);
|
||||||
this.tags = new PublicTagsApi(requests, groupSlug);
|
this.tags = new PublicTagsApi(requests, groupSlug);
|
||||||
this.tools = new PublicToolsApi(requests, groupSlug);
|
this.tools = new PublicToolsApi(requests, groupSlug);
|
||||||
|
this.households = new PublicHouseholdApi(requests, groupSlug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
frontend/lib/api/public/explore/households.ts
Normal file
20
frontend/lib/api/public/explore/households.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||||
|
import { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
|
import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
|
const prefix = "/api";
|
||||||
|
const exploreGroupSlug = (groupSlug: string | number) => `${prefix}/explore/groups/${groupSlug}`
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
householdsGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/households`,
|
||||||
|
householdsGroupSlugHouseholdSlug: (groupSlug: string | number, householdSlug: string | number) => `${exploreGroupSlug(groupSlug)}/households/${householdSlug}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PublicHouseholdApi extends BaseCRUDAPIReadOnly<HouseholdSummary> {
|
||||||
|
baseRoute = routes.householdsGroupSlug(this.groupSlug);
|
||||||
|
itemRoute = (itemId: string | number) => routes.householdsGroupSlugHouseholdSlug(this.groupSlug, itemId);
|
||||||
|
|
||||||
|
constructor(requests: ApiRequestInstance, private readonly groupSlug: string) {
|
||||||
|
super(requests);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import { BaseCRUDAPI } from "../base/base-clients";
|
import { BaseCRUDAPI } from "../base/base-clients";
|
||||||
import { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
|
import { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
|
||||||
import { HouseholdSummary } from "~/lib/api/types/household";
|
|
||||||
import {
|
import {
|
||||||
GroupAdminUpdate,
|
GroupAdminUpdate,
|
||||||
GroupStorage,
|
GroupStorage,
|
||||||
@ -15,8 +14,6 @@ const routes = {
|
|||||||
groupsSelf: `${prefix}/groups/self`,
|
groupsSelf: `${prefix}/groups/self`,
|
||||||
preferences: `${prefix}/groups/preferences`,
|
preferences: `${prefix}/groups/preferences`,
|
||||||
storage: `${prefix}/groups/storage`,
|
storage: `${prefix}/groups/storage`,
|
||||||
households: `${prefix}/groups/households`,
|
|
||||||
householdsId: (id: string | number) => `${prefix}/groups/households/${id}`,
|
|
||||||
membersHouseholdId: (householdId: string | number | null) => {
|
membersHouseholdId: (householdId: string | number | null) => {
|
||||||
return householdId ?
|
return householdId ?
|
||||||
`${prefix}/households/members?householdId=${householdId}` :
|
`${prefix}/households/members?householdId=${householdId}` :
|
||||||
@ -47,14 +44,6 @@ export class GroupAPI extends BaseCRUDAPI<GroupBase, GroupInDB, GroupAdminUpdate
|
|||||||
return await this.requests.get<UserSummary[]>(routes.membersHouseholdId(householdId));
|
return await this.requests.get<UserSummary[]>(routes.membersHouseholdId(householdId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchHouseholds() {
|
|
||||||
return await this.requests.get<HouseholdSummary[]>(routes.households);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchHousehold(householdId: string | number) {
|
|
||||||
return await this.requests.get<HouseholdSummary>(routes.householdsId(householdId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async storage() {
|
async storage() {
|
||||||
return await this.requests.get<GroupStorage>(routes.storage);
|
return await this.requests.get<GroupStorage>(routes.storage);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
import { BaseCRUDAPI } from "../base/base-clients";
|
import { BaseCRUDAPIReadOnly } from "../base/base-clients";
|
||||||
import { UserOut } from "~/lib/api/types/user";
|
import { UserOut } from "~/lib/api/types/user";
|
||||||
import {
|
import {
|
||||||
HouseholdCreate,
|
|
||||||
HouseholdInDB,
|
HouseholdInDB,
|
||||||
UpdateHouseholdAdmin,
|
|
||||||
HouseholdStatistics,
|
HouseholdStatistics,
|
||||||
ReadHouseholdPreferences,
|
ReadHouseholdPreferences,
|
||||||
SetPermissions,
|
SetPermissions,
|
||||||
UpdateHouseholdPreferences,
|
UpdateHouseholdPreferences,
|
||||||
CreateInviteToken,
|
CreateInviteToken,
|
||||||
ReadInviteToken,
|
ReadInviteToken,
|
||||||
|
HouseholdSummary,
|
||||||
} from "~/lib/api/types/household";
|
} from "~/lib/api/types/household";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
households: `${prefix}/admin/households`,
|
households: `${prefix}/groups/households`,
|
||||||
householdsSelf: `${prefix}/households/self`,
|
householdsSelf: `${prefix}/households/self`,
|
||||||
members: `${prefix}/households/members`,
|
members: `${prefix}/households/members`,
|
||||||
permissions: `${prefix}/households/permissions`,
|
permissions: `${prefix}/households/permissions`,
|
||||||
@ -24,13 +23,13 @@ const routes = {
|
|||||||
statistics: `${prefix}/households/statistics`,
|
statistics: `${prefix}/households/statistics`,
|
||||||
invitation: `${prefix}/households/invitations`,
|
invitation: `${prefix}/households/invitations`,
|
||||||
|
|
||||||
householdsId: (id: string | number) => `${prefix}/admin/households/${id}`,
|
householdsId: (id: string | number) => `${prefix}/groups/households/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class HouseholdAPI extends BaseCRUDAPI<HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin> {
|
export class HouseholdAPI extends BaseCRUDAPIReadOnly<HouseholdSummary> {
|
||||||
baseRoute = routes.households;
|
baseRoute = routes.households;
|
||||||
itemRoute = routes.householdsId;
|
itemRoute = routes.householdsId;
|
||||||
/** Returns the Group Data for the Current User
|
/** Returns the Household Data for the Current User
|
||||||
*/
|
*/
|
||||||
async getCurrentUserHousehold() {
|
async getCurrentUserHousehold() {
|
||||||
return await this.requests.get<HouseholdInDB>(routes.householdsSelf);
|
return await this.requests.get<HouseholdInDB>(routes.householdsSelf);
|
||||||
|
@ -56,13 +56,14 @@ const routes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RecipeSearchQuery = {
|
export type RecipeSearchQuery = {
|
||||||
search: string;
|
search?: string;
|
||||||
orderDirection?: "asc" | "desc";
|
orderDirection?: "asc" | "desc";
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
|
|
||||||
queryFilter?: string;
|
queryFilter?: string;
|
||||||
|
|
||||||
cookbook?: string;
|
cookbook?: string;
|
||||||
|
households?: string[];
|
||||||
|
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
requireAllCategories?: boolean;
|
requireAllCategories?: boolean;
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
import { defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
|
import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
|
||||||
import { useGroups } from "~/composables/use-groups";
|
import { useGroups } from "~/composables/use-groups";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useAdminApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { HouseholdInDB } from "~/lib/api/types/household";
|
import { HouseholdInDB } from "~/lib/api/types/household";
|
||||||
@ -68,14 +68,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
const refHouseholdEditForm = ref<VForm | null>(null);
|
const refHouseholdEditForm = ref<VForm | null>(null);
|
||||||
|
|
||||||
const userApi = useUserApi();
|
const adminApi = useAdminApi();
|
||||||
|
|
||||||
const household = ref<HouseholdInDB | null>(null);
|
const household = ref<HouseholdInDB | null>(null);
|
||||||
|
|
||||||
const userError = ref(false);
|
const userError = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const { data, error } = await userApi.households.getOne(householdId);
|
const { data, error } = await adminApi.households.getOne(householdId);
|
||||||
|
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
alert.error(i18n.tc("user.user-not-found"));
|
alert.error(i18n.tc("user.user-not-found"));
|
||||||
@ -92,7 +92,7 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { response, data } = await userApi.households.updateOne(household.value.id, household.value);
|
const { response, data } = await adminApi.households.updateOne(household.value.id, household.value);
|
||||||
if (response?.status === 200 && data) {
|
if (response?.status === 200 && data) {
|
||||||
household.value = data;
|
household.value = data;
|
||||||
alert.success(i18n.tc("settings.settings-updated"));
|
alert.success(i18n.tc("settings.settings-updated"));
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
import { defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
|
import { defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||||
import { fieldTypes } from "~/composables/forms";
|
import { fieldTypes } from "~/composables/forms";
|
||||||
import { useGroups } from "~/composables/use-groups";
|
import { useGroups } from "~/composables/use-groups";
|
||||||
import { useHouseholds } from "~/composables/use-households";
|
import { useAdminHouseholds } from "~/composables/use-households";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { HouseholdInDB } from "~/lib/api/types/household";
|
import { HouseholdInDB } from "~/lib/api/types/household";
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const { i18n } = useContext();
|
const { i18n } = useContext();
|
||||||
const { groups } = useGroups();
|
const { groups } = useGroups();
|
||||||
const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useHouseholds();
|
const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useAdminHouseholds();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
createDialog: false,
|
createDialog: false,
|
||||||
|
@ -80,7 +80,7 @@
|
|||||||
import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||||
import { useGroups } from "~/composables/use-groups";
|
import { useGroups } from "~/composables/use-groups";
|
||||||
import { useHouseholds } from "~/composables/use-households";
|
import { useAdminHouseholds } from "~/composables/use-households";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useUserForm } from "~/composables/use-users";
|
import { useUserForm } from "~/composables/use-users";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
@ -92,7 +92,7 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const { userForm } = useUserForm();
|
const { userForm } = useUserForm();
|
||||||
const { groups } = useGroups();
|
const { groups } = useGroups();
|
||||||
const { useHouseholdsInGroup } = useHouseholds();
|
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||||
const { i18n } = useContext();
|
const { i18n } = useContext();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
import { computed, defineComponent, useRouter, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, useRouter, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
|
||||||
import { useAdminApi } from "~/composables/api";
|
import { useAdminApi } from "~/composables/api";
|
||||||
import { useGroups } from "~/composables/use-groups";
|
import { useGroups } from "~/composables/use-groups";
|
||||||
import { useHouseholds } from "~/composables/use-households";
|
import { useAdminHouseholds } from "~/composables/use-households";
|
||||||
import { useUserForm } from "~/composables/use-users";
|
import { useUserForm } from "~/composables/use-users";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { VForm } from "~/types/vuetify";
|
import { VForm } from "~/types/vuetify";
|
||||||
@ -60,7 +60,7 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const { userForm } = useUserForm();
|
const { userForm } = useUserForm();
|
||||||
const { groups } = useGroups();
|
const { groups } = useGroups();
|
||||||
const { useHouseholdsInGroup } = useHouseholds();
|
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// ==============================================
|
// ==============================================
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref, useContext, useRouter } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, ref, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
|
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
|
||||||
@ -108,7 +108,8 @@ export default defineComponent({
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// Setup
|
// Setup
|
||||||
const { $auth, $globals, i18n } = useContext();
|
const { $auth, $globals, i18n } = useContext();
|
||||||
const api = useUserApi();
|
const userApi = useUserApi();
|
||||||
|
const adminApi = useAdminApi();
|
||||||
|
|
||||||
const groupSlug = computed(() => $auth.user?.groupSlug);
|
const groupSlug = computed(() => $auth.user?.groupSlug);
|
||||||
const { locale } = useLocales();
|
const { locale } = useLocales();
|
||||||
@ -264,7 +265,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function updateUser() {
|
async function updateUser() {
|
||||||
// @ts-ignore-next-line user will never be null here
|
// @ts-ignore-next-line user will never be null here
|
||||||
const { response } = await api.users.updateOne($auth.user?.id, {
|
const { response } = await userApi.users.updateOne($auth.user?.id, {
|
||||||
...$auth.user,
|
...$auth.user,
|
||||||
email: accountDetails.email.value,
|
email: accountDetails.email.value,
|
||||||
username: accountDetails.username.value,
|
username: accountDetails.username.value,
|
||||||
@ -285,7 +286,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updatePassword() {
|
async function updatePassword() {
|
||||||
const { response } = await api.users.changePassword({
|
const { response } = await userApi.users.changePassword({
|
||||||
currentPassword: "MyPassword",
|
currentPassword: "MyPassword",
|
||||||
newPassword: credentials.password1.value,
|
newPassword: credentials.password1.value,
|
||||||
});
|
});
|
||||||
@ -303,7 +304,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function updateGroup() {
|
async function updateGroup() {
|
||||||
// @ts-ignore-next-line user will never be null here
|
// @ts-ignore-next-line user will never be null here
|
||||||
const { data } = await api.groups.getOne($auth.user?.groupId);
|
const { data } = await userApi.groups.getOne($auth.user?.groupId);
|
||||||
if (!data || !data.preferences) {
|
if (!data || !data.preferences) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
return;
|
return;
|
||||||
@ -320,7 +321,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore-next-line user will never be null here
|
// @ts-ignore-next-line user will never be null here
|
||||||
const { response } = await api.groups.updateOne($auth.user?.groupId, payload);
|
const { response } = await userApi.groups.updateOne($auth.user?.groupId, payload);
|
||||||
if (!response || response.status !== 200) {
|
if (!response || response.status !== 200) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
@ -328,7 +329,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function updateHousehold() {
|
async function updateHousehold() {
|
||||||
// @ts-ignore-next-line user will never be null here
|
// @ts-ignore-next-line user will never be null here
|
||||||
const { data } = await api.households.getOne($auth.user?.householdId);
|
const { data } = await adminApi.households.getOne($auth.user?.householdId);
|
||||||
if (!data || !data.preferences) {
|
if (!data || !data.preferences) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
return;
|
return;
|
||||||
@ -346,28 +347,28 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore-next-line user will never be null here
|
// @ts-ignore-next-line user will never be null here
|
||||||
const { response } = await api.households.updateOne($auth.user?.householdId, payload);
|
const { response } = await adminApi.households.updateOne($auth.user?.householdId, payload);
|
||||||
if (!response || response.status !== 200) {
|
if (!response || response.status !== 200) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedFoods() {
|
async function seedFoods() {
|
||||||
const { response } = await api.seeders.foods({ locale: locale.value })
|
const { response } = await userApi.seeders.foods({ locale: locale.value })
|
||||||
if (!response || response.status !== 200) {
|
if (!response || response.status !== 200) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedUnits() {
|
async function seedUnits() {
|
||||||
const { response } = await api.seeders.units({ locale: locale.value })
|
const { response } = await userApi.seeders.units({ locale: locale.value })
|
||||||
if (!response || response.status !== 200) {
|
if (!response || response.status !== 200) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedLabels() {
|
async function seedLabels() {
|
||||||
const { response } = await api.seeders.labels({ locale: locale.value })
|
const { response } = await userApi.seeders.labels({ locale: locale.value })
|
||||||
if (!response || response.status !== 200) {
|
if (!response || response.status !== 200) {
|
||||||
alert.error(i18n.tc("events.something-went-wrong"));
|
alert.error(i18n.tc("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
|
@ -272,12 +272,10 @@ export default defineComponent({
|
|||||||
const errors = ref<Error[]>([]);
|
const errors = ref<Error[]>([]);
|
||||||
|
|
||||||
function checkForUnit(unit?: IngredientUnit | CreateIngredientUnit) {
|
function checkForUnit(unit?: IngredientUnit | CreateIngredientUnit) {
|
||||||
// @ts-expect-error; we're just checking if there's an id on this unit and returning a boolean
|
|
||||||
return !!unit?.id;
|
return !!unit?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForFood(food?: IngredientFood | CreateIngredientFood) {
|
function checkForFood(food?: IngredientFood | CreateIngredientFood) {
|
||||||
// @ts-expect-error; we're just checking if there's an id on this food and returning a boolean
|
|
||||||
return !!food?.id;
|
return !!food?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeOrganizerPage
|
<RecipeOrganizerPage
|
||||||
v-if="items"
|
v-if="store"
|
||||||
:items="items"
|
:items="store"
|
||||||
:icon="$globals.icons.categories"
|
:icon="$globals.icons.categories"
|
||||||
item-type="categories"
|
item-type="categories"
|
||||||
@delete="actions.deleteOne"
|
@delete="actions.deleteOne"
|
||||||
@ -24,10 +24,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
middleware: ["auth", "group-only"],
|
middleware: ["auth", "group-only"],
|
||||||
setup() {
|
setup() {
|
||||||
const { items, actions } = useCategoryStore();
|
const { store, actions } = useCategoryStore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
store,
|
||||||
actions,
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeOrganizerPage
|
<RecipeOrganizerPage
|
||||||
v-if="items"
|
v-if="store"
|
||||||
:items="items"
|
:items="store"
|
||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
item-type="tags"
|
item-type="tags"
|
||||||
@delete="actions.deleteOne"
|
@delete="actions.deleteOne"
|
||||||
@ -24,10 +24,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
middleware: ["auth", "group-only"],
|
middleware: ["auth", "group-only"],
|
||||||
setup() {
|
setup() {
|
||||||
const { items, actions } = useTagStore();
|
const { store, actions } = useTagStore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
store,
|
||||||
actions,
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -29,7 +29,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dialog,
|
dialog,
|
||||||
tools: toolStore.items,
|
tools: toolStore.store,
|
||||||
actions: toolStore.actions,
|
actions: toolStore.actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -81,6 +81,7 @@
|
|||||||
:headers.sync="tableHeaders"
|
:headers.sync="tableHeaders"
|
||||||
:data="categories || []"
|
:data="categories || []"
|
||||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||||
|
initial-sort="name"
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@delete-selected="bulkDeleteEventHandler"
|
@delete-selected="bulkDeleteEventHandler"
|
||||||
@ -198,7 +199,7 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
tableConfig,
|
tableConfig,
|
||||||
tableHeaders,
|
tableHeaders,
|
||||||
categories: categoryStore.items,
|
categories: categoryStore.store,
|
||||||
validators,
|
validators,
|
||||||
|
|
||||||
// create
|
// create
|
||||||
|
@ -241,6 +241,8 @@
|
|||||||
{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'},
|
{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'},
|
||||||
{icon: $globals.icons.tags, text: $tc('data-pages.labels.assign-label'), event: 'assign-selected'}
|
{icon: $globals.icons.tags, text: $tc('data-pages.labels.assign-label'), event: 'assign-selected'}
|
||||||
]"
|
]"
|
||||||
|
initial-sort="createdAt"
|
||||||
|
initial-sort-desc
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@create-one="createEventHandler"
|
@create-one="createEventHandler"
|
||||||
@ -264,6 +266,9 @@
|
|||||||
{{ item.onHand ? $globals.icons.check : $globals.icons.close }}
|
{{ item.onHand ? $globals.icons.check : $globals.icons.close }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
|
<template #item.createdAt="{ item }">
|
||||||
|
{{ formatDate(item.createdAt) }}
|
||||||
|
</template>
|
||||||
<template #button-bottom>
|
<template #button-bottom>
|
||||||
<BaseButton @click="seedDialog = true">
|
<BaseButton @click="seedDialog = true">
|
||||||
<template #icon> {{ $globals.icons.database }} </template>
|
<template #icon> {{ $globals.icons.database }} </template>
|
||||||
@ -326,8 +331,21 @@ export default defineComponent({
|
|||||||
value: "onHand",
|
value: "onHand",
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: i18n.tc("general.date-added"),
|
||||||
|
value: "createdAt",
|
||||||
|
show: false,
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function formatDate(date: string) {
|
||||||
|
try {
|
||||||
|
return i18n.d(Date.parse(date), "medium");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const foodStore = useFoodStore();
|
const foodStore = useFoodStore();
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
@ -453,7 +471,7 @@ export default defineComponent({
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Labels
|
// Labels
|
||||||
|
|
||||||
const { labels: allLabels } = useLabelStore();
|
const { store: allLabels } = useLabelStore();
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Seed
|
// Seed
|
||||||
@ -501,16 +519,15 @@ export default defineComponent({
|
|||||||
bulkAssignTarget.value = [];
|
bulkAssignTarget.value = [];
|
||||||
bulkAssignLabelId.value = undefined;
|
bulkAssignLabelId.value = undefined;
|
||||||
foodStore.actions.refresh();
|
foodStore.actions.refresh();
|
||||||
// reload page, because foodStore.actions.refresh() does not update the table, reactivity for this seems to be broken (again)
|
|
||||||
document.location.reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tableConfig,
|
tableConfig,
|
||||||
tableHeaders,
|
tableHeaders,
|
||||||
foods: foodStore.foods,
|
foods: foodStore.store,
|
||||||
allLabels,
|
allLabels,
|
||||||
validators,
|
validators,
|
||||||
|
formatDate,
|
||||||
// Create
|
// Create
|
||||||
createDialog,
|
createDialog,
|
||||||
domNewFoodForm,
|
domNewFoodForm,
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
:headers.sync="tableHeaders"
|
:headers.sync="tableHeaders"
|
||||||
:data="labels || []"
|
:data="labels || []"
|
||||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||||
|
initial-sort="name"
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@delete-selected="bulkDeleteEventHandler"
|
@delete-selected="bulkDeleteEventHandler"
|
||||||
@ -271,7 +272,7 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
tableConfig,
|
tableConfig,
|
||||||
tableHeaders,
|
tableHeaders,
|
||||||
labels: labelStore.labels,
|
labels: labelStore.store,
|
||||||
validators,
|
validators,
|
||||||
|
|
||||||
// create
|
// create
|
||||||
|
@ -101,6 +101,7 @@
|
|||||||
:headers.sync="tableHeaders"
|
:headers.sync="tableHeaders"
|
||||||
:data="actions || []"
|
:data="actions || []"
|
||||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||||
|
initial-sort="title"
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@delete-selected="bulkDeleteEventHandler"
|
@delete-selected="bulkDeleteEventHandler"
|
||||||
|
@ -81,6 +81,7 @@
|
|||||||
:headers.sync="tableHeaders"
|
:headers.sync="tableHeaders"
|
||||||
:data="tags || []"
|
:data="tags || []"
|
||||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||||
|
initial-sort="name"
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@delete-selected="bulkDeleteEventHandler"
|
@delete-selected="bulkDeleteEventHandler"
|
||||||
@ -199,7 +200,7 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
tableConfig,
|
tableConfig,
|
||||||
tableHeaders,
|
tableHeaders,
|
||||||
tags: tagStore.items,
|
tags: tagStore.store,
|
||||||
validators,
|
validators,
|
||||||
|
|
||||||
// create
|
// create
|
||||||
|
@ -83,6 +83,7 @@
|
|||||||
:headers.sync="tableHeaders"
|
:headers.sync="tableHeaders"
|
||||||
:data="tools || []"
|
:data="tools || []"
|
||||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||||
|
initial-sort="name"
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@delete-selected="bulkDeleteEventHandler"
|
@delete-selected="bulkDeleteEventHandler"
|
||||||
@ -209,7 +210,7 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
tableConfig,
|
tableConfig,
|
||||||
tableHeaders,
|
tableHeaders,
|
||||||
tools: toolStore.items,
|
tools: toolStore.store,
|
||||||
validators,
|
validators,
|
||||||
|
|
||||||
// create
|
// create
|
||||||
|
@ -9,11 +9,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
||||||
<v-autocomplete v-model="fromUnit" return-object :items="units" item-text="id" :label="$t('data-pages.units.source-unit')">
|
<v-autocomplete v-model="fromUnit" return-object :items="store" item-text="id" :label="$t('data-pages.units.source-unit')">
|
||||||
<template #selection="{ item }"> {{ item.name }}</template>
|
<template #selection="{ item }"> {{ item.name }}</template>
|
||||||
<template #item="{ item }"> {{ item.name }} </template>
|
<template #item="{ item }"> {{ item.name }} </template>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
<v-autocomplete v-model="toUnit" return-object :items="units" item-text="id" :label="$t('data-pages.units.target-unit')">
|
<v-autocomplete v-model="toUnit" return-object :items="store" item-text="id" :label="$t('data-pages.units.target-unit')">
|
||||||
<template #selection="{ item }"> {{ item.name }}</template>
|
<template #selection="{ item }"> {{ item.name }}</template>
|
||||||
<template #item="{ item }"> {{ item.name }} </template>
|
<template #item="{ item }"> {{ item.name }} </template>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
@ -185,7 +185,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
|
|
||||||
<v-alert v-if="units && units.length > 0" type="error" class="mb-0 text-body-2">
|
<v-alert v-if="store && store.length > 0" type="error" class="mb-0 text-body-2">
|
||||||
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@ -196,8 +196,10 @@
|
|||||||
<CrudTable
|
<CrudTable
|
||||||
:table-config="tableConfig"
|
:table-config="tableConfig"
|
||||||
:headers.sync="tableHeaders"
|
:headers.sync="tableHeaders"
|
||||||
:data="units || []"
|
:data="store"
|
||||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||||
|
initial-sort="createdAt"
|
||||||
|
initial-sort-desc
|
||||||
@delete-one="deleteEventHandler"
|
@delete-one="deleteEventHandler"
|
||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
@create-one="createEventHandler"
|
@create-one="createEventHandler"
|
||||||
@ -221,6 +223,9 @@
|
|||||||
{{ item.fraction ? $globals.icons.check : $globals.icons.close }}
|
{{ item.fraction ? $globals.icons.check : $globals.icons.close }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
|
<template #item.createdAt="{ item }">
|
||||||
|
{{ formatDate(item.createdAt) }}
|
||||||
|
</template>
|
||||||
<template #button-bottom>
|
<template #button-bottom>
|
||||||
<BaseButton @click="seedDialog = true">
|
<BaseButton @click="seedDialog = true">
|
||||||
<template #icon> {{ $globals.icons.database }} </template>
|
<template #icon> {{ $globals.icons.database }} </template>
|
||||||
@ -292,9 +297,22 @@ export default defineComponent({
|
|||||||
value: "fraction",
|
value: "fraction",
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: i18n.tc("general.date-added"),
|
||||||
|
value: "createdAt",
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { units, actions: unitActions } = useUnitStore();
|
function formatDate(date: string) {
|
||||||
|
try {
|
||||||
|
return i18n.d(Date.parse(date), "medium");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { store, actions: unitActions } = useUnitStore();
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Create Units
|
// Create Units
|
||||||
@ -447,8 +465,9 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tableConfig,
|
tableConfig,
|
||||||
tableHeaders,
|
tableHeaders,
|
||||||
units,
|
store,
|
||||||
validators,
|
validators,
|
||||||
|
formatDate,
|
||||||
// Create
|
// Create
|
||||||
createDialog,
|
createDialog,
|
||||||
domNewUnitForm,
|
domNewUnitForm,
|
||||||
|
@ -602,9 +602,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
const localLabels = ref<ShoppingListMultiPurposeLabelOut[]>()
|
const localLabels = ref<ShoppingListMultiPurposeLabelOut[]>()
|
||||||
|
|
||||||
const { labels: allLabels } = useLabelStore();
|
const { store: allLabels } = useLabelStore();
|
||||||
const { units: allUnits } = useUnitStore();
|
const { store: allUnits } = useUnitStore();
|
||||||
const { foods: allFoods } = useFoodStore();
|
const { store: allFoods } = useFoodStore();
|
||||||
|
|
||||||
function getLabelColor(item: ShoppingListItemOut | null) {
|
function getLabelColor(item: ShoppingListItemOut | null) {
|
||||||
return item?.label?.color;
|
return item?.label?.color;
|
||||||
|
@ -5,34 +5,40 @@
|
|||||||
:icon="$globals.icons.heart"
|
:icon="$globals.icons.heart"
|
||||||
:title="$tc('user.user-favorites')"
|
:title="$tc('user.user-favorites')"
|
||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
|
:query="query"
|
||||||
|
@sortRecipes="assignSorted"
|
||||||
|
@replaceRecipes="replaceRecipes"
|
||||||
|
@appendRecipes="appendRecipes"
|
||||||
|
@delete="removeRecipe"
|
||||||
/>
|
/>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
|
import { defineComponent, useRoute } from "@nuxtjs/composition-api";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
setup() {
|
setup() {
|
||||||
const api = useUserApi();
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const userId = route.value.params.id;
|
const userId = route.value.params.id;
|
||||||
const recipes = useAsync(async () => {
|
const query = { queryFilter: `favoritedBy.id = "${userId}"` }
|
||||||
const { data } = await api.recipes.getAll(1, -1, { queryFilter: `favoritedBy.id = "${userId}"` });
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||||
return data?.items || null;
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
query,
|
||||||
recipes,
|
recipes,
|
||||||
isOwnGroup,
|
isOwnGroup,
|
||||||
|
appendRecipes,
|
||||||
|
assignSorted,
|
||||||
|
removeRecipe,
|
||||||
|
replaceRecipes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
|
@ -10,6 +10,7 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
from sqlalchemy.orm import InstrumentedAttribute
|
from sqlalchemy.orm import InstrumentedAttribute
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from mealie.db.models.household.household import Household
|
||||||
from mealie.db.models.recipe.category import Category
|
from mealie.db.models.recipe.category import Category
|
||||||
from mealie.db.models.recipe.ingredient import RecipeIngredientModel
|
from mealie.db.models.recipe.ingredient import RecipeIngredientModel
|
||||||
from mealie.db.models.recipe.recipe import RecipeModel
|
from mealie.db.models.recipe.recipe import RecipeModel
|
||||||
@ -155,6 +156,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
tags: list[UUID4 | str] | None = None,
|
tags: list[UUID4 | str] | None = None,
|
||||||
tools: list[UUID4 | str] | None = None,
|
tools: list[UUID4 | str] | None = None,
|
||||||
foods: list[UUID4 | str] | None = None,
|
foods: list[UUID4 | str] | None = None,
|
||||||
|
households: list[UUID4 | str] | None = None,
|
||||||
require_all_categories=True,
|
require_all_categories=True,
|
||||||
require_all_tags=True,
|
require_all_tags=True,
|
||||||
require_all_tools=True,
|
require_all_tools=True,
|
||||||
@ -170,6 +172,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
|
|
||||||
if cookbook:
|
if cookbook:
|
||||||
cb_filters = self._build_recipe_filter(
|
cb_filters = self._build_recipe_filter(
|
||||||
|
households=[cookbook.household_id],
|
||||||
categories=extract_uuids(cookbook.categories),
|
categories=extract_uuids(cookbook.categories),
|
||||||
tags=extract_uuids(cookbook.tags),
|
tags=extract_uuids(cookbook.tags),
|
||||||
tools=extract_uuids(cookbook.tools),
|
tools=extract_uuids(cookbook.tools),
|
||||||
@ -183,11 +186,13 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
category_ids = self._uuids_for_items(categories, Category)
|
category_ids = self._uuids_for_items(categories, Category)
|
||||||
tag_ids = self._uuids_for_items(tags, Tag)
|
tag_ids = self._uuids_for_items(tags, Tag)
|
||||||
tool_ids = self._uuids_for_items(tools, Tool)
|
tool_ids = self._uuids_for_items(tools, Tool)
|
||||||
|
household_ids = self._uuids_for_items(households, Household)
|
||||||
filters = self._build_recipe_filter(
|
filters = self._build_recipe_filter(
|
||||||
categories=category_ids,
|
categories=category_ids,
|
||||||
tags=tag_ids,
|
tags=tag_ids,
|
||||||
tools=tool_ids,
|
tools=tool_ids,
|
||||||
foods=foods,
|
foods=foods,
|
||||||
|
households=household_ids,
|
||||||
require_all_categories=require_all_categories,
|
require_all_categories=require_all_categories,
|
||||||
require_all_tags=require_all_tags,
|
require_all_tags=require_all_tags,
|
||||||
require_all_tools=require_all_tools,
|
require_all_tools=require_all_tools,
|
||||||
@ -245,6 +250,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
tags: list[UUID4] | None = None,
|
tags: list[UUID4] | None = None,
|
||||||
tools: list[UUID4] | None = None,
|
tools: list[UUID4] | None = None,
|
||||||
foods: list[UUID4] | None = None,
|
foods: list[UUID4] | None = None,
|
||||||
|
households: list[UUID4] | None = None,
|
||||||
require_all_categories: bool = True,
|
require_all_categories: bool = True,
|
||||||
require_all_tags: bool = True,
|
require_all_tags: bool = True,
|
||||||
require_all_tools: bool = True,
|
require_all_tools: bool = True,
|
||||||
@ -278,6 +284,8 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
fltr.extend(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id == food) for food in foods)
|
fltr.extend(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id == food) for food in foods)
|
||||||
else:
|
else:
|
||||||
fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id.in_(foods)))
|
fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id.in_(foods)))
|
||||||
|
if households:
|
||||||
|
fltr.append(RecipeModel.household_id.in_(households))
|
||||||
return fltr
|
return fltr
|
||||||
|
|
||||||
def by_category_and_tags(
|
def by_category_and_tags(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends, HTTPException
|
||||||
from pydantic import UUID4, ConfigDict
|
from pydantic import UUID4, ConfigDict
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@ -97,6 +97,12 @@ class BasePublicGroupExploreController(BasePublicController):
|
|||||||
def group_id(self) -> UUID4 | None | NotSet:
|
def group_id(self) -> UUID4 | None | NotSet:
|
||||||
return self.group.id
|
return self.group.id
|
||||||
|
|
||||||
|
def get_public_household(self, household_slug_or_id: str | UUID4) -> HouseholdInDB:
|
||||||
|
household = self.repos.households.get_by_slug_or_id(household_slug_or_id)
|
||||||
|
if not household or household.preferences.private_household:
|
||||||
|
raise HTTPException(404, "household not found")
|
||||||
|
return household
|
||||||
|
|
||||||
def get_explore_url_path(self, endpoint: str) -> str:
|
def get_explore_url_path(self, endpoint: str) -> str:
|
||||||
if endpoint.startswith("/"):
|
if endpoint.startswith("/"):
|
||||||
endpoint = endpoint[1:]
|
endpoint = endpoint[1:]
|
||||||
|
@ -3,6 +3,7 @@ from fastapi import APIRouter
|
|||||||
from . import (
|
from . import (
|
||||||
controller_public_cookbooks,
|
controller_public_cookbooks,
|
||||||
controller_public_foods,
|
controller_public_foods,
|
||||||
|
controller_public_households,
|
||||||
controller_public_organizers,
|
controller_public_organizers,
|
||||||
controller_public_recipes,
|
controller_public_recipes,
|
||||||
)
|
)
|
||||||
@ -11,6 +12,7 @@ router = APIRouter(prefix="/explore/groups/{group_slug}")
|
|||||||
|
|
||||||
# group
|
# group
|
||||||
router.include_router(controller_public_foods.router, tags=["Explore: Foods"])
|
router.include_router(controller_public_foods.router, tags=["Explore: Foods"])
|
||||||
|
router.include_router(controller_public_households.router, tags=["Explore: Households"])
|
||||||
router.include_router(controller_public_organizers.categories_router, tags=["Explore: Categories"])
|
router.include_router(controller_public_organizers.categories_router, tags=["Explore: Categories"])
|
||||||
router.include_router(controller_public_organizers.tags_router, tags=["Explore: Tags"])
|
router.include_router(controller_public_organizers.tags_router, tags=["Explore: Tags"])
|
||||||
router.include_router(controller_public_organizers.tools_router, tags=["Explore: Tools"])
|
router.include_router(controller_public_organizers.tools_router, tags=["Explore: Tools"])
|
||||||
|
35
mealie/routes/explore/controller_public_households.py
Normal file
35
mealie/routes/explore/controller_public_households.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from mealie.routes._base import controller
|
||||||
|
from mealie.routes._base.base_controllers import BasePublicGroupExploreController
|
||||||
|
from mealie.schema.household.household import HouseholdSummary
|
||||||
|
from mealie.schema.make_dependable import make_dependable
|
||||||
|
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/households")
|
||||||
|
|
||||||
|
|
||||||
|
@controller(router)
|
||||||
|
class PublicHouseholdsController(BasePublicGroupExploreController):
|
||||||
|
@property
|
||||||
|
def households(self):
|
||||||
|
return self.repos.households
|
||||||
|
|
||||||
|
@router.get("", response_model=PaginationBase[HouseholdSummary])
|
||||||
|
def get_all(
|
||||||
|
self, q: PaginationQuery = Depends(make_dependable(PaginationQuery))
|
||||||
|
) -> PaginationBase[HouseholdSummary]:
|
||||||
|
public_filter = "(preferences.private_household = FALSE)"
|
||||||
|
if q.query_filter:
|
||||||
|
q.query_filter = f"({q.query_filter}) AND {public_filter}"
|
||||||
|
else:
|
||||||
|
q.query_filter = public_filter
|
||||||
|
|
||||||
|
response = self.households.page_all(pagination=q, override=HouseholdSummary)
|
||||||
|
response.set_pagination_guides(self.get_explore_url_path(router.url_path_for("get_all")), q.model_dump())
|
||||||
|
return response
|
||||||
|
|
||||||
|
@router.get("/{household_slug}", response_model=HouseholdSummary)
|
||||||
|
def get_household(self, household_slug: str) -> HouseholdSummary:
|
||||||
|
household = self.get_public_household(household_slug)
|
||||||
|
return household.cast(HouseholdSummary)
|
@ -37,6 +37,7 @@ class PublicRecipesController(BasePublicHouseholdExploreController):
|
|||||||
tags: list[UUID4 | str] | None = Query(None),
|
tags: list[UUID4 | str] | None = Query(None),
|
||||||
tools: list[UUID4 | str] | None = Query(None),
|
tools: list[UUID4 | str] | None = Query(None),
|
||||||
foods: list[UUID4 | str] | None = Query(None),
|
foods: list[UUID4 | str] | None = Query(None),
|
||||||
|
households: list[UUID4 | str] | None = Query(None),
|
||||||
) -> PaginationBase[RecipeSummary]:
|
) -> PaginationBase[RecipeSummary]:
|
||||||
cookbook_data: ReadCookBook | None = None
|
cookbook_data: ReadCookBook | None = None
|
||||||
recipes_repo = self.cross_household_recipes
|
recipes_repo = self.cross_household_recipes
|
||||||
@ -76,6 +77,7 @@ class PublicRecipesController(BasePublicHouseholdExploreController):
|
|||||||
tags=tags,
|
tags=tags,
|
||||||
tools=tools,
|
tools=tools,
|
||||||
foods=foods,
|
foods=foods,
|
||||||
|
households=households,
|
||||||
require_all_categories=search_query.require_all_categories,
|
require_all_categories=search_query.require_all_categories,
|
||||||
require_all_tags=search_query.require_all_tags,
|
require_all_tags=search_query.require_all_tags,
|
||||||
require_all_tools=search_query.require_all_tools,
|
require_all_tools=search_query.require_all_tools,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
controller_group_households,
|
||||||
controller_group_reports,
|
controller_group_reports,
|
||||||
controller_group_self_service,
|
controller_group_self_service,
|
||||||
controller_labels,
|
controller_labels,
|
||||||
@ -10,6 +11,7 @@ from . import (
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(controller_group_households.router)
|
||||||
router.include_router(controller_group_self_service.router)
|
router.include_router(controller_group_self_service.router)
|
||||||
router.include_router(controller_migrations.router)
|
router.include_router(controller_migrations.router)
|
||||||
router.include_router(controller_group_reports.router)
|
router.include_router(controller_group_reports.router)
|
||||||
|
27
mealie/routes/groups/controller_group_households.py
Normal file
27
mealie/routes/groups/controller_group_households.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
|
||||||
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
|
from mealie.routes._base.controller import controller
|
||||||
|
from mealie.routes._base.routers import UserAPIRouter
|
||||||
|
from mealie.schema.household.household import HouseholdSummary
|
||||||
|
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||||
|
|
||||||
|
router = UserAPIRouter(prefix="/groups/households", tags=["Groups: Households"])
|
||||||
|
|
||||||
|
|
||||||
|
@controller(router)
|
||||||
|
class GroupHouseholdsController(BaseUserController):
|
||||||
|
@router.get("", response_model=PaginationBase[HouseholdSummary])
|
||||||
|
def get_all_households(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
|
response = self.repos.households.page_all(pagination=q, override=HouseholdSummary)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all_households"), q.model_dump())
|
||||||
|
return response
|
||||||
|
|
||||||
|
@router.get("/{household_slug}", response_model=HouseholdSummary)
|
||||||
|
def get_one_household(self, household_slug: str):
|
||||||
|
household = self.repos.households.get_by_slug_or_id(household_slug)
|
||||||
|
|
||||||
|
if not household:
|
||||||
|
raise HTTPException(status_code=404, detail="Household not found")
|
||||||
|
return household.cast(HouseholdSummary)
|
@ -1,6 +1,6 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import HTTPException, Query
|
from fastapi import Query
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
@ -8,9 +8,7 @@ from mealie.routes._base.controller import controller
|
|||||||
from mealie.routes._base.routers import UserAPIRouter
|
from mealie.routes._base.routers import UserAPIRouter
|
||||||
from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
||||||
from mealie.schema.group.group_statistics import GroupStorage
|
from mealie.schema.group.group_statistics import GroupStorage
|
||||||
from mealie.schema.household.household import HouseholdSummary
|
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import ErrorResponse
|
|
||||||
from mealie.schema.user.user import GroupSummary, UserSummary
|
from mealie.schema.user.user import GroupSummary, UserSummary
|
||||||
from mealie.services.group_services.group_service import GroupService
|
from mealie.services.group_services.group_service import GroupService
|
||||||
|
|
||||||
@ -36,23 +34,6 @@ class GroupSelfServiceController(BaseUserController):
|
|||||||
private_users = self.repos.users.page_all(PaginationQuery(page=1, per_page=-1, query_filter=query_filter)).items
|
private_users = self.repos.users.page_all(PaginationQuery(page=1, per_page=-1, query_filter=query_filter)).items
|
||||||
return [user.cast(UserSummary) for user in private_users]
|
return [user.cast(UserSummary) for user in private_users]
|
||||||
|
|
||||||
@router.get("/households", response_model=list[HouseholdSummary])
|
|
||||||
def get_group_households(self):
|
|
||||||
"""Returns all households belonging to the current group"""
|
|
||||||
|
|
||||||
households = self.repos.households.page_all(PaginationQuery(page=1, per_page=-1)).items
|
|
||||||
return [household.cast(HouseholdSummary) for household in households]
|
|
||||||
|
|
||||||
@router.get("/households/{slug}", response_model=HouseholdSummary)
|
|
||||||
def get_group_household(self, slug: str):
|
|
||||||
"""Returns a single household belonging to the current group"""
|
|
||||||
|
|
||||||
household = self.repos.households.get_by_slug_or_id(slug)
|
|
||||||
if not household:
|
|
||||||
raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found"))
|
|
||||||
|
|
||||||
return household.cast(HouseholdSummary)
|
|
||||||
|
|
||||||
@router.get("/preferences", response_model=ReadGroupPreferences)
|
@router.get("/preferences", response_model=ReadGroupPreferences)
|
||||||
def get_group_preferences(self):
|
def get_group_preferences(self):
|
||||||
return self.group.preferences
|
return self.group.preferences
|
||||||
|
@ -320,6 +320,7 @@ class RecipeController(BaseRecipeController):
|
|||||||
tags: list[UUID4 | str] | None = Query(None),
|
tags: list[UUID4 | str] | None = Query(None),
|
||||||
tools: list[UUID4 | str] | None = Query(None),
|
tools: list[UUID4 | str] | None = Query(None),
|
||||||
foods: list[UUID4 | str] | None = Query(None),
|
foods: list[UUID4 | str] | None = Query(None),
|
||||||
|
households: list[UUID4 | str] | None = Query(None),
|
||||||
):
|
):
|
||||||
cookbook_data: ReadCookBook | None = None
|
cookbook_data: ReadCookBook | None = None
|
||||||
if search_query.cookbook:
|
if search_query.cookbook:
|
||||||
@ -345,6 +346,7 @@ class RecipeController(BaseRecipeController):
|
|||||||
tags=tags,
|
tags=tags,
|
||||||
tools=tools,
|
tools=tools,
|
||||||
foods=foods,
|
foods=foods,
|
||||||
|
households=households,
|
||||||
require_all_categories=search_query.require_all_categories,
|
require_all_categories=search_query.require_all_categories,
|
||||||
require_all_tags=search_query.require_all_tags,
|
require_all_tags=search_query.require_all_tags,
|
||||||
require_all_tools=search_query.require_all_tools,
|
require_all_tools=search_query.require_all_tools,
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from mealie.schema.household.household import HouseholdCreate
|
||||||
|
from mealie.schema.household.household_preferences import CreateHouseholdPreferences
|
||||||
|
from mealie.services.household_services.household_service import HouseholdService
|
||||||
|
from tests.utils import api_routes
|
||||||
|
from tests.utils.factories import random_string
|
||||||
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_private_group", [True, False])
|
||||||
|
def test_get_all_households(api_client: TestClient, unique_user: TestUser, is_private_group: bool):
|
||||||
|
unique_user.repos.group_preferences.patch(UUID(unique_user.group_id), {"private_group": is_private_group})
|
||||||
|
households = [
|
||||||
|
HouseholdService.create_household(
|
||||||
|
unique_user.repos,
|
||||||
|
HouseholdCreate(name=random_string()),
|
||||||
|
CreateHouseholdPreferences(private_household=False),
|
||||||
|
)
|
||||||
|
for _ in range(5)
|
||||||
|
]
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.explore_groups_group_slug_households(unique_user.group_id))
|
||||||
|
if is_private_group:
|
||||||
|
assert response.status_code == 404
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_ids = [item["id"] for item in response.json()["items"]]
|
||||||
|
for household in households:
|
||||||
|
assert str(household.id) in response_ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_private_group", [True, False])
|
||||||
|
def test_get_all_households_public_only(api_client: TestClient, unique_user: TestUser, is_private_group: bool):
|
||||||
|
unique_user.repos.group_preferences.patch(UUID(unique_user.group_id), {"private_group": is_private_group})
|
||||||
|
public_household = HouseholdService.create_household(
|
||||||
|
unique_user.repos,
|
||||||
|
HouseholdCreate(name=random_string()),
|
||||||
|
CreateHouseholdPreferences(private_household=False),
|
||||||
|
)
|
||||||
|
private_household = HouseholdService.create_household(
|
||||||
|
unique_user.repos,
|
||||||
|
HouseholdCreate(name=random_string()),
|
||||||
|
CreateHouseholdPreferences(private_household=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.explore_groups_group_slug_households(unique_user.group_id))
|
||||||
|
if is_private_group:
|
||||||
|
assert response.status_code == 404
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_ids = [item["id"] for item in response.json()["items"]]
|
||||||
|
assert str(public_household.id) in response_ids
|
||||||
|
assert str(private_household.id) not in response_ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_private_group", [True, False])
|
||||||
|
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||||
|
def test_get_household(
|
||||||
|
api_client: TestClient, unique_user: TestUser, is_private_group: bool, is_private_household: bool
|
||||||
|
):
|
||||||
|
unique_user.repos.group_preferences.patch(UUID(unique_user.group_id), {"private_group": is_private_group})
|
||||||
|
household = household = HouseholdService.create_household(
|
||||||
|
unique_user.repos,
|
||||||
|
HouseholdCreate(name=random_string()),
|
||||||
|
CreateHouseholdPreferences(private_household=is_private_household),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
api_routes.explore_groups_group_slug_households_household_slug(unique_user.group_id, household.slug),
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_private_group or is_private_household:
|
||||||
|
assert response.status_code == 404
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["id"] == str(household.id)
|
@ -1,3 +1,5 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from mealie.repos.repository_factory import AllRepositories
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
@ -33,13 +35,10 @@ def test_get_group_members_filtered(api_client: TestClient, unique_user: TestUse
|
|||||||
assert str(h2_user.user_id) in all_ids
|
assert str(h2_user.user_id) in all_ids
|
||||||
|
|
||||||
|
|
||||||
def test_get_households(unfiltered_database: AllRepositories, api_client: TestClient, unique_user: TestUser):
|
def test_get_households(api_client: TestClient, unique_user: TestUser):
|
||||||
households = [
|
households = [unique_user.repos.households.create({"name": random_string()}) for _ in range(5)]
|
||||||
unfiltered_database.households.create({"name": random_string(), "group_id": unique_user.group_id})
|
|
||||||
for _ in range(5)
|
|
||||||
]
|
|
||||||
response = api_client.get(api_routes.groups_households, headers=unique_user.token)
|
response = api_client.get(api_routes.groups_households, headers=unique_user.token)
|
||||||
response_ids = [item["id"] for item in response.json()]
|
response_ids = [item["id"] for item in response.json()["items"]]
|
||||||
for household in households:
|
for household in households:
|
||||||
assert str(household.id) in response_ids
|
assert str(household.id) in response_ids
|
||||||
|
|
||||||
@ -58,23 +57,22 @@ def test_get_households_filtered(unfiltered_database: AllRepositories, api_clien
|
|||||||
]
|
]
|
||||||
|
|
||||||
response = api_client.get(api_routes.groups_households, headers=unique_user.token)
|
response = api_client.get(api_routes.groups_households, headers=unique_user.token)
|
||||||
response_ids = [item["id"] for item in response.json()]
|
response_ids = [item["id"] for item in response.json()["items"]]
|
||||||
for household in group_1_households:
|
for household in group_1_households:
|
||||||
assert str(household.id) in response_ids
|
assert str(household.id) in response_ids
|
||||||
for household in group_2_households:
|
for household in group_2_households:
|
||||||
assert str(household.id) not in response_ids
|
assert str(household.id) not in response_ids
|
||||||
|
|
||||||
|
|
||||||
def test_get_household(unfiltered_database: AllRepositories, api_client: TestClient, unique_user: TestUser):
|
def test_get_one_household(api_client: TestClient, unique_user: TestUser):
|
||||||
group_1_id = unique_user.group_id
|
households = [unique_user.repos.households.create({"name": random_string()}) for _ in range(5)]
|
||||||
group_2_id = str(unfiltered_database.groups.create({"name": random_string()}).id)
|
household = random.choice(households)
|
||||||
|
|
||||||
group_1_household = unfiltered_database.households.create({"name": random_string(), "group_id": group_1_id})
|
response = api_client.get(api_routes.groups_households_household_slug(household.slug), headers=unique_user.token)
|
||||||
group_2_household = unfiltered_database.households.create({"name": random_string(), "group_id": group_2_id})
|
|
||||||
|
|
||||||
response = api_client.get(api_routes.groups_households_slug(group_1_household.slug), headers=unique_user.token)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["id"] == str(group_1_household.id)
|
assert response.json()["id"] == str(household.id)
|
||||||
|
|
||||||
response = api_client.get(api_routes.groups_households_slug(group_2_household.slug), headers=unique_user.token)
|
|
||||||
|
def test_get_one_household_not_found(api_client: TestClient, unique_user: TestUser):
|
||||||
|
response = api_client.get(api_routes.groups_households_household_slug(random_string()), headers=unique_user.token)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import random
|
import random
|
||||||
|
from collections.abc import Generator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -35,19 +36,20 @@ class TestCookbook:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def cookbooks(unique_user: TestUser) -> list[TestCookbook]:
|
def cookbooks(unique_user: TestUser) -> Generator[list[TestCookbook]]:
|
||||||
database = unique_user.repos
|
database = unique_user.repos
|
||||||
|
|
||||||
data: list[ReadCookBook] = []
|
data: list[ReadCookBook] = []
|
||||||
yield_data: list[TestCookbook] = []
|
yield_data: list[TestCookbook] = []
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
cb = database.cookbooks.create(SaveCookBook(**get_page_data(unique_user.group_id, unique_user.household_id)))
|
cb = database.cookbooks.create(SaveCookBook(**get_page_data(unique_user.group_id, unique_user.household_id)))
|
||||||
|
assert cb.slug
|
||||||
data.append(cb)
|
data.append(cb)
|
||||||
yield_data.append(TestCookbook(id=cb.id, slug=cb.slug, name=cb.name, data=cb.model_dump()))
|
yield_data.append(TestCookbook(id=cb.id, slug=cb.slug, name=cb.name, data=cb.model_dump()))
|
||||||
|
|
||||||
yield yield_data
|
yield yield_data
|
||||||
|
|
||||||
for cb in yield_data:
|
for cb in data:
|
||||||
try:
|
try:
|
||||||
database.cookbooks.delete(cb.id)
|
database.cookbooks.delete(cb.id)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -3,6 +3,9 @@ from datetime import datetime, timezone
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from mealie.schema.cookbook.cookbook import SaveCookBook
|
||||||
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
|
from mealie.schema.recipe.recipe_category import TagSave
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.factories import random_string
|
from tests.utils.factories import random_string
|
||||||
from tests.utils.fixture_schemas import TestUser
|
from tests.utils.fixture_schemas import TestUser
|
||||||
@ -65,6 +68,38 @@ def test_get_all_recipes_includes_all_households(
|
|||||||
assert str(h2_recipe_id) in response_ids
|
assert str(h2_recipe_id) in response_ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||||
|
def test_get_all_recipes_with_household_filter(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||||
|
):
|
||||||
|
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||||
|
assert household and household.preferences
|
||||||
|
household.preferences.private_household = is_private_household
|
||||||
|
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||||
|
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
recipe = unique_user.repos.recipes.get_one(response.json())
|
||||||
|
assert recipe and recipe.id
|
||||||
|
recipe_id = recipe.id
|
||||||
|
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
h2_recipe = h2_user.repos.recipes.get_one(response.json())
|
||||||
|
assert h2_recipe and h2_recipe.id
|
||||||
|
h2_recipe_id = h2_recipe.id
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
api_routes.recipes,
|
||||||
|
params={"households": [h2_recipe.household_id], "page": 1, "perPage": -1},
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_ids = {recipe["id"] for recipe in response.json()["items"]}
|
||||||
|
assert str(recipe_id) not in response_ids
|
||||||
|
assert str(h2_recipe_id) in response_ids
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||||
def test_get_one_recipe_from_another_household(
|
def test_get_one_recipe_from_another_household(
|
||||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||||
@ -220,3 +255,49 @@ def test_user_can_update_last_made_on_other_household(
|
|||||||
assert recipe["id"] == str(h2_recipe_id)
|
assert recipe["id"] == str(h2_recipe_id)
|
||||||
new_last_made = recipe["lastMade"]
|
new_last_made = recipe["lastMade"]
|
||||||
assert new_last_made == now != old_last_made
|
assert new_last_made == now != old_last_made
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookbook_recipes_only_includes_current_households(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser
|
||||||
|
):
|
||||||
|
tag = unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
|
||||||
|
recipes = unique_user.repos.recipes.create_many(
|
||||||
|
[
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
tags=[tag],
|
||||||
|
)
|
||||||
|
for _ in range(3)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
other_recipes = h2_user.repos.recipes.create_many(
|
||||||
|
[
|
||||||
|
Recipe(
|
||||||
|
user_id=h2_user.user_id,
|
||||||
|
group_id=h2_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
)
|
||||||
|
for _ in range(3)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
cookbook = unique_user.repos.cookbooks.create(
|
||||||
|
SaveCookBook(
|
||||||
|
name=random_string(),
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
household_id=unique_user.household_id,
|
||||||
|
tags=[tag],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes, params={"cookbook": cookbook.slug}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
recipes = [Recipe.model_validate(data) for data in response.json()["items"]]
|
||||||
|
|
||||||
|
fetched_recipe_ids = {recipe.id for recipe in recipes}
|
||||||
|
for recipe in recipes:
|
||||||
|
assert recipe.id in fetched_recipe_ids
|
||||||
|
for recipe in other_recipes:
|
||||||
|
assert recipe.id not in fetched_recipe_ids
|
||||||
|
@ -20,6 +20,7 @@ from recipe_scrapers.plugins import SchemaOrgFillPlugin
|
|||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
from mealie.pkgs.safehttp.transport import AsyncSafeTransport
|
from mealie.pkgs.safehttp.transport import AsyncSafeTransport
|
||||||
|
from mealie.schema.cookbook.cookbook import SaveCookBook
|
||||||
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipeSummary, RecipeTag
|
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipeSummary, RecipeTag
|
||||||
from mealie.schema.recipe.recipe_category import CategorySave, TagSave
|
from mealie.schema.recipe.recipe_category import CategorySave, TagSave
|
||||||
from mealie.schema.recipe.recipe_notes import RecipeNote
|
from mealie.schema.recipe.recipe_notes import RecipeNote
|
||||||
@ -791,3 +792,47 @@ def test_get_random_order(api_client: TestClient, unique_user: utils.TestUser):
|
|||||||
badparams: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random"}
|
badparams: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random"}
|
||||||
response = api_client.get(api_routes.recipes, params=badparams, headers=unique_user.token)
|
response = api_client.get(api_routes.recipes, params=badparams, headers=unique_user.token)
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_cookbook_recipes(api_client: TestClient, unique_user: utils.TestUser):
|
||||||
|
tag = unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
|
||||||
|
cookbook_recipes = unique_user.repos.recipes.create_many(
|
||||||
|
[
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
tags=[tag],
|
||||||
|
)
|
||||||
|
for _ in range(3)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
other_recipes = unique_user.repos.recipes.create_many(
|
||||||
|
[
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
)
|
||||||
|
for _ in range(3)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
cookbook = unique_user.repos.cookbooks.create(
|
||||||
|
SaveCookBook(
|
||||||
|
name=random_string(),
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
household_id=unique_user.household_id,
|
||||||
|
tags=[tag],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes, params={"cookbook": cookbook.slug}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
recipes = [Recipe.model_validate(data) for data in response.json()["items"]]
|
||||||
|
|
||||||
|
fetched_recipe_ids = {recipe.id for recipe in recipes}
|
||||||
|
for recipe in cookbook_recipes:
|
||||||
|
assert recipe.id in fetched_recipe_ids
|
||||||
|
for recipe in other_recipes:
|
||||||
|
assert recipe.id not in fetched_recipe_ids
|
||||||
|
@ -247,6 +247,16 @@ def explore_groups_group_slug_foods_item_id(group_slug, item_id):
|
|||||||
return f"{prefix}/explore/groups/{group_slug}/foods/{item_id}"
|
return f"{prefix}/explore/groups/{group_slug}/foods/{item_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def explore_groups_group_slug_households(group_slug):
|
||||||
|
"""`/api/explore/groups/{group_slug}/households`"""
|
||||||
|
return f"{prefix}/explore/groups/{group_slug}/households"
|
||||||
|
|
||||||
|
|
||||||
|
def explore_groups_group_slug_households_household_slug(group_slug, household_slug):
|
||||||
|
"""`/api/explore/groups/{group_slug}/households/{household_slug}`"""
|
||||||
|
return f"{prefix}/explore/groups/{group_slug}/households/{household_slug}"
|
||||||
|
|
||||||
|
|
||||||
def explore_groups_group_slug_organizers_categories(group_slug):
|
def explore_groups_group_slug_organizers_categories(group_slug):
|
||||||
"""`/api/explore/groups/{group_slug}/organizers/categories`"""
|
"""`/api/explore/groups/{group_slug}/organizers/categories`"""
|
||||||
return f"{prefix}/explore/groups/{group_slug}/organizers/categories"
|
return f"{prefix}/explore/groups/{group_slug}/organizers/categories"
|
||||||
@ -292,9 +302,9 @@ def foods_item_id(item_id):
|
|||||||
return f"{prefix}/foods/{item_id}"
|
return f"{prefix}/foods/{item_id}"
|
||||||
|
|
||||||
|
|
||||||
def groups_households_slug(slug):
|
def groups_households_household_slug(household_slug):
|
||||||
"""`/api/groups/households/{slug}`"""
|
"""`/api/groups/households/{household_slug}`"""
|
||||||
return f"{prefix}/groups/households/{slug}"
|
return f"{prefix}/groups/households/{household_slug}"
|
||||||
|
|
||||||
|
|
||||||
def groups_labels_item_id(item_id):
|
def groups_labels_item_id(item_id):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user