mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 21:45:25 +02:00
feat: Cross-Household Recipes (#4089)
This commit is contained in:
parent
7ef2e91ecf
commit
9acf9ec27c
16 changed files with 545 additions and 92 deletions
|
@ -21,31 +21,23 @@
|
|||
|
||||
<v-spacer></v-spacer>
|
||||
<div v-if="!open" class="custom-btn-group ma-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="ml-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style class="ml-1" :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="!locked" bottom color="info">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-btn fab small class="ml-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-else bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> {{ $t("recipe.locked-by-owner") }} </span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<RecipeTimerMenu
|
||||
fab
|
||||
color="info"
|
||||
class="mr-1"
|
||||
class="ml-1"
|
||||
/>
|
||||
|
||||
<RecipeContextMenu
|
||||
|
@ -72,6 +64,7 @@
|
|||
share: loggedIn,
|
||||
recipeActions: true,
|
||||
}"
|
||||
class="ml-1"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -135,7 +128,7 @@ export default defineComponent({
|
|||
required: true,
|
||||
type: String,
|
||||
},
|
||||
locked: {
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
:recipe="recipe"
|
||||
:slug="recipe.slug"
|
||||
:recipe-scale="recipeScale"
|
||||
:locked="isOwnGroup && user.id !== recipe.userId && recipe.settings.locked"
|
||||
:can-edit="canEditRecipe"
|
||||
:name="recipe.name"
|
||||
:logged-in="isOwnGroup"
|
||||
:open="isEditMode"
|
||||
|
@ -64,6 +64,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, useContext, computed, ref, watch } from "@nuxtjs/composition-api";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useRecipePermissions } from "~/composables/recipes";
|
||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
||||
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
||||
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
|
||||
|
@ -99,6 +100,7 @@ export default defineComponent({
|
|||
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const { canEditRecipe } = useRecipePermissions(props.recipe, user);
|
||||
|
||||
function printRecipe() {
|
||||
window.print();
|
||||
|
@ -125,6 +127,7 @@ export default defineComponent({
|
|||
setMode,
|
||||
toggleEditMode,
|
||||
recipeImage,
|
||||
canEditRecipe,
|
||||
imageKey,
|
||||
user,
|
||||
PageMode,
|
||||
|
|
|
@ -4,3 +4,4 @@ export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-rec
|
|||
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
|
||||
export { useNutritionLabels } from "./use-recipe-nutrition";
|
||||
export { useTools } from "./use-recipe-tools";
|
||||
export { useRecipePermissions } from "./use-recipe-permissions";
|
||||
|
|
81
frontend/composables/recipes/use-recipe-permissions.test.ts
Normal file
81
frontend/composables/recipes/use-recipe-permissions.test.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { describe, test, expect } from "vitest";
|
||||
import { useRecipePermissions } from "./use-recipe-permissions";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
describe("test use recipe permissions", () => {
|
||||
const commonUserId = "my-user-id";
|
||||
const commonGroupId = "my-group-id";
|
||||
const commonHouseholdId = "my-household-id";
|
||||
|
||||
const createRecipe = (overrides: Partial<Recipe>, isLocked = false): Recipe => ({
|
||||
id: "my-recipe-id",
|
||||
userId: commonUserId,
|
||||
groupId: commonGroupId,
|
||||
householdId: commonHouseholdId,
|
||||
settings: {
|
||||
locked: isLocked,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createUser = (overrides: Partial<UserOut>): UserOut => ({
|
||||
id: commonUserId,
|
||||
groupId: commonGroupId,
|
||||
groupSlug: "my-group",
|
||||
group: "my-group",
|
||||
householdId: commonHouseholdId,
|
||||
householdSlug: "my-household",
|
||||
household: "my-household",
|
||||
email: "bender.rodriguez@example.com",
|
||||
cacheKey: "1234",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("when user is null, cannot edit", () => {
|
||||
const result = useRecipePermissions(createRecipe({}), null);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is recipe owner, can edit", () => {
|
||||
const result = useRecipePermissions(createRecipe({}), createUser({}));
|
||||
expect(result.canEditRecipe.value).toBe(true);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, is correct group and household, and recipe is unlocked, can edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}),
|
||||
createUser({ id: "other-user-id" }),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(true);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, and user is other group, cannot edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}),
|
||||
createUser({ id: "other-user-id", groupId: "other-group-id"}),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, and user is other household, cannot edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}),
|
||||
createUser({ id: "other-user-id", householdId: "other-household-id" }),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, and recipe is locked, cannot edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}, true),
|
||||
createUser({ id: "other-user-id"}),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is recipe owner, and recipe is locked, can edit", () => {
|
||||
const result = useRecipePermissions(createRecipe({}, true), createUser({}));
|
||||
expect(result.canEditRecipe.value).toBe(true);
|
||||
});
|
||||
});
|
34
frontend/composables/recipes/use-recipe-permissions.ts
Normal file
34
frontend/composables/recipes/use-recipe-permissions.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { computed } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export function useRecipePermissions(recipe: Recipe, user: UserOut | null) {
|
||||
const canEditRecipe = computed(() => {
|
||||
// Check recipe owner
|
||||
if (!user?.id) {
|
||||
return false;
|
||||
}
|
||||
if (user.id === recipe.userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check group and household
|
||||
if (user.groupId !== recipe.groupId) {
|
||||
return false;
|
||||
}
|
||||
if (user.householdId !== recipe.householdId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check recipe
|
||||
if (recipe.settings?.locked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
canEditRecipe,
|
||||
}
|
||||
}
|
|
@ -84,8 +84,10 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
|
|||
};
|
||||
|
||||
export const useRecipes = (
|
||||
all = false, fetchRecipes = true,
|
||||
all = false,
|
||||
fetchRecipes = true,
|
||||
loadFood = false,
|
||||
queryFilter: string | null = null,
|
||||
publicGroupSlug: string | null = null
|
||||
) => {
|
||||
const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi();
|
||||
|
@ -108,7 +110,7 @@ export const useRecipes = (
|
|||
})();
|
||||
|
||||
async function refreshRecipes() {
|
||||
const { data } = await api.recipes.getAll(page, perPage, { loadFood, orderBy: "created_at" });
|
||||
const { data } = await api.recipes.getAll(page, perPage, { loadFood, orderBy: "created_at", queryFilter });
|
||||
if (data) {
|
||||
recipes.value = data.items;
|
||||
}
|
||||
|
|
|
@ -27,8 +27,7 @@ export default defineComponent({
|
|||
async function fetchHousehold() {
|
||||
const { data } = await api.households.getCurrentUserHousehold();
|
||||
if (data) {
|
||||
// TODO: once users are able to fetch other households' recipes, remove the household filter
|
||||
queryFilter.value = `recipe.group_id="${data.groupId}" AND recipe.household_id="${data.id}"`;
|
||||
queryFilter.value = `recipe.group_id="${data.groupId}"`;
|
||||
groupName.value = data.group;
|
||||
}
|
||||
|
||||
|
|
|
@ -178,10 +178,8 @@ export default defineComponent({
|
|||
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches },
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
||||
|
||||
const { $globals, i18n } = useContext();
|
||||
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true, false, `householdId=${$auth.user?.householdId || ""}`);
|
||||
const selected = ref<Recipe[]>([]);
|
||||
|
||||
function resetAll() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue