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()"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<div v-if="recipes" class="mt-2">
|
||||
<v-row v-if="!useMobileCards">
|
||||
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
||||
<v-lazy>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col
|
||||
v-for="recipe in recipes"
|
||||
:key="recipe.name"
|
||||
cols="12"
|
||||
:sm="singleColumn ? '12' : '12'"
|
||||
:md="singleColumn ? '12' : '6'"
|
||||
:lg="singleColumn ? '12' : '4'"
|
||||
:xl="singleColumn ? '12' : '3'"
|
||||
>
|
||||
<v-lazy>
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-if="recipes && ready">
|
||||
<div class="mt-2">
|
||||
<v-row v-if="!useMobileCards">
|
||||
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
||||
<v-lazy>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col
|
||||
v-for="recipe in recipes"
|
||||
:key="recipe.name"
|
||||
cols="12"
|
||||
:sm="singleColumn ? '12' : '12'"
|
||||
:md="singleColumn ? '12' : '6'"
|
||||
:lg="singleColumn ? '12' : '4'"
|
||||
:xl="singleColumn ? '12' : '3'"
|
||||
>
|
||||
<v-lazy>
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</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>
|
||||
<v-card v-intersect="infiniteScroll"></v-card>
|
||||
<v-fade-transition>
|
||||
<AppLoader v-if="loading" :loading="loading" />
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -223,36 +225,42 @@ export default defineComponent({
|
||||
|
||||
const queryFilter = computed(() => {
|
||||
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) {
|
||||
return await fetchMore(
|
||||
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,
|
||||
props.query?.orderBy || preferences.value.orderBy,
|
||||
props.query?.orderDirection || preferences.value.orderDirection,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.query) {
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
}
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
});
|
||||
|
||||
let lastQuery: string | undefined;
|
||||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||
watch(
|
||||
() => props.query,
|
||||
async (newValue: RecipeSearchQuery | undefined) => {
|
||||
const newValueString = JSON.stringify(newValue)
|
||||
if (newValue && (!ready.value || lastQuery !== newValueString)) {
|
||||
if (lastQuery !== newValueString) {
|
||||
lastQuery = newValueString;
|
||||
ready.value = false;
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
}
|
||||
@ -261,8 +269,12 @@ export default defineComponent({
|
||||
|
||||
async function initRecipes() {
|
||||
page.value = 1;
|
||||
const newRecipes = await fetchRecipes(2);
|
||||
if (!newRecipes.length) {
|
||||
hasMore.value = true;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -274,7 +286,7 @@ export default defineComponent({
|
||||
|
||||
const infiniteScroll = useThrottleFn(() => {
|
||||
useAsync(async () => {
|
||||
if (!ready.value || !hasMore.value || loading.value) {
|
||||
if (!hasMore.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -282,9 +294,10 @@ export default defineComponent({
|
||||
page.value = page.value + 1;
|
||||
|
||||
const newRecipes = await fetchRecipes();
|
||||
if (!newRecipes.length) {
|
||||
if (newRecipes.length < perPage) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
}
|
||||
if (newRecipes.length) {
|
||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
@ -379,6 +392,7 @@ export default defineComponent({
|
||||
displayTitleIcon,
|
||||
EVENTS,
|
||||
infiniteScroll,
|
||||
ready,
|
||||
loading,
|
||||
navigateRandom,
|
||||
preferences,
|
||||
|
@ -3,6 +3,8 @@
|
||||
v-model="selected"
|
||||
item-key="id"
|
||||
show-select
|
||||
sort-by="dateAdded"
|
||||
sort-desc
|
||||
:headers="headers"
|
||||
:items="recipes"
|
||||
:items-per-page="15"
|
||||
@ -39,6 +41,9 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #item.dateAdded="{ item }">
|
||||
{{ formatDate(item.dateAdded) }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
@ -132,6 +137,14 @@ export default defineComponent({
|
||||
return hdrs;
|
||||
});
|
||||
|
||||
function formatDate(date: string) {
|
||||
try {
|
||||
return i18n.d(Date.parse(date), "medium");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Group Members
|
||||
const api = useUserApi();
|
||||
@ -160,6 +173,7 @@ export default defineComponent({
|
||||
groupSlug,
|
||||
setValue,
|
||||
headers,
|
||||
formatDate,
|
||||
members,
|
||||
getMember,
|
||||
};
|
||||
|
@ -53,6 +53,14 @@
|
||||
{{ $t("general.foods") }}
|
||||
</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 -->
|
||||
<v-menu offset-y nudge-bottom="3">
|
||||
<template #activator="{ on, attrs }">
|
||||
@ -142,17 +150,25 @@ import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref,
|
||||
import { watchDebounced } from "@vueuse/shared";
|
||||
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
||||
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 RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||
import { usePublicCategoryStore } from "~/composables/store/use-category-store";
|
||||
import { usePublicFoodStore } from "~/composables/store/use-food-store";
|
||||
import { usePublicTagStore } from "~/composables/store/use-tag-store";
|
||||
import { usePublicToolStore } from "~/composables/store/use-tool-store";
|
||||
import { HouseholdSummary } from "~/lib/api/types/household";
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchFilter, RecipeCardSection },
|
||||
@ -186,6 +202,9 @@ export default defineComponent({
|
||||
const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||
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 selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
||||
|
||||
@ -199,6 +218,7 @@ export default defineComponent({
|
||||
search: state.value.search ? state.value.search : "",
|
||||
categories: toIDArray(selectedCategories.value),
|
||||
foods: toIDArray(selectedFoods.value),
|
||||
households: toIDArray(selectedHouseholds.value),
|
||||
tags: toIDArray(selectedTags.value),
|
||||
tools: toIDArray(selectedTools.value),
|
||||
requireAllCategories: state.value.requireAllCategories,
|
||||
@ -239,10 +259,9 @@ export default defineComponent({
|
||||
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
||||
selectedCategories.value = [];
|
||||
selectedFoods.value = [];
|
||||
selectedHouseholds.value = [];
|
||||
selectedTags.value = [];
|
||||
selectedTools.value = [];
|
||||
|
||||
search();
|
||||
}
|
||||
|
||||
function toggleOrderDirection() {
|
||||
@ -280,6 +299,7 @@ export default defineComponent({
|
||||
search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search,
|
||||
orderBy: passedQuery.value.orderBy === queryDefaults.orderBy ? undefined : passedQuery.value.orderBy,
|
||||
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,
|
||||
requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
|
||||
requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
|
||||
@ -361,13 +381,10 @@ export default defineComponent({
|
||||
watch(
|
||||
() => route.value.query,
|
||||
() => {
|
||||
if (state.value.ready) {
|
||||
hydrateSearch();
|
||||
if (!Object.keys(route.value.query).length) {
|
||||
reset();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
async function hydrateSearch() {
|
||||
@ -423,9 +440,9 @@ export default defineComponent({
|
||||
if (query.categories?.length) {
|
||||
promises.push(
|
||||
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)
|
||||
);
|
||||
|
||||
@ -440,9 +457,9 @@ export default defineComponent({
|
||||
if (query.tags?.length) {
|
||||
promises.push(
|
||||
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>[];
|
||||
}
|
||||
)
|
||||
@ -454,9 +471,9 @@ export default defineComponent({
|
||||
if (query.tools?.length) {
|
||||
promises.push(
|
||||
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>[];
|
||||
}
|
||||
)
|
||||
@ -469,13 +486,13 @@ export default defineComponent({
|
||||
promises.push(
|
||||
waitUntilAndExecute(
|
||||
() => {
|
||||
if (foods.foods.value) {
|
||||
return foods.foods.value.length > 0;
|
||||
if (foods.store.value) {
|
||||
return foods.store.value.length > 0;
|
||||
}
|
||||
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 ?? [];
|
||||
}
|
||||
)
|
||||
@ -484,6 +501,25 @@ export default defineComponent({
|
||||
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);
|
||||
};
|
||||
|
||||
@ -515,6 +551,7 @@ export default defineComponent({
|
||||
() => state.value.orderDirection,
|
||||
selectedCategories,
|
||||
selectedFoods,
|
||||
selectedHouseholds,
|
||||
selectedTags,
|
||||
selectedTools,
|
||||
],
|
||||
@ -533,10 +570,11 @@ export default defineComponent({
|
||||
search,
|
||||
reset,
|
||||
state,
|
||||
categories: categories.items as unknown as NoUndefinedField<RecipeCategory>[],
|
||||
tags: tags.items as unknown as NoUndefinedField<RecipeTag>[],
|
||||
foods: foods.foods,
|
||||
tools: tools.items as unknown as NoUndefinedField<RecipeTool>[],
|
||||
categories: categories.store as unknown as NoUndefinedField<RecipeCategory>[],
|
||||
tags: tags.store as unknown as NoUndefinedField<RecipeTag>[],
|
||||
foods: foods.store,
|
||||
tools: tools.store as unknown as NoUndefinedField<RecipeTool>[],
|
||||
households: households.store as unknown as NoUndefinedField<HouseholdSummary>[],
|
||||
|
||||
sortable,
|
||||
toggleOrderDirection,
|
||||
@ -545,6 +583,7 @@ export default defineComponent({
|
||||
|
||||
selectedCategories,
|
||||
selectedFoods,
|
||||
selectedHouseholds,
|
||||
selectedTags,
|
||||
selectedTools,
|
||||
appendRecipes,
|
||||
|
@ -289,11 +289,11 @@ export default defineComponent({
|
||||
createAssignFood,
|
||||
unitAutocomplete,
|
||||
createAssignUnit,
|
||||
foods: foodStore.foods,
|
||||
foods: foodStore.store,
|
||||
foodSearch,
|
||||
toggleTitle,
|
||||
unitActions: unitStore.actions,
|
||||
units: unitStore.units,
|
||||
units: unitStore.store,
|
||||
unitSearch,
|
||||
validators,
|
||||
workingUnitData: unitsData.data,
|
||||
|
@ -135,7 +135,7 @@ export default defineComponent({
|
||||
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);
|
||||
dialog.value = false;
|
||||
|
@ -127,9 +127,9 @@ export default defineComponent({
|
||||
|
||||
const items = computed(() => {
|
||||
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) {
|
||||
|
@ -105,7 +105,7 @@ export default defineComponent({
|
||||
const recipeHousehold = ref<HouseholdSummary>();
|
||||
if (user) {
|
||||
const userApi = useUserApi();
|
||||
userApi.groups.fetchHousehold(props.recipe.householdId).then(({ data }) => {
|
||||
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
||||
recipeHousehold.value = data || undefined;
|
||||
});
|
||||
}
|
||||
|
@ -11,28 +11,43 @@
|
||||
<v-card width="400">
|
||||
<v-card-text>
|
||||
<v-text-field v-model="state.search" class="mb-2" hide-details dense :label="$tc('search.search')" clearable />
|
||||
<v-switch
|
||||
v-if="requireAll != undefined"
|
||||
v-model="requireAllValue"
|
||||
dense
|
||||
small
|
||||
:label="`${requireAll ? $tc('search.has-all') : $tc('search.has-any')}`"
|
||||
>
|
||||
</v-switch>
|
||||
<div class="d-flex py-4">
|
||||
<v-switch
|
||||
v-if="requireAll != undefined"
|
||||
v-model="requireAllValue"
|
||||
dense
|
||||
small
|
||||
hide-details
|
||||
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-radio-group v-model="selectedRadio" class="ma-0 pa-0">
|
||||
<v-virtual-scroll :items="filtered" height="300" item-height="51">
|
||||
<template #default="{ item }">
|
||||
<v-list-item :key="item.id" dense :value="item">
|
||||
<v-list-item-action>
|
||||
<v-checkbox v-model="selected" :value="item"></v-checkbox>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ item.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item :key="item.id" dense :value="item">
|
||||
<v-list-item-action>
|
||||
<v-radio v-if="radio" :value="item" @click="handleRadioClick(item)" />
|
||||
<v-checkbox v-else v-model="selected" :value="item" />
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</v-radio-group>
|
||||
</v-card>
|
||||
<div v-else>
|
||||
<v-alert type="info" text> {{ $tc('search.no-results') }} </v-alert>
|
||||
@ -65,6 +80,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
radio: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
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(() => {
|
||||
if (!state.search) {
|
||||
return props.items;
|
||||
@ -94,11 +120,26 @@ export default defineComponent({
|
||||
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 {
|
||||
requireAllValue,
|
||||
state,
|
||||
selected,
|
||||
selectedRadio,
|
||||
filtered,
|
||||
handleRadioClick,
|
||||
clearSelection,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -44,6 +44,8 @@
|
||||
item-key="id"
|
||||
:show-select="bulkActions.length > 0"
|
||||
:headers="activeHeaders"
|
||||
:sort-by="initialSort"
|
||||
:sort-desc="initialSortDesc"
|
||||
:items="data || []"
|
||||
:items-per-page="15"
|
||||
:search="search"
|
||||
@ -126,6 +128,14 @@ export default defineComponent({
|
||||
type: Array as () => BulkAction[],
|
||||
default: () => [],
|
||||
},
|
||||
initialSort: {
|
||||
type: String,
|
||||
default: "id",
|
||||
},
|
||||
initialSortDesc: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
// ===========================================================
|
||||
|
@ -1,3 +1,3 @@
|
||||
export { useAppInfo } from "./use-app-info";
|
||||
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 { useAsyncKey } from "../use-utils";
|
||||
import { BoundT } from "./types";
|
||||
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||
import { QueryValue } from "~/lib/api/base/route";
|
||||
|
||||
type BoundT = {
|
||||
id?: string | number | null;
|
||||
};
|
||||
|
||||
interface PublicStoreActions<T extends BoundT> {
|
||||
interface ReadOnlyStoreActions<T extends BoundT> {
|
||||
getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>;
|
||||
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>;
|
||||
updateOne(updateData: T): 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
|
||||
* Vuex. This is primarily used for basic GET/GETALL operations that required
|
||||
* 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>,
|
||||
allRef: Ref<T[] | null> | null,
|
||||
loading: Ref<boolean>
|
||||
): PublicStoreActions<T> {
|
||||
): ReadOnlyStoreActions<T> {
|
||||
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
params.orderBy ??= "name";
|
||||
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
|
||||
search: query?.search,
|
||||
cookbook: query?.cookbook,
|
||||
households: query?.households,
|
||||
categories: query?.categories,
|
||||
requireAllCategories: query?.requireAllCategories,
|
||||
tags: query?.tags,
|
||||
|
@ -1,6 +1,7 @@
|
||||
export { useFoodStore, useFoodData } from "./use-food-store";
|
||||
export { useUnitStore, useUnitData } from "./use-unit-store";
|
||||
export { useCategoryStore, usePublicCategoryStore, useCategoryData } from "./use-category-store";
|
||||
export { useFoodStore, usePublicFoodStore, useFoodData } from "./use-food-store";
|
||||
export { useHouseholdStore, usePublicHouseholdStore } from "./use-household-store";
|
||||
export { useLabelStore, useLabelData } from "./use-label-store";
|
||||
export { useToolStore, useToolData } from "./use-tool-store";
|
||||
export { useCategoryStore, useCategoryData } from "./use-category-store";
|
||||
export { useTagStore, useTagData } from "./use-tag-store";
|
||||
export { useTagStore, usePublicTagStore, useTagData } from "./use-tag-store";
|
||||
export { useToolStore, usePublicToolStore, useToolData } from "./use-tool-store";
|
||||
export { useUnitStore, useUnitData } from "./use-unit-store";
|
||||
|
@ -1,73 +1,26 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
||||
import { usePublicExploreApi } from "../api/api-client";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||
import { RecipeCategory } from "~/lib/api/types/recipe";
|
||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const categoryStore: Ref<RecipeCategory[]> = ref([]);
|
||||
const publicStoreLoading = ref(false);
|
||||
const storeLoading = ref(false);
|
||||
const store: Ref<RecipeCategory[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
|
||||
export function useCategoryData() {
|
||||
const data = reactive({
|
||||
export const useCategoryData = function () {
|
||||
return useData<RecipeCategory>({
|
||||
id: "",
|
||||
name: "",
|
||||
slug: undefined,
|
||||
slug: "",
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.slug = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePublicCategoryStore(groupSlug: string) {
|
||||
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
|
||||
export const useCategoryStore = function () {
|
||||
const api = useUserApi();
|
||||
const loading = storeLoading;
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
|
||||
flushStore() {
|
||||
categoryStore.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!loading.value && (!categoryStore.value || categoryStore.value?.length === 0)) {
|
||||
actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
items: categoryStore,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
return useStore<RecipeCategory>(store, loading, api.categories);
|
||||
}
|
||||
|
||||
export const usePublicCategoryStore = function (groupSlug: string) {
|
||||
const api = usePublicExploreApi(groupSlug).explore;
|
||||
return useReadOnlyStore<RecipeCategory>(store, publicLoading, api.categories);
|
||||
}
|
||||
|
@ -1,73 +1,28 @@
|
||||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
||||
import { usePublicExploreApi } from "../api/api-client";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||
import { IngredientFood } from "~/lib/api/types/recipe";
|
||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
let foodStore: Ref<IngredientFood[] | null> = ref([]);
|
||||
const publicStoreLoading = ref(false);
|
||||
const storeLoading = ref(false);
|
||||
const store: Ref<IngredientFood[]> = ref([]);
|
||||
const loading = 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 () {
|
||||
const data: IngredientFood = reactive({
|
||||
return useData<IngredientFood>({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
labelId: undefined,
|
||||
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 () {
|
||||
const api = useUserApi();
|
||||
const loading = storeLoading;
|
||||
return useStore<IngredientFood>(store, loading, api.foods);
|
||||
}
|
||||
|
||||
const actions = {
|
||||
...useStoreActions(api.foods, foodStore, loading),
|
||||
flushStore() {
|
||||
foodStore.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!loading.value && (!foodStore.value || foodStore.value.length === 0)) {
|
||||
foodStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { foods: foodStore, actions };
|
||||
};
|
||||
export const usePublicFoodStore = function (groupSlug: string) {
|
||||
const api = usePublicExploreApi(groupSlug).explore;
|
||||
return useReadOnlyStore<IngredientFood>(store, publicLoading, api.foods);
|
||||
}
|
||||
|
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 { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useData, useStore } from "../partials/use-store-factory";
|
||||
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
let labelStore: Ref<MultiPurposeLabelOut[] | null> = ref([]);
|
||||
const storeLoading = ref(false);
|
||||
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
export function useLabelData() {
|
||||
const data = reactive({
|
||||
export const useLabelData = function () {
|
||||
return useData<MultiPurposeLabelOut>({
|
||||
groupId: "",
|
||||
id: "",
|
||||
name: "",
|
||||
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 loading = storeLoading;
|
||||
|
||||
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,
|
||||
};
|
||||
return useStore<MultiPurposeLabelOut>(store, loading, api.multiPurposeLabels);
|
||||
}
|
||||
|
@ -1,72 +1,26 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
||||
import { usePublicExploreApi } from "../api/api-client";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||
import { RecipeTag } from "~/lib/api/types/recipe";
|
||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const items: Ref<RecipeTag[]> = ref([]);
|
||||
const publicStoreLoading = ref(false);
|
||||
const storeLoading = ref(false);
|
||||
const store: Ref<RecipeTag[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
|
||||
export function useTagData() {
|
||||
const data = reactive({
|
||||
export const useTagData = function () {
|
||||
return useData<RecipeTag>({
|
||||
id: "",
|
||||
name: "",
|
||||
slug: undefined,
|
||||
slug: "",
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.slug = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePublicTagStore(groupSlug: string) {
|
||||
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() {
|
||||
export const useTagStore = function () {
|
||||
const api = useUserApi();
|
||||
const loading = storeLoading;
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<RecipeTag>(api.tags, items, loading),
|
||||
flushStore() {
|
||||
items.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!loading.value && (!items.value || items.value?.length === 0)) {
|
||||
actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
return useStore<RecipeTag>(store, loading, api.tags);
|
||||
}
|
||||
|
||||
export const usePublicTagStore = function (groupSlug: string) {
|
||||
const api = usePublicExploreApi(groupSlug).explore;
|
||||
return useReadOnlyStore<RecipeTag>(store, publicLoading, api.tags);
|
||||
}
|
||||
|
@ -1,74 +1,27 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { usePublicExploreApi } from "../api/api-client";
|
||||
import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||
import { RecipeTool } from "~/lib/api/types/recipe";
|
||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const toolStore: Ref<RecipeTool[]> = ref([]);
|
||||
const publicStoreLoading = ref(false);
|
||||
const storeLoading = ref(false);
|
||||
const store: Ref<RecipeTool[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
|
||||
export function useToolData() {
|
||||
const data = reactive({
|
||||
export const useToolData = function () {
|
||||
return useData<RecipeTool>({
|
||||
id: "",
|
||||
name: "",
|
||||
slug: undefined,
|
||||
slug: "",
|
||||
onHand: false,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.slug = undefined;
|
||||
data.onHand = false;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePublicToolStore(groupSlug: string) {
|
||||
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() {
|
||||
export const useToolStore = function () {
|
||||
const api = useUserApi();
|
||||
const loading = storeLoading;
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<RecipeTool>(api.tools, toolStore, loading),
|
||||
flushStore() {
|
||||
toolStore.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!loading.value && (!toolStore.value || toolStore.value?.length === 0)) {
|
||||
actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
items: toolStore,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
return useStore<RecipeTool>(store, loading, api.tools);
|
||||
}
|
||||
|
||||
export const usePublicToolStore = function (groupSlug: string) {
|
||||
const api = usePublicExploreApi(groupSlug).explore;
|
||||
return useReadOnlyStore<RecipeTool>(store, publicLoading, api.tools);
|
||||
}
|
||||
|
@ -1,53 +1,22 @@
|
||||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useData, useStore } from "../partials/use-store-factory";
|
||||
import { IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
let unitStore: Ref<IngredientUnit[] | null> = ref([]);
|
||||
const storeLoading = ref(false);
|
||||
const store: Ref<IngredientUnit[]> = ref([]);
|
||||
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 () {
|
||||
const data: IngredientUnit = reactive({
|
||||
return useData<IngredientUnit>({
|
||||
id: "",
|
||||
name: "",
|
||||
fraction: true,
|
||||
abbreviation: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.fraction = true;
|
||||
data.abbreviation = "";
|
||||
data.description = "";
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const useUnitStore = function () {
|
||||
const api = useUserApi();
|
||||
const loading = storeLoading;
|
||||
|
||||
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 };
|
||||
};
|
||||
return useStore<IngredientUnit>(store, loading, api.units);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
const householdSelfRef = ref<HouseholdInDB | null>(null);
|
||||
@ -46,8 +46,8 @@ export const useHouseholdSelf = function () {
|
||||
return { actions, household };
|
||||
};
|
||||
|
||||
export const useHouseholds = function () {
|
||||
const api = useUserApi();
|
||||
export const useAdminHouseholds = function () {
|
||||
const api = useAdminApi();
|
||||
const loading = ref(false);
|
||||
|
||||
function getAllHouseholds() {
|
||||
|
@ -7,34 +7,37 @@ const loading = ref(false);
|
||||
const ready = ref(false);
|
||||
|
||||
export const useUserSelfRatings = function () {
|
||||
const { $auth } = useContext();
|
||||
const api = useUserApi();
|
||||
const { $auth } = useContext();
|
||||
const api = useUserApi();
|
||||
|
||||
async function refreshUserRatings() {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.users.getSelfRatings();
|
||||
userRatings.value = data?.ratings || [];
|
||||
loading.value = false;
|
||||
ready.value = true;
|
||||
async function refreshUserRatings() {
|
||||
if (!$auth.user || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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;
|
||||
const userId = $auth.user?.id || "";
|
||||
await api.users.setRating(userId, slug, rating, isFavorite);
|
||||
loading.value = false;
|
||||
await refreshUserRatings();
|
||||
}
|
||||
|
||||
if (!ready.value) {
|
||||
refreshUserRatings();
|
||||
return {
|
||||
userRatings,
|
||||
refreshUserRatings,
|
||||
setRating,
|
||||
ready,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userRatings,
|
||||
refreshUserRatings,
|
||||
setRating,
|
||||
ready,
|
||||
}
|
||||
}
|
||||
|
@ -652,6 +652,7 @@
|
||||
"or": "Or",
|
||||
"has-any": "Has Any",
|
||||
"has-all": "Has All",
|
||||
"clear-selection": "Clear Selection",
|
||||
"results": "Results",
|
||||
"search": "Search",
|
||||
"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 { AdminUsersApi } from "./admin/admin-users";
|
||||
import { AdminHouseholdsApi } from "./admin/admin-households";
|
||||
import { AdminGroupsApi } from "./admin/admin-groups";
|
||||
import { AdminBackupsApi } from "./admin/admin-backups";
|
||||
import { AdminMaintenanceApi } from "./admin/admin-maintenance";
|
||||
@ -9,6 +10,7 @@ import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||
export class AdminAPI {
|
||||
public about: AdminAboutAPI;
|
||||
public users: AdminUsersApi;
|
||||
public households: AdminHouseholdsApi;
|
||||
public groups: AdminGroupsApi;
|
||||
public backups: AdminBackupsApi;
|
||||
public maintenance: AdminMaintenanceApi;
|
||||
@ -17,6 +19,7 @@ export class AdminAPI {
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
this.about = new AdminAboutAPI(requests);
|
||||
this.users = new AdminUsersApi(requests);
|
||||
this.households = new AdminHouseholdsApi(requests);
|
||||
this.groups = new AdminGroupsApi(requests);
|
||||
this.backups = new AdminBackupsApi(requests);
|
||||
this.maintenance = new AdminMaintenanceApi(requests);
|
||||
|
@ -4,6 +4,7 @@ import { PublicRecipeApi } from "./explore/recipes";
|
||||
import { PublicFoodsApi } from "./explore/foods";
|
||||
import { PublicCategoriesApi, PublicTagsApi, PublicToolsApi } from "./explore/organizers";
|
||||
import { PublicCookbooksApi } from "./explore/cookbooks";
|
||||
import { PublicHouseholdApi } from "./explore/households";
|
||||
|
||||
export class ExploreApi extends BaseAPI {
|
||||
public recipes: PublicRecipeApi;
|
||||
@ -12,6 +13,7 @@ export class ExploreApi extends BaseAPI {
|
||||
public categories: PublicCategoriesApi;
|
||||
public tags: PublicTagsApi;
|
||||
public tools: PublicToolsApi;
|
||||
public households: PublicHouseholdApi
|
||||
|
||||
constructor(requests: ApiRequestInstance, groupSlug: string) {
|
||||
super(requests);
|
||||
@ -21,5 +23,6 @@ export class ExploreApi extends BaseAPI {
|
||||
this.categories = new PublicCategoriesApi(requests, groupSlug);
|
||||
this.tags = new PublicTagsApi(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 { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
|
||||
import { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import {
|
||||
GroupAdminUpdate,
|
||||
GroupStorage,
|
||||
@ -15,8 +14,6 @@ const routes = {
|
||||
groupsSelf: `${prefix}/groups/self`,
|
||||
preferences: `${prefix}/groups/preferences`,
|
||||
storage: `${prefix}/groups/storage`,
|
||||
households: `${prefix}/groups/households`,
|
||||
householdsId: (id: string | number) => `${prefix}/groups/households/${id}`,
|
||||
membersHouseholdId: (householdId: string | number | null) => {
|
||||
return 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));
|
||||
}
|
||||
|
||||
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() {
|
||||
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 {
|
||||
HouseholdCreate,
|
||||
HouseholdInDB,
|
||||
UpdateHouseholdAdmin,
|
||||
HouseholdStatistics,
|
||||
ReadHouseholdPreferences,
|
||||
SetPermissions,
|
||||
UpdateHouseholdPreferences,
|
||||
CreateInviteToken,
|
||||
ReadInviteToken,
|
||||
HouseholdSummary,
|
||||
} from "~/lib/api/types/household";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
households: `${prefix}/admin/households`,
|
||||
households: `${prefix}/groups/households`,
|
||||
householdsSelf: `${prefix}/households/self`,
|
||||
members: `${prefix}/households/members`,
|
||||
permissions: `${prefix}/households/permissions`,
|
||||
@ -24,13 +23,13 @@ const routes = {
|
||||
statistics: `${prefix}/households/statistics`,
|
||||
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;
|
||||
itemRoute = routes.householdsId;
|
||||
/** Returns the Group Data for the Current User
|
||||
/** Returns the Household Data for the Current User
|
||||
*/
|
||||
async getCurrentUserHousehold() {
|
||||
return await this.requests.get<HouseholdInDB>(routes.householdsSelf);
|
||||
|
@ -56,13 +56,14 @@ const routes = {
|
||||
};
|
||||
|
||||
export type RecipeSearchQuery = {
|
||||
search: string;
|
||||
search?: string;
|
||||
orderDirection?: "asc" | "desc";
|
||||
groupId?: string;
|
||||
|
||||
queryFilter?: string;
|
||||
|
||||
cookbook?: string;
|
||||
households?: string[];
|
||||
|
||||
categories?: string[];
|
||||
requireAllCategories?: boolean;
|
||||
|
@ -45,7 +45,7 @@
|
||||
import { defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { HouseholdInDB } from "~/lib/api/types/household";
|
||||
@ -68,14 +68,14 @@ export default defineComponent({
|
||||
|
||||
const refHouseholdEditForm = ref<VForm | null>(null);
|
||||
|
||||
const userApi = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const household = ref<HouseholdInDB | null>(null);
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data, error } = await userApi.households.getOne(householdId);
|
||||
const { data, error } = await adminApi.households.getOne(householdId);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.tc("user.user-not-found"));
|
||||
@ -92,7 +92,7 @@ export default defineComponent({
|
||||
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) {
|
||||
household.value = data;
|
||||
alert.success(i18n.tc("settings.settings-updated"));
|
||||
|
@ -88,7 +88,7 @@
|
||||
import { defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||
import { fieldTypes } from "~/composables/forms";
|
||||
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 { HouseholdInDB } from "~/lib/api/types/household";
|
||||
|
||||
@ -97,7 +97,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
const { i18n } = useContext();
|
||||
const { groups } = useGroups();
|
||||
const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useHouseholds();
|
||||
const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useAdminHouseholds();
|
||||
|
||||
const state = reactive({
|
||||
createDialog: false,
|
||||
|
@ -80,7 +80,7 @@
|
||||
import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
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 { useUserForm } from "~/composables/use-users";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@ -92,7 +92,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
const { userForm } = useUserForm();
|
||||
const { groups } = useGroups();
|
||||
const { useHouseholdsInGroup } = useHouseholds();
|
||||
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||
const { i18n } = useContext();
|
||||
const route = useRoute();
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
import { computed, defineComponent, useRouter, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
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 { validators } from "~/composables/use-validators";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
@ -60,7 +60,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
const { userForm } = useUserForm();
|
||||
const { groups } = useGroups();
|
||||
const { useHouseholdsInGroup } = useHouseholds();
|
||||
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||
const router = useRouter();
|
||||
|
||||
// ==============================================
|
||||
|
@ -94,7 +94,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 { alert } from "~/composables/use-toast";
|
||||
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
|
||||
@ -108,7 +108,8 @@ export default defineComponent({
|
||||
// ================================================================
|
||||
// Setup
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
const api = useUserApi();
|
||||
const userApi = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const groupSlug = computed(() => $auth.user?.groupSlug);
|
||||
const { locale } = useLocales();
|
||||
@ -264,7 +265,7 @@ export default defineComponent({
|
||||
|
||||
async function updateUser() {
|
||||
// @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,
|
||||
email: accountDetails.email.value,
|
||||
username: accountDetails.username.value,
|
||||
@ -285,7 +286,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function updatePassword() {
|
||||
const { response } = await api.users.changePassword({
|
||||
const { response } = await userApi.users.changePassword({
|
||||
currentPassword: "MyPassword",
|
||||
newPassword: credentials.password1.value,
|
||||
});
|
||||
@ -303,7 +304,7 @@ export default defineComponent({
|
||||
|
||||
async function updateGroup() {
|
||||
// @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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
return;
|
||||
@ -320,7 +321,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// @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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
@ -328,7 +329,7 @@ export default defineComponent({
|
||||
|
||||
async function updateHousehold() {
|
||||
// @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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
return;
|
||||
@ -346,28 +347,28 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// @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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
|
@ -272,12 +272,10 @@ export default defineComponent({
|
||||
const errors = ref<Error[]>([]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<RecipeOrganizerPage
|
||||
v-if="items"
|
||||
:items="items"
|
||||
v-if="store"
|
||||
:items="store"
|
||||
:icon="$globals.icons.categories"
|
||||
item-type="categories"
|
||||
@delete="actions.deleteOne"
|
||||
@ -24,10 +24,10 @@ export default defineComponent({
|
||||
},
|
||||
middleware: ["auth", "group-only"],
|
||||
setup() {
|
||||
const { items, actions } = useCategoryStore();
|
||||
const { store, actions } = useCategoryStore();
|
||||
|
||||
return {
|
||||
items,
|
||||
store,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<RecipeOrganizerPage
|
||||
v-if="items"
|
||||
:items="items"
|
||||
v-if="store"
|
||||
:items="store"
|
||||
:icon="$globals.icons.tags"
|
||||
item-type="tags"
|
||||
@delete="actions.deleteOne"
|
||||
@ -24,10 +24,10 @@ export default defineComponent({
|
||||
},
|
||||
middleware: ["auth", "group-only"],
|
||||
setup() {
|
||||
const { items, actions } = useTagStore();
|
||||
const { store, actions } = useTagStore();
|
||||
|
||||
return {
|
||||
items,
|
||||
store,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
|
@ -29,7 +29,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
dialog,
|
||||
tools: toolStore.items,
|
||||
tools: toolStore.store,
|
||||
actions: toolStore.actions,
|
||||
};
|
||||
},
|
||||
|
@ -81,6 +81,7 @@
|
||||
:headers.sync="tableHeaders"
|
||||
:data="categories || []"
|
||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||
initial-sort="name"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@delete-selected="bulkDeleteEventHandler"
|
||||
@ -198,7 +199,7 @@ export default defineComponent({
|
||||
state,
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
categories: categoryStore.items,
|
||||
categories: categoryStore.store,
|
||||
validators,
|
||||
|
||||
// create
|
||||
|
@ -241,6 +241,8 @@
|
||||
{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'}
|
||||
]"
|
||||
initial-sort="createdAt"
|
||||
initial-sort-desc
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@create-one="createEventHandler"
|
||||
@ -264,6 +266,9 @@
|
||||
{{ item.onHand ? $globals.icons.check : $globals.icons.close }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #item.createdAt="{ item }">
|
||||
{{ formatDate(item.createdAt) }}
|
||||
</template>
|
||||
<template #button-bottom>
|
||||
<BaseButton @click="seedDialog = true">
|
||||
<template #icon> {{ $globals.icons.database }} </template>
|
||||
@ -326,8 +331,21 @@ export default defineComponent({
|
||||
value: "onHand",
|
||||
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();
|
||||
|
||||
// ===============================================================
|
||||
@ -453,7 +471,7 @@ export default defineComponent({
|
||||
// ============================================================
|
||||
// Labels
|
||||
|
||||
const { labels: allLabels } = useLabelStore();
|
||||
const { store: allLabels } = useLabelStore();
|
||||
|
||||
// ============================================================
|
||||
// Seed
|
||||
@ -501,16 +519,15 @@ export default defineComponent({
|
||||
bulkAssignTarget.value = [];
|
||||
bulkAssignLabelId.value = undefined;
|
||||
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 {
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
foods: foodStore.foods,
|
||||
foods: foodStore.store,
|
||||
allLabels,
|
||||
validators,
|
||||
formatDate,
|
||||
// Create
|
||||
createDialog,
|
||||
domNewFoodForm,
|
||||
|
@ -115,6 +115,7 @@
|
||||
:headers.sync="tableHeaders"
|
||||
:data="labels || []"
|
||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||
initial-sort="name"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@delete-selected="bulkDeleteEventHandler"
|
||||
@ -271,7 +272,7 @@ export default defineComponent({
|
||||
state,
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
labels: labelStore.labels,
|
||||
labels: labelStore.store,
|
||||
validators,
|
||||
|
||||
// create
|
||||
|
@ -101,6 +101,7 @@
|
||||
:headers.sync="tableHeaders"
|
||||
:data="actions || []"
|
||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||
initial-sort="title"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@delete-selected="bulkDeleteEventHandler"
|
||||
|
@ -81,6 +81,7 @@
|
||||
:headers.sync="tableHeaders"
|
||||
:data="tags || []"
|
||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||
initial-sort="name"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@delete-selected="bulkDeleteEventHandler"
|
||||
@ -199,7 +200,7 @@ export default defineComponent({
|
||||
state,
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
tags: tagStore.items,
|
||||
tags: tagStore.store,
|
||||
validators,
|
||||
|
||||
// create
|
||||
|
@ -83,6 +83,7 @@
|
||||
:headers.sync="tableHeaders"
|
||||
:data="tools || []"
|
||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||
initial-sort="name"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@delete-selected="bulkDeleteEventHandler"
|
||||
@ -209,7 +210,7 @@ export default defineComponent({
|
||||
state,
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
tools: toolStore.items,
|
||||
tools: toolStore.store,
|
||||
validators,
|
||||
|
||||
// create
|
||||
|
@ -9,11 +9,11 @@
|
||||
</template>
|
||||
</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 #item="{ item }"> {{ item.name }} </template>
|
||||
</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 #item="{ item }"> {{ item.name }} </template>
|
||||
</v-autocomplete>
|
||||
@ -185,7 +185,7 @@
|
||||
</template>
|
||||
</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") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@ -196,8 +196,10 @@
|
||||
<CrudTable
|
||||
:table-config="tableConfig"
|
||||
:headers.sync="tableHeaders"
|
||||
:data="units || []"
|
||||
:data="store"
|
||||
:bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
|
||||
initial-sort="createdAt"
|
||||
initial-sort-desc
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@create-one="createEventHandler"
|
||||
@ -221,6 +223,9 @@
|
||||
{{ item.fraction ? $globals.icons.check : $globals.icons.close }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #item.createdAt="{ item }">
|
||||
{{ formatDate(item.createdAt) }}
|
||||
</template>
|
||||
<template #button-bottom>
|
||||
<BaseButton @click="seedDialog = true">
|
||||
<template #icon> {{ $globals.icons.database }} </template>
|
||||
@ -292,9 +297,22 @@ export default defineComponent({
|
||||
value: "fraction",
|
||||
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
|
||||
@ -447,8 +465,9 @@ export default defineComponent({
|
||||
return {
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
units,
|
||||
store,
|
||||
validators,
|
||||
formatDate,
|
||||
// Create
|
||||
createDialog,
|
||||
domNewUnitForm,
|
||||
|
@ -602,9 +602,9 @@ export default defineComponent({
|
||||
|
||||
const localLabels = ref<ShoppingListMultiPurposeLabelOut[]>()
|
||||
|
||||
const { labels: allLabels } = useLabelStore();
|
||||
const { units: allUnits } = useUnitStore();
|
||||
const { foods: allFoods } = useFoodStore();
|
||||
const { store: allLabels } = useLabelStore();
|
||||
const { store: allUnits } = useUnitStore();
|
||||
const { store: allFoods } = useFoodStore();
|
||||
|
||||
function getLabelColor(item: ShoppingListItemOut | null) {
|
||||
return item?.label?.color;
|
||||
|
@ -5,34 +5,40 @@
|
||||
:icon="$globals.icons.heart"
|
||||
:title="$tc('user.user-favorites')"
|
||||
:recipes="recipes"
|
||||
:query="query"
|
||||
@sortRecipes="assignSorted"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
@delete="removeRecipe"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<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 { useLazyRecipes } from "~/composables/recipes";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
middleware: "auth",
|
||||
setup() {
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const userId = route.value.params.id;
|
||||
const recipes = useAsync(async () => {
|
||||
const { data } = await api.recipes.getAll(1, -1, { queryFilter: `favoritedBy.id = "${userId}"` });
|
||||
return data?.items || null;
|
||||
}, useAsyncKey());
|
||||
const query = { queryFilter: `favoritedBy.id = "${userId}"` }
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||
|
||||
return {
|
||||
query,
|
||||
recipes,
|
||||
isOwnGroup,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
|
@ -10,6 +10,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import InstrumentedAttribute
|
||||
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.ingredient import RecipeIngredientModel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
@ -155,6 +156,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
tags: list[UUID4 | str] | None = None,
|
||||
tools: list[UUID4 | str] | None = None,
|
||||
foods: list[UUID4 | str] | None = None,
|
||||
households: list[UUID4 | str] | None = None,
|
||||
require_all_categories=True,
|
||||
require_all_tags=True,
|
||||
require_all_tools=True,
|
||||
@ -170,6 +172,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
|
||||
if cookbook:
|
||||
cb_filters = self._build_recipe_filter(
|
||||
households=[cookbook.household_id],
|
||||
categories=extract_uuids(cookbook.categories),
|
||||
tags=extract_uuids(cookbook.tags),
|
||||
tools=extract_uuids(cookbook.tools),
|
||||
@ -183,11 +186,13 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
category_ids = self._uuids_for_items(categories, Category)
|
||||
tag_ids = self._uuids_for_items(tags, Tag)
|
||||
tool_ids = self._uuids_for_items(tools, Tool)
|
||||
household_ids = self._uuids_for_items(households, Household)
|
||||
filters = self._build_recipe_filter(
|
||||
categories=category_ids,
|
||||
tags=tag_ids,
|
||||
tools=tool_ids,
|
||||
foods=foods,
|
||||
households=household_ids,
|
||||
require_all_categories=require_all_categories,
|
||||
require_all_tags=require_all_tags,
|
||||
require_all_tools=require_all_tools,
|
||||
@ -245,6 +250,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
tags: list[UUID4] | None = None,
|
||||
tools: list[UUID4] | None = None,
|
||||
foods: list[UUID4] | None = None,
|
||||
households: list[UUID4] | None = None,
|
||||
require_all_categories: bool = True,
|
||||
require_all_tags: 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)
|
||||
else:
|
||||
fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id.in_(foods)))
|
||||
if households:
|
||||
fltr.append(RecipeModel.household_id.in_(households))
|
||||
return fltr
|
||||
|
||||
def by_category_and_tags(
|
||||
|
@ -1,7 +1,7 @@
|
||||
from abc import ABC
|
||||
from logging import Logger
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, HTTPException
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@ -97,6 +97,12 @@ class BasePublicGroupExploreController(BasePublicController):
|
||||
def group_id(self) -> UUID4 | None | NotSet:
|
||||
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:
|
||||
if endpoint.startswith("/"):
|
||||
endpoint = endpoint[1:]
|
||||
|
@ -3,6 +3,7 @@ from fastapi import APIRouter
|
||||
from . import (
|
||||
controller_public_cookbooks,
|
||||
controller_public_foods,
|
||||
controller_public_households,
|
||||
controller_public_organizers,
|
||||
controller_public_recipes,
|
||||
)
|
||||
@ -11,6 +12,7 @@ router = APIRouter(prefix="/explore/groups/{group_slug}")
|
||||
|
||||
# group
|
||||
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.tags_router, tags=["Explore: Tags"])
|
||||
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),
|
||||
tools: list[UUID4 | str] | None = Query(None),
|
||||
foods: list[UUID4 | str] | None = Query(None),
|
||||
households: list[UUID4 | str] | None = Query(None),
|
||||
) -> PaginationBase[RecipeSummary]:
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
recipes_repo = self.cross_household_recipes
|
||||
@ -76,6 +77,7 @@ class PublicRecipesController(BasePublicHouseholdExploreController):
|
||||
tags=tags,
|
||||
tools=tools,
|
||||
foods=foods,
|
||||
households=households,
|
||||
require_all_categories=search_query.require_all_categories,
|
||||
require_all_tags=search_query.require_all_tags,
|
||||
require_all_tools=search_query.require_all_tools,
|
||||
|
@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import (
|
||||
controller_group_households,
|
||||
controller_group_reports,
|
||||
controller_group_self_service,
|
||||
controller_labels,
|
||||
@ -10,6 +11,7 @@ from . import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(controller_group_households.router)
|
||||
router.include_router(controller_group_self_service.router)
|
||||
router.include_router(controller_migrations.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 fastapi import HTTPException, Query
|
||||
from fastapi import Query
|
||||
from pydantic import UUID4
|
||||
|
||||
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.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
||||
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.responses import ErrorResponse
|
||||
from mealie.schema.user.user import GroupSummary, UserSummary
|
||||
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
|
||||
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)
|
||||
def get_group_preferences(self):
|
||||
return self.group.preferences
|
||||
|
@ -320,6 +320,7 @@ class RecipeController(BaseRecipeController):
|
||||
tags: list[UUID4 | str] | None = Query(None),
|
||||
tools: 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
|
||||
if search_query.cookbook:
|
||||
@ -345,6 +346,7 @@ class RecipeController(BaseRecipeController):
|
||||
tags=tags,
|
||||
tools=tools,
|
||||
foods=foods,
|
||||
households=households,
|
||||
require_all_categories=search_query.require_all_categories,
|
||||
require_all_tags=search_query.require_all_tags,
|
||||
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 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
|
||||
|
||||
|
||||
def test_get_households(unfiltered_database: AllRepositories, api_client: TestClient, unique_user: TestUser):
|
||||
households = [
|
||||
unfiltered_database.households.create({"name": random_string(), "group_id": unique_user.group_id})
|
||||
for _ in range(5)
|
||||
]
|
||||
def test_get_households(api_client: TestClient, unique_user: TestUser):
|
||||
households = [unique_user.repos.households.create({"name": random_string()}) for _ in range(5)]
|
||||
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:
|
||||
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_ids = [item["id"] for item in response.json()]
|
||||
response_ids = [item["id"] for item in response.json()["items"]]
|
||||
for household in group_1_households:
|
||||
assert str(household.id) in response_ids
|
||||
for household in group_2_households:
|
||||
assert str(household.id) not in response_ids
|
||||
|
||||
|
||||
def test_get_household(unfiltered_database: AllRepositories, api_client: TestClient, unique_user: TestUser):
|
||||
group_1_id = unique_user.group_id
|
||||
group_2_id = str(unfiltered_database.groups.create({"name": random_string()}).id)
|
||||
def test_get_one_household(api_client: TestClient, unique_user: TestUser):
|
||||
households = [unique_user.repos.households.create({"name": random_string()}) for _ in range(5)]
|
||||
household = random.choice(households)
|
||||
|
||||
group_1_household = unfiltered_database.households.create({"name": random_string(), "group_id": group_1_id})
|
||||
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)
|
||||
response = api_client.get(api_routes.groups_households_household_slug(household.slug), headers=unique_user.token)
|
||||
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
|
||||
|
@ -1,4 +1,5 @@
|
||||
import random
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
@ -35,19 +36,20 @@ class TestCookbook:
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def cookbooks(unique_user: TestUser) -> list[TestCookbook]:
|
||||
def cookbooks(unique_user: TestUser) -> Generator[list[TestCookbook]]:
|
||||
database = unique_user.repos
|
||||
|
||||
data: list[ReadCookBook] = []
|
||||
yield_data: list[TestCookbook] = []
|
||||
for _ in range(3):
|
||||
cb = database.cookbooks.create(SaveCookBook(**get_page_data(unique_user.group_id, unique_user.household_id)))
|
||||
assert cb.slug
|
||||
data.append(cb)
|
||||
yield_data.append(TestCookbook(id=cb.id, slug=cb.slug, name=cb.name, data=cb.model_dump()))
|
||||
|
||||
yield yield_data
|
||||
|
||||
for cb in yield_data:
|
||||
for cb in data:
|
||||
try:
|
||||
database.cookbooks.delete(cb.id)
|
||||
except Exception:
|
||||
|
@ -3,6 +3,9 @@ from datetime import datetime, timezone
|
||||
import pytest
|
||||
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.factories import random_string
|
||||
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
|
||||
|
||||
|
||||
@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])
|
||||
def test_get_one_recipe_from_another_household(
|
||||
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)
|
||||
new_last_made = recipe["lastMade"]
|
||||
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 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_category import CategorySave, TagSave
|
||||
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"}
|
||||
response = api_client.get(api_routes.recipes, params=badparams, headers=unique_user.token)
|
||||
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}"
|
||||
|
||||
|
||||
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):
|
||||
"""`/api/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}"
|
||||
|
||||
|
||||
def groups_households_slug(slug):
|
||||
"""`/api/groups/households/{slug}`"""
|
||||
return f"{prefix}/groups/households/{slug}"
|
||||
def groups_households_household_slug(household_slug):
|
||||
"""`/api/groups/households/{household_slug}`"""
|
||||
return f"{prefix}/groups/households/{household_slug}"
|
||||
|
||||
|
||||
def groups_labels_item_id(item_id):
|
||||
|
Loading…
x
Reference in New Issue
Block a user