From ac53257fa4cc52c0d6bc5b90c53db39b1d9342c4 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:06:16 +0000 Subject: [PATCH 01/26] added alembic revision task --- Taskfile.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 6df84dd94d1..4c2c94e2b6e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -92,6 +92,11 @@ tasks: - rm -f ./dev/data/mealie.log - rm -f ./dev/data/.secret + dev:revision: + desc: creates a new alembic revision; optionally set a message with "task dev:revision -- YOUR REVISION MESSAGE" + cmds: + - poetry run alembic revision --autogenerate -m "{{ .CLI_ARGS }}" + py:mypy: desc: runs python type checking cmds: From 7564c5f7a24297d94ef303283e132e7348deadd1 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:06:35 +0000 Subject: [PATCH 02/26] added recipe actions db model --- ...0_7788478a0338_add_group_recipe_actions.py | 52 +++++++++++++++++++ mealie/db/models/group/__init__.py | 1 + mealie/db/models/group/group.py | 3 ++ mealie/db/models/group/recipe_action.py | 25 +++++++++ 4 files changed, 81 insertions(+) create mode 100644 alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py create mode 100644 mealie/db/models/group/recipe_action.py diff --git a/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py b/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py new file mode 100644 index 00000000000..6e1f2556cd6 --- /dev/null +++ b/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py @@ -0,0 +1,52 @@ +"""add group recipe actions + +Revision ID: 7788478a0338 +Revises: 09aba125b57a +Create Date: 2024-04-07 01:05:20.816270 + +""" + +import sqlalchemy as sa + +import mealie.db.migration_types +from alembic import op + +# revision identifiers, used by Alembic. +revision = "7788478a0338" +down_revision = "09aba125b57a" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "recipe_actions", + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("action_type", sa.String(), nullable=False), + sa.Column("title", sa.String(), nullable=False), + sa.Column("url", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_recipe_actions_action_type"), "recipe_actions", ["action_type"], unique=False) + op.create_index(op.f("ix_recipe_actions_created_at"), "recipe_actions", ["created_at"], unique=False) + op.create_index(op.f("ix_recipe_actions_group_id"), "recipe_actions", ["group_id"], unique=False) + op.create_index(op.f("ix_recipe_actions_title"), "recipe_actions", ["title"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_recipe_actions_title"), table_name="recipe_actions") + op.drop_index(op.f("ix_recipe_actions_group_id"), table_name="recipe_actions") + op.drop_index(op.f("ix_recipe_actions_created_at"), table_name="recipe_actions") + op.drop_index(op.f("ix_recipe_actions_action_type"), table_name="recipe_actions") + op.drop_table("recipe_actions") + # ### end Alembic commands ### diff --git a/mealie/db/models/group/__init__.py b/mealie/db/models/group/__init__.py index 5053f5c5f39..84afb502cd7 100644 --- a/mealie/db/models/group/__init__.py +++ b/mealie/db/models/group/__init__.py @@ -5,6 +5,7 @@ from .invite_tokens import * from .mealplan import * from .preferences import * +from .recipe_action import * from .report import * from .shopping_list import * from .webhooks import * diff --git a/mealie/db/models/group/group.py b/mealie/db/models/group/group.py index 8d2691356f1..d344ce58b05 100644 --- a/mealie/db/models/group/group.py +++ b/mealie/db/models/group/group.py @@ -25,6 +25,7 @@ from ..users import User from .events import GroupEventNotifierModel from .exports import GroupDataExportsModel + from .recipe_action import GroupRecipeAction from .report import ReportModel from .shopping_list import ShoppingList @@ -64,6 +65,7 @@ class Group(SqlAlchemyBase, BaseMixins): GroupMealPlan, order_by="GroupMealPlan.date", **common_args ) webhooks: Mapped[list[GroupWebhooksModel]] = orm.relationship(GroupWebhooksModel, **common_args) + recipe_actions: Mapped[list["GroupRecipeAction"]] = orm.relationship("GroupRecipeAction", **common_args) cookbooks: Mapped[list[CookBook]] = orm.relationship(CookBook, **common_args) server_tasks: Mapped[list[ServerTaskModel]] = orm.relationship(ServerTaskModel, **common_args) data_exports: Mapped[list["GroupDataExportsModel"]] = orm.relationship("GroupDataExportsModel", **common_args) @@ -82,6 +84,7 @@ class Group(SqlAlchemyBase, BaseMixins): exclude={ "users", "webhooks", + "recipe_actions", "shopping_lists", "cookbooks", "preferences", diff --git a/mealie/db/models/group/recipe_action.py b/mealie/db/models/group/recipe_action.py new file mode 100644 index 00000000000..75704461e10 --- /dev/null +++ b/mealie/db/models/group/recipe_action.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .._model_base import BaseMixins, SqlAlchemyBase +from .._model_utils import GUID, auto_init + +if TYPE_CHECKING: + from group import Group + + +class GroupRecipeAction(SqlAlchemyBase, BaseMixins): + __tablename__ = "recipe_actions" + id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate) + group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), index=True) + group: Mapped["Group"] = relationship("Group", back_populates="recipe_actions", single_parent=True) + + action_type: Mapped[str] = mapped_column(String, index=True) + title: Mapped[str] = mapped_column(String, index=True) + url: Mapped[str] = mapped_column(String) + + @auto_init() + def __init__(self, **_) -> None: + pass From 4061e234aff737b2f79e7c2b1d0cae9bbcb28e89 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:09:30 +0000 Subject: [PATCH 03/26] added pydantic models --- frontend/lib/api/types/group.ts | 19 +++++++++++++ mealie/schema/group/__init__.py | 28 +++++++++++++------ mealie/schema/group/group_recipe_action.py | 31 ++++++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 mealie/schema/group/group_recipe_action.py diff --git a/frontend/lib/api/types/group.ts b/frontend/lib/api/types/group.ts index f9bb8c9ece9..df4b3154eb1 100644 --- a/frontend/lib/api/types/group.ts +++ b/frontend/lib/api/types/group.ts @@ -5,6 +5,7 @@ /* Do not modify it by hand - just update the pydantic models and then re-run the script */ +export type RecipeActionType = "link"; export type WebhookType = "mealplan"; export type SupportedMigrations = | "nextcloud" @@ -26,6 +27,11 @@ export interface CreateGroupPreferences { recipeDisableAmount?: boolean; groupId: string; } +export interface CreateGroupRecipeAction { + actionType: RecipeActionType; + title: string; + url: string; +} export interface CreateInviteToken { uses: number; } @@ -191,6 +197,13 @@ export interface GroupEventNotifierUpdate { options?: GroupEventNotifierOptions; id: string; } +export interface GroupRecipeActionOut { + actionType: RecipeActionType; + title: string; + url: string; + groupId: string; + id: string; +} export interface GroupStatistics { totalRecipes: number; totalUsers: number; @@ -230,6 +243,12 @@ export interface ReadWebhook { groupId: string; id: string; } +export interface SaveGroupRecipeAction { + actionType: RecipeActionType; + title: string; + url: string; + groupId: string; +} export interface SaveInviteToken { usesLeft: number; groupId: string; diff --git a/mealie/schema/group/__init__.py b/mealie/schema/group/__init__.py index 13f0ff8bfcc..1acb1d58489 100644 --- a/mealie/schema/group/__init__.py +++ b/mealie/schema/group/__init__.py @@ -15,6 +15,13 @@ from .group_migration import DataMigrationCreate, SupportedMigrations from .group_permissions import SetPermissions from .group_preferences import CreateGroupPreferences, ReadGroupPreferences, UpdateGroupPreferences +from .group_recipe_action import ( + CreateGroupRecipeAction, + GroupRecipeActionOut, + GroupRecipeActionPagination, + RecipeActionType, + SaveGroupRecipeAction, +) from .group_seeder import SeederConfig from .group_shopping_list import ( ShoppingListAddRecipeParams, @@ -45,12 +52,6 @@ from .webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination, WebhookType __all__ = [ - "CreateWebhook", - "ReadWebhook", - "SaveWebhook", - "WebhookPagination", - "WebhookType", - "GroupDataExport", "GroupEventNotifierCreate", "GroupEventNotifierOptions", "GroupEventNotifierOptionsOut", @@ -60,21 +61,31 @@ "GroupEventNotifierSave", "GroupEventNotifierUpdate", "GroupEventPagination", + "CreateGroupRecipeAction", + "GroupRecipeActionOut", + "GroupRecipeActionPagination", + "RecipeActionType", + "SaveGroupRecipeAction", + "CreateWebhook", + "ReadWebhook", + "SaveWebhook", + "WebhookPagination", + "WebhookType", + "GroupDataExport", "CreateGroupPreferences", "ReadGroupPreferences", "UpdateGroupPreferences", "GroupStatistics", "GroupStorage", - "GroupAdminUpdate", "DataMigrationCreate", "SupportedMigrations", "SeederConfig", - "SetPermissions", "CreateInviteToken", "EmailInitationResponse", "EmailInvitation", "ReadInviteToken", "SaveInviteToken", + "SetPermissions", "ShoppingListAddRecipeParams", "ShoppingListCreate", "ShoppingListItemBase", @@ -97,4 +108,5 @@ "ShoppingListSave", "ShoppingListSummary", "ShoppingListUpdate", + "GroupAdminUpdate", ] diff --git a/mealie/schema/group/group_recipe_action.py b/mealie/schema/group/group_recipe_action.py new file mode 100644 index 00000000000..9fe209a946f --- /dev/null +++ b/mealie/schema/group/group_recipe_action.py @@ -0,0 +1,31 @@ +from enum import Enum + +from pydantic import UUID4, ConfigDict + +from mealie.schema._mealie import MealieModel +from mealie.schema.response.pagination import PaginationBase + + +class RecipeActionType(Enum): + link = "link" + + +class CreateGroupRecipeAction(MealieModel): + action_type: RecipeActionType + title: str + url: str + + model_config = ConfigDict(use_enum_values=True) + + +class SaveGroupRecipeAction(CreateGroupRecipeAction): + group_id: UUID4 + + +class GroupRecipeActionOut(SaveGroupRecipeAction): + id: UUID4 + model_config = ConfigDict(from_attributes=True) + + +class GroupRecipeActionPagination(PaginationBase): + items: list[GroupRecipeActionOut] From daca746fca0cf5304bf087ec144d5e734cc8bfb8 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:27:13 +0000 Subject: [PATCH 04/26] added repo/routes --- mealie/repos/repository_factory.py | 6 ++ mealie/routes/groups/__init__.py | 2 + .../groups/controller_group_recipe_actions.py | 55 +++++++++++++++++++ tests/utils/api_routes/__init__.py | 7 +++ 4 files changed, 70 insertions(+) create mode 100644 mealie/routes/groups/controller_group_recipe_actions.py diff --git a/mealie/repos/repository_factory.py b/mealie/repos/repository_factory.py index f0a07d9ca19..7b68b154e9e 100644 --- a/mealie/repos/repository_factory.py +++ b/mealie/repos/repository_factory.py @@ -11,6 +11,7 @@ from mealie.db.models.group.invite_tokens import GroupInviteToken from mealie.db.models.group.mealplan import GroupMealPlanRules from mealie.db.models.group.preferences import GroupPreferencesModel +from mealie.db.models.group.recipe_action import GroupRecipeAction from mealie.db.models.group.shopping_list import ( ShoppingList, ShoppingListItem, @@ -38,6 +39,7 @@ from mealie.schema.group.group_events import GroupEventNotifierOut from mealie.schema.group.group_exports import GroupDataExport from mealie.schema.group.group_preferences import ReadGroupPreferences +from mealie.schema.group.group_recipe_action import GroupRecipeActionOut from mealie.schema.group.group_shopping_list import ( ShoppingListItemOut, ShoppingListItemRecipeRefOut, @@ -186,6 +188,10 @@ def group_report_entries(self) -> RepositoryGeneric[ReportEntryOut, ReportEntryM def cookbooks(self) -> RepositoryGeneric[ReadCookBook, CookBook]: return RepositoryGeneric(self.session, PK_ID, CookBook, ReadCookBook) + @cached_property + def group_recipe_actions(self) -> RepositoryGeneric[GroupRecipeActionOut, GroupRecipeAction]: + return RepositoryGeneric(self.session, PK_ID, GroupRecipeAction, GroupRecipeActionOut) + # ================================================================ # Meal Plan diff --git a/mealie/routes/groups/__init__.py b/mealie/routes/groups/__init__.py index 52459414d7b..2aa62bf8f12 100644 --- a/mealie/routes/groups/__init__.py +++ b/mealie/routes/groups/__init__.py @@ -3,6 +3,7 @@ from . import ( controller_cookbooks, controller_group_notifications, + controller_group_recipe_actions, controller_group_reports, controller_group_self_service, controller_invitations, @@ -31,4 +32,5 @@ router.include_router(controller_shopping_lists.item_router) router.include_router(controller_labels.router) router.include_router(controller_group_notifications.router) +router.include_router(controller_group_recipe_actions.router) router.include_router(controller_seeder.router) diff --git a/mealie/routes/groups/controller_group_recipe_actions.py b/mealie/routes/groups/controller_group_recipe_actions.py new file mode 100644 index 00000000000..e4b1496788c --- /dev/null +++ b/mealie/routes/groups/controller_group_recipe_actions.py @@ -0,0 +1,55 @@ +from functools import cached_property + +from fastapi import APIRouter, Depends +from pydantic import UUID4 + +from mealie.routes._base.base_controllers import BaseUserController +from mealie.routes._base.controller import controller +from mealie.schema.group.group_recipe_action import ( + CreateGroupRecipeAction, + GroupRecipeActionOut, + GroupRecipeActionPagination, + SaveGroupRecipeAction, +) +from mealie.schema.response.pagination import PaginationQuery + +router = APIRouter(prefix="/groups/recipe-actions", tags=["Groups: Recipe Actions"]) +from mealie.routes._base.mixins import HttpRepo + + +@controller(router) +class GroupRecipeActionController(BaseUserController): + @cached_property + def repo(self): + return self.repos.group_recipe_actions.by_group(self.group_id) + + @property + def mixins(self): + return HttpRepo[CreateGroupRecipeAction, GroupRecipeActionOut, SaveGroupRecipeAction](self.repo, self.logger) + + @router.get("", response_model=GroupRecipeActionPagination) + def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): + response = self.repo.page_all( + pagination=q, + override=GroupRecipeActionOut, + ) + + response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump()) + return response + + @router.post("", response_model=GroupRecipeActionOut, status_code=201) + def create_one(self, data: CreateGroupRecipeAction): + save = data.cast(SaveGroupRecipeAction, group_id=self.group.id) + return self.mixins.create_one(save) + + @router.get("/{item_id}", response_model=GroupRecipeActionOut) + def get_one(self, item_id: UUID4): + return self.mixins.get_one(item_id) + + @router.put("/{item_id}", response_model=GroupRecipeActionOut) + def update_one(self, item_id: UUID4, data: SaveGroupRecipeAction): + return self.mixins.update_one(data, item_id) + + @router.delete("/{item_id}", response_model=GroupRecipeActionOut) + def delete_one(self, item_id: UUID4): + return self.mixins.delete_one(item_id) diff --git a/tests/utils/api_routes/__init__.py b/tests/utils/api_routes/__init__.py index 3b1eb13d475..d1d001c878e 100644 --- a/tests/utils/api_routes/__init__.py +++ b/tests/utils/api_routes/__init__.py @@ -87,6 +87,8 @@ """`/api/groups/permissions`""" groups_preferences = "/api/groups/preferences" """`/api/groups/preferences`""" +groups_recipe_actions = "/api/groups/recipe-actions" +"""`/api/groups/recipe-actions`""" groups_reports = "/api/groups/reports" """`/api/groups/reports`""" groups_seeders_foods = "/api/groups/seeders/foods" @@ -320,6 +322,11 @@ def groups_mealplans_rules_item_id(item_id): return f"{prefix}/groups/mealplans/rules/{item_id}" +def groups_recipe_actions_item_id(item_id): + """`/api/groups/recipe-actions/{item_id}`""" + return f"{prefix}/groups/recipe-actions/{item_id}" + + def groups_reports_item_id(item_id): """`/api/groups/reports/{item_id}`""" return f"{prefix}/groups/reports/{item_id}" From a1ca75abb9eecd42f16c9c7790e2b2f23894e82d Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:58:32 +0000 Subject: [PATCH 05/26] added backend tests --- mealie/schema/group/__init__.py | 4 +- mealie/schema/group/group_recipe_action.py | 4 +- .../test_group_recipe_actions.py | 101 ++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 tests/integration_tests/user_group_tests/test_group_recipe_actions.py diff --git a/mealie/schema/group/__init__.py b/mealie/schema/group/__init__.py index 1acb1d58489..ea38796bee5 100644 --- a/mealie/schema/group/__init__.py +++ b/mealie/schema/group/__init__.py @@ -19,7 +19,7 @@ CreateGroupRecipeAction, GroupRecipeActionOut, GroupRecipeActionPagination, - RecipeActionType, + GroupRecipeActionType, SaveGroupRecipeAction, ) from .group_seeder import SeederConfig @@ -64,7 +64,7 @@ "CreateGroupRecipeAction", "GroupRecipeActionOut", "GroupRecipeActionPagination", - "RecipeActionType", + "GroupRecipeActionType", "SaveGroupRecipeAction", "CreateWebhook", "ReadWebhook", diff --git a/mealie/schema/group/group_recipe_action.py b/mealie/schema/group/group_recipe_action.py index 9fe209a946f..417646fc5c3 100644 --- a/mealie/schema/group/group_recipe_action.py +++ b/mealie/schema/group/group_recipe_action.py @@ -6,12 +6,12 @@ from mealie.schema.response.pagination import PaginationBase -class RecipeActionType(Enum): +class GroupRecipeActionType(Enum): link = "link" class CreateGroupRecipeAction(MealieModel): - action_type: RecipeActionType + action_type: GroupRecipeActionType title: str url: str diff --git a/tests/integration_tests/user_group_tests/test_group_recipe_actions.py b/tests/integration_tests/user_group_tests/test_group_recipe_actions.py new file mode 100644 index 00000000000..2df532414e0 --- /dev/null +++ b/tests/integration_tests/user_group_tests/test_group_recipe_actions.py @@ -0,0 +1,101 @@ +import pytest +from fastapi.testclient import TestClient +from pydantic import UUID4 + +from mealie.schema.group.group_recipe_action import CreateGroupRecipeAction, GroupRecipeActionOut, GroupRecipeActionType +from tests.utils import api_routes, assert_derserialize +from tests.utils.factories import random_int, random_string +from tests.utils.fixture_schemas import TestUser + + +def new_link_action() -> CreateGroupRecipeAction: + return CreateGroupRecipeAction( + action_type=GroupRecipeActionType.link, + title=random_string(), + url=random_string(), + ) + + +def test_group_recipe_actions_create_one(api_client: TestClient, unique_user: TestUser): + action_in = new_link_action() + response = api_client.post(api_routes.groups_recipe_actions, json=action_in.model_dump(), headers=unique_user.token) + data = assert_derserialize(response, 201) + + action_out = GroupRecipeActionOut(**data) + assert action_out.id + assert str(action_out.group_id) == unique_user.group_id + assert action_out.action_type == action_in.action_type + assert action_out.title == action_in.title + assert action_out.url == action_in.url + + +def test_group_recipe_actions_get_all(api_client: TestClient, unique_user: TestUser): + expected_ids: set[str] = set() + for _ in range(random_int(3, 5)): + response = api_client.post( + api_routes.groups_recipe_actions, + json=new_link_action().model_dump(), + headers=unique_user.token, + ) + data = assert_derserialize(response, 201) + expected_ids.add(data["id"]) + + response = api_client.get(api_routes.groups_recipe_actions, headers=unique_user.token) + data = assert_derserialize(response, 200) + fetched_ids = set(item["id"] for item in data["items"]) + for expected_id in expected_ids: + assert expected_id in fetched_ids + + +@pytest.mark.parametrize("is_own_group", [True, False]) +def test_group_recipe_actions_get_one( + api_client: TestClient, unique_user: TestUser, g2_user: TestUser, is_own_group: bool +): + action_in = new_link_action() + response = api_client.post(api_routes.groups_recipe_actions, json=action_in.model_dump(), headers=unique_user.token) + data = assert_derserialize(response, 201) + expected_action_out = GroupRecipeActionOut(**data) + + if is_own_group: + fetch_user = unique_user + else: + fetch_user = g2_user + + response = api_client.get( + api_routes.groups_recipe_actions_item_id(expected_action_out.id), headers=fetch_user.token + ) + if not is_own_group: + assert response.status_code == 404 + return + + data = assert_derserialize(response, 200) + action_out = GroupRecipeActionOut(**data) + assert action_out == expected_action_out + + +def test_group_recipe_actions_update_one(api_client: TestClient, unique_user: TestUser): + action_in = new_link_action() + response = api_client.post(api_routes.groups_recipe_actions, json=action_in.model_dump(), headers=unique_user.token) + data = assert_derserialize(response, 201) + action_id = data["id"] + + new_title = random_string() + data["title"] = new_title + response = api_client.put(api_routes.groups_recipe_actions_item_id(action_id), json=data, headers=unique_user.token) + data = assert_derserialize(response, 200) + updated_action = GroupRecipeActionOut(**data) + + assert updated_action.title == new_title + + +def test_group_recipe_actions_delete_one(api_client: TestClient, unique_user: TestUser): + action_in = new_link_action() + response = api_client.post(api_routes.groups_recipe_actions, json=action_in.model_dump(), headers=unique_user.token) + data = assert_derserialize(response, 201) + action_id = data["id"] + + response = api_client.delete(api_routes.groups_recipe_actions_item_id(action_id), headers=unique_user.token) + assert response.status_code == 200 + + response = api_client.get(api_routes.groups_recipe_actions_item_id(action_id), headers=unique_user.token) + assert response.status_code == 404 From 27a7b7a789044cb42020747618e137acd4e462c8 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 7 Apr 2024 02:02:35 +0000 Subject: [PATCH 06/26] added frontend API lib --- frontend/lib/api/client-user.ts | 3 +++ frontend/lib/api/user/group-recipe-actions.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 frontend/lib/api/user/group-recipe-actions.ts diff --git a/frontend/lib/api/client-user.ts b/frontend/lib/api/client-user.ts index 6c7e1dd85f5..cc40f42f35c 100644 --- a/frontend/lib/api/client-user.ts +++ b/frontend/lib/api/client-user.ts @@ -9,6 +9,7 @@ import { UtilsAPI } from "./user/utils"; import { FoodAPI } from "./user/recipe-foods"; import { UnitAPI } from "./user/recipe-units"; import { CookbookAPI } from "./user/group-cookbooks"; +import { GroupRecipeActionsAPI } from "./user/group-recipe-actions"; import { WebhooksAPI } from "./user/group-webhooks"; import { RegisterAPI } from "./user/user-registration"; import { MealPlanAPI } from "./user/group-mealplan"; @@ -36,6 +37,7 @@ export class UserApiClient { public foods: FoodAPI; public units: UnitAPI; public cookbooks: CookbookAPI; + public groupRecipeActions: GroupRecipeActionsAPI; public groupWebhooks: WebhooksAPI; public register: RegisterAPI; public mealplans: MealPlanAPI; @@ -65,6 +67,7 @@ export class UserApiClient { this.users = new UserApi(requests); this.groups = new GroupAPI(requests); this.cookbooks = new CookbookAPI(requests); + this.groupRecipeActions = new GroupRecipeActionsAPI(requests); this.groupWebhooks = new WebhooksAPI(requests); this.register = new RegisterAPI(requests); this.mealplans = new MealPlanAPI(requests); diff --git a/frontend/lib/api/user/group-recipe-actions.ts b/frontend/lib/api/user/group-recipe-actions.ts new file mode 100644 index 00000000000..b1288bf1bad --- /dev/null +++ b/frontend/lib/api/user/group-recipe-actions.ts @@ -0,0 +1,14 @@ +import { BaseCRUDAPI } from "../base/base-clients"; +import { CreateGroupRecipeAction, GroupRecipeActionOut } from "~/lib/api/types/group"; + +const prefix = "/api"; + +const routes = { + groupRecipeActions: `${prefix}/groups/recipe-actions`, + groupRecipeActionsId: (id: string | number) => `${prefix}/groups/recipe-actions/${id}`, + }; + + export class GroupRecipeActionsAPI extends BaseCRUDAPI { + baseRoute = routes.groupRecipeActions; + itemRoute = routes.groupRecipeActionsId; + } From e9bc9a3a032dd58d8bfa215d4fcdf11fbb3744d3 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:54:30 +0000 Subject: [PATCH 07/26] added recipe actions to recipe page --- .../Domain/Recipe/RecipeActionMenu.vue | 1 + .../Domain/Recipe/RecipeContextMenu.vue | 26 ++++++++++ .../composables/use-group-recipe-actions.ts | 50 +++++++++++++++++++ frontend/lang/messages/en-US.json | 3 +- 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 frontend/composables/use-group-recipe-actions.ts diff --git a/frontend/components/Domain/Recipe/RecipeActionMenu.vue b/frontend/components/Domain/Recipe/RecipeActionMenu.vue index 4fd418ffbb3..7f98f54ce50 100644 --- a/frontend/components/Domain/Recipe/RecipeActionMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeActionMenu.vue @@ -70,6 +70,7 @@ print: true, printPreferences: true, share: loggedIn, + recipeActions: true, }" @print="$emit('print')" /> diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index dd96bb1372e..b8f9246fcd1 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -105,6 +105,26 @@ {{ item.title }} +
+ + + + + + + {{ action.title }} + + + + +
@@ -117,6 +137,7 @@ import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue"; import RecipeDialogShare from "./RecipeDialogShare.vue"; import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useUserApi } from "~/composables/api"; +import { useGroupRecipeActions } from "~/composables/use-group-recipe-actions"; import { useGroupSelf } from "~/composables/use-groups"; import { alert } from "~/composables/use-toast"; import { usePlanTypeOptions } from "~/composables/use-group-mealplan"; @@ -134,6 +155,7 @@ export interface ContextMenuIncludes { print: boolean; printPreferences: boolean; share: boolean; + recipeActions: boolean; } export interface ContextMenuItem { @@ -163,6 +185,7 @@ export default defineComponent({ print: true, printPreferences: true, share: true, + recipeActions: true, }), }, // Append items are added at the end of the useItems list @@ -347,6 +370,7 @@ export default defineComponent({ } const router = useRouter(); + const { executeRecipeAction, recipeActions } = useGroupRecipeActions(); async function deleteRecipe() { await api.recipes.deleteOne(props.slug); @@ -437,6 +461,8 @@ export default defineComponent({ ...toRefs(state), recipeRef, recipeRefWithScale, + executeRecipeAction, + recipeActions, shoppingLists, duplicateRecipe, contextMenuEventHandler, diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts new file mode 100644 index 00000000000..f7d210bf244 --- /dev/null +++ b/frontend/composables/use-group-recipe-actions.ts @@ -0,0 +1,50 @@ +import { computed, ref } from "@nuxtjs/composition-api"; +import { useUserApi } from "~/composables/api"; +import { GroupRecipeActionOut } from "~/lib/api/types/group"; + +const groupRecipeActions = ref(null); +const loading = ref(false); + +export const useGroupRecipeActions = function () { + const api = useUserApi(); + async function refreshGroupRecipeActions() { + loading.value = true; + const { data } = await api.groupRecipeActions.getAll(1, -1); + groupRecipeActions.value = data?.items || null; + loading.value = false; + } + + const recipeActions = computed(() => { + if (groupRecipeActions.value === null) { + return null; + } + + return groupRecipeActions.value.map(action => { + action.url = parseRecipeActionUrl(action.url); + return action; + }); + }); + + function parseRecipeActionUrl(url: string): string { + return url.replace("${url}", window.location.href) + }; + + async function executeRecipeAction(action: GroupRecipeActionOut) { + switch (action.actionType) { + case "link": + window.open(action.url, "_blank")?.focus(); + return; + default: + return; + } + }; + + if (!groupRecipeActions.value && !loading.value) { + refreshGroupRecipeActions(); + }; + + return { + executeRecipeAction, + recipeActions, + }; +}; diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 8dabe5b82ec..da979b5451b 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -582,7 +582,8 @@ "upload-image": "Upload image", "screen-awake": "Keep Screen Awake", "remove-image": "Remove image", - "nextStep": "Next step" + "nextStep": "Next step", + "recipe-actions": "Recipe Actions" }, "search": { "advanced-search": "Advanced Search", From 5c99a5481557ef784a3234cad194a9e2da1a1b6b Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 00:48:31 +0000 Subject: [PATCH 08/26] add data management page for recipe actions --- .../composables/use-group-recipe-actions.ts | 43 ++- frontend/lang/messages/en-US.json | 7 + frontend/pages/group/data.vue | 15 +- frontend/pages/group/data/recipe-actions.vue | 250 ++++++++++++++++++ 4 files changed, 310 insertions(+), 5 deletions(-) create mode 100644 frontend/pages/group/data/recipe-actions.vue diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts index f7d210bf244..be410b8bf66 100644 --- a/frontend/composables/use-group-recipe-actions.ts +++ b/frontend/composables/use-group-recipe-actions.ts @@ -1,19 +1,45 @@ -import { computed, ref } from "@nuxtjs/composition-api"; +import { computed, reactive, ref } from "@nuxtjs/composition-api"; +import { useStoreActions } from "./partials/use-actions-factory"; import { useUserApi } from "~/composables/api"; -import { GroupRecipeActionOut } from "~/lib/api/types/group"; +import { GroupRecipeActionOut, RecipeActionType } from "~/lib/api/types/group"; const groupRecipeActions = ref(null); const loading = ref(false); -export const useGroupRecipeActions = function () { +export function useGroupRecipeActionData() { + const data = reactive({ + id: "", + actionType: "link" as RecipeActionType, + title: "", + url: "", + }); + + function reset() { + data.id = ""; + data.actionType = "link"; + data.title = ""; + data.url = ""; + } + + return { + data, + reset, + }; +} + +export const useGroupRecipeActions = function (orderBy: string = "title", orderDirection: string = "asc") { const api = useUserApi(); async function refreshGroupRecipeActions() { loading.value = true; - const { data } = await api.groupRecipeActions.getAll(1, -1); + const { data } = await api.groupRecipeActions.getAll(1, -1, { orderBy, orderDirection }); groupRecipeActions.value = data?.items || null; loading.value = false; } + const recipeActionsData = computed(() => { + return groupRecipeActions.value; + }); + const recipeActions = computed(() => { if (groupRecipeActions.value === null) { return null; @@ -43,8 +69,17 @@ export const useGroupRecipeActions = function () { refreshGroupRecipeActions(); }; + const actions = { + ...useStoreActions(api.groupRecipeActions, groupRecipeActions, loading), + flushStore() { + groupRecipeActions.value = []; + } + } + return { + actions, executeRecipeAction, recipeActions, + recipeActionsData, }; }; diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index da979b5451b..4b8cde1d83b 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -160,6 +160,7 @@ "test": "Test", "themes": "Themes", "thursday": "Thursday", + "title": "Title", "token": "Token", "tuesday": "Tuesday", "type": "Type", @@ -1002,6 +1003,12 @@ "delete-recipes": "Delete Recipes", "source-unit-will-be-deleted": "Source Unit will be deleted" }, + "recipe-actions": { + "recipe-actions-data": "Recipe Actions Data", + "new-recipe-action": "New Recipe Action", + "edit-recipe-action": "Edit Recipe Action", + "action-type": "Action Type" + }, "create-alias": "Create Alias", "manage-aliases": "Manage Aliases", "seed-data": "Seed Data", diff --git a/frontend/pages/group/data.vue b/frontend/pages/group/data.vue index f48b59a9c30..c59b2d1d421 100644 --- a/frontend/pages/group/data.vue +++ b/frontend/pages/group/data.vue @@ -41,6 +41,7 @@ export default defineComponent({ const { i18n } = useContext(); const buttonLookup: { [key: string]: string } = { recipes: i18n.tc("general.recipes"), + recipeActions: i18n.tc("recipe.recipe-actions"), foods: i18n.tc("general.foods"), units: i18n.tc("general.units"), labels: i18n.tc("data-pages.labels.labels"), @@ -56,6 +57,12 @@ export default defineComponent({ text: i18n.t("general.recipes"), value: "new", to: "/group/data/recipes", + divider: false, + }, + { + text: i18n.t("recipe.recipe-actions"), + value: "new", + to: "/group/data/recipe-actions", divider: true, }, { @@ -92,7 +99,13 @@ export default defineComponent({ ]); const buttonText = computed(() => { - const last = route.value.path.split("/").pop(); + const last = route.value.path + .split("/") + .pop() + // convert hypenated-values to camelCase + ?.replace(/-([a-z])/g, function (g) { + return g[1].toUpperCase(); + }) if (last) { return buttonLookup[last]; diff --git a/frontend/pages/group/data/recipe-actions.vue b/frontend/pages/group/data/recipe-actions.vue new file mode 100644 index 00000000000..a94fdff23a5 --- /dev/null +++ b/frontend/pages/group/data/recipe-actions.vue @@ -0,0 +1,250 @@ + + + From a29cb31e29695cf76183172657983113981fe9e7 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:11:40 +0000 Subject: [PATCH 09/26] visual tweaks --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index b8f9246fcd1..1816b282b7d 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -111,11 +111,11 @@ - + From 6423f22588a42da258e6842c117bb4913d145396 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:50:45 +0000 Subject: [PATCH 10/26] lint --- frontend/composables/use-group-recipe-actions.ts | 9 +++++---- mealie/routes/groups/controller_group_recipe_actions.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts index be410b8bf66..0d0b0ee158e 100644 --- a/frontend/composables/use-group-recipe-actions.ts +++ b/frontend/composables/use-group-recipe-actions.ts @@ -27,7 +27,7 @@ export function useGroupRecipeActionData() { }; } -export const useGroupRecipeActions = function (orderBy: string = "title", orderDirection: string = "asc") { +export const useGroupRecipeActions = function (orderBy = "title", orderDirection = "asc") { const api = useUserApi(); async function refreshGroupRecipeActions() { loading.value = true; @@ -52,16 +52,17 @@ export const useGroupRecipeActions = function (orderBy: string = "title", orderD }); function parseRecipeActionUrl(url: string): string { + // eslint-disable-next-line no-template-curly-in-string return url.replace("${url}", window.location.href) }; - async function executeRecipeAction(action: GroupRecipeActionOut) { + function executeRecipeAction(action: GroupRecipeActionOut) { switch (action.actionType) { case "link": window.open(action.url, "_blank")?.focus(); - return; + break; default: - return; + break; } }; diff --git a/mealie/routes/groups/controller_group_recipe_actions.py b/mealie/routes/groups/controller_group_recipe_actions.py index e4b1496788c..a43bd9ae817 100644 --- a/mealie/routes/groups/controller_group_recipe_actions.py +++ b/mealie/routes/groups/controller_group_recipe_actions.py @@ -5,6 +5,7 @@ from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.controller import controller +from mealie.routes._base.mixins import HttpRepo from mealie.schema.group.group_recipe_action import ( CreateGroupRecipeAction, GroupRecipeActionOut, @@ -14,7 +15,6 @@ from mealie.schema.response.pagination import PaginationQuery router = APIRouter(prefix="/groups/recipe-actions", tags=["Groups: Recipe Actions"]) -from mealie.routes._base.mixins import HttpRepo @controller(router) From 104505c68fb62340cc325f6cb06f2807fadab1cc Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:33:11 +0000 Subject: [PATCH 11/26] added post type simplified store interface added action type to data management page --- .../Domain/Recipe/RecipeContextMenu.vue | 18 +++++-- .../composables/use-group-recipe-actions.ts | 47 ++++++++++--------- frontend/lang/messages/en-US.json | 1 + frontend/lib/api/types/group.ts | 4 +- frontend/pages/group/data/recipe-actions.vue | 33 +++++++++---- mealie/schema/group/group_recipe_action.py | 1 + 6 files changed, 70 insertions(+), 34 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index 1816b282b7d..391c9b3fa4e 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -142,7 +142,7 @@ import { useGroupSelf } from "~/composables/use-groups"; import { alert } from "~/composables/use-toast"; import { usePlanTypeOptions } from "~/composables/use-group-mealplan"; import { Recipe } from "~/lib/api/types/recipe"; -import { ShoppingListSummary } from "~/lib/api/types/group"; +import { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/group"; import { PlanEntryType } from "~/lib/api/types/meal-plan"; import { useAxiosDownloader } from "~/composables/api/use-axios-download"; @@ -370,7 +370,19 @@ export default defineComponent({ } const router = useRouter(); - const { executeRecipeAction, recipeActions } = useGroupRecipeActions(); + const groupRecipeActionsStore = useGroupRecipeActions(); + + async function executeRecipeAction(action: GroupRecipeActionOut) { + await groupRecipeActionsStore.execute(action, props.recipe); + + switch (action.actionType) { + case "post": + alert.success(i18n.tc("events.message-sent") as string); + break; + default: + break; + } + } async function deleteRecipe() { await api.recipes.deleteOne(props.slug); @@ -462,7 +474,7 @@ export default defineComponent({ recipeRef, recipeRefWithScale, executeRecipeAction, - recipeActions, + recipeActions: groupRecipeActionsStore.recipeActions, shoppingLists, duplicateRecipe, contextMenuEventHandler, diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts index 0d0b0ee158e..94cf8a07f90 100644 --- a/frontend/composables/use-group-recipe-actions.ts +++ b/frontend/composables/use-group-recipe-actions.ts @@ -1,7 +1,8 @@ -import { computed, reactive, ref } from "@nuxtjs/composition-api"; +import { computed, reactive, ref, useContext } from "@nuxtjs/composition-api"; import { useStoreActions } from "./partials/use-actions-factory"; import { useUserApi } from "~/composables/api"; import { GroupRecipeActionOut, RecipeActionType } from "~/lib/api/types/group"; +import { Recipe } from "~/lib/api/types/recipe"; const groupRecipeActions = ref(null); const loading = ref(false); @@ -27,8 +28,13 @@ export function useGroupRecipeActionData() { }; } -export const useGroupRecipeActions = function (orderBy = "title", orderDirection = "asc") { +export const useGroupRecipeActions = function ( + orderBy: string | null = "title", + orderDirection: string | null = "asc", +) { + const { $axios } = useContext(); const api = useUserApi(); + async function refreshGroupRecipeActions() { loading.value = true; const { data } = await api.groupRecipeActions.getAll(1, -1, { orderBy, orderDirection }); @@ -36,30 +42,30 @@ export const useGroupRecipeActions = function (orderBy = "title", orderDirection loading.value = false; } - const recipeActionsData = computed(() => { - return groupRecipeActions.value; - }); - const recipeActions = computed(() => { - if (groupRecipeActions.value === null) { - return null; - } - - return groupRecipeActions.value.map(action => { - action.url = parseRecipeActionUrl(action.url); - return action; - }); + return groupRecipeActions.value; }); - function parseRecipeActionUrl(url: string): string { - // eslint-disable-next-line no-template-curly-in-string - return url.replace("${url}", window.location.href) + function parseRecipeActionUrl(url: string, recipe: Recipe): string { + /* eslint-disable no-template-curly-in-string */ + return url + .replace("${url}", window.location.href) + .replace("${id}", recipe.id || "") + .replace("${slug}", recipe.slug || "") + /* eslint-enable no-template-curly-in-string */ }; - function executeRecipeAction(action: GroupRecipeActionOut) { + async function execute(action: GroupRecipeActionOut, recipe: Recipe) { + const url = parseRecipeActionUrl(action.url, recipe); + switch (action.actionType) { case "link": - window.open(action.url, "_blank")?.focus(); + window.open(url, "_blank")?.focus(); + break; + case "post": + await $axios.post(url).catch((error) => { + console.error(error); + }); break; default: break; @@ -79,8 +85,7 @@ export const useGroupRecipeActions = function (orderBy = "title", orderDirection return { actions, - executeRecipeAction, + execute, recipeActions, - recipeActionsData, }; }; diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 4b8cde1d83b..47c2e41e8b8 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -64,6 +64,7 @@ "something-went-wrong": "Something Went Wrong!", "subscribed-events": "Subscribed Events", "test-message-sent": "Test Message Sent", + "message-sent": "Message Sent", "new-notification": "New Notification", "event-notifiers": "Event Notifiers", "apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)", diff --git a/frontend/lib/api/types/group.ts b/frontend/lib/api/types/group.ts index df4b3154eb1..a824019cacd 100644 --- a/frontend/lib/api/types/group.ts +++ b/frontend/lib/api/types/group.ts @@ -5,7 +5,9 @@ /* Do not modify it by hand - just update the pydantic models and then re-run the script */ -export type RecipeActionType = "link"; +export type RecipeActionType = + | "link" + | "post"; export type WebhookType = "mealplan"; export type SupportedMigrations = | "nextcloud" diff --git a/frontend/pages/group/data/recipe-actions.vue b/frontend/pages/group/data/recipe-actions.vue index a94fdff23a5..6389f9e2860 100644 --- a/frontend/pages/group/data/recipe-actions.vue +++ b/frontend/pages/group/data/recipe-actions.vue @@ -20,6 +20,12 @@ :label="$t('general.url')" :rules="[validators.required]" /> + @@ -34,10 +40,17 @@ >
- + +
+
+
- +
@@ -123,11 +136,6 @@ export default defineComponent({ value: "id", show: false, }, - { - text: i18n.t("data-pages.recipe-actions.action-type"), - value: "actionType", - show: false, - }, { text: i18n.t("general.title"), value: "title", @@ -138,6 +146,11 @@ export default defineComponent({ value: "url", show: true, }, + { + text: i18n.t("data-pages.recipe-actions.action-type"), + value: "actionType", + show: true, + }, ]; const state = reactive({ @@ -148,7 +161,8 @@ export default defineComponent({ }); const actionData = useGroupRecipeActionData(); - const actionStore = useGroupRecipeActions(); + const actionStore = useGroupRecipeActions(null, null); + const actionTypeOptions = ["link", "post"] // ============================================================ @@ -223,7 +237,8 @@ export default defineComponent({ state, tableConfig, tableHeaders, - actions: actionStore.recipeActionsData, + actionTypeOptions, + actions: actionStore.recipeActions, validators, // create diff --git a/mealie/schema/group/group_recipe_action.py b/mealie/schema/group/group_recipe_action.py index 417646fc5c3..95e8800ceb0 100644 --- a/mealie/schema/group/group_recipe_action.py +++ b/mealie/schema/group/group_recipe_action.py @@ -8,6 +8,7 @@ class GroupRecipeActionType(Enum): link = "link" + post = "post" class CreateGroupRecipeAction(MealieModel): From d8054663d12925a75f7dd3c1a5b65d67b0340b4c Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:45:25 +0000 Subject: [PATCH 12/26] bypass preflight OPTIONS request --- frontend/composables/use-group-recipe-actions.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts index 94cf8a07f90..3ee105cd137 100644 --- a/frontend/composables/use-group-recipe-actions.ts +++ b/frontend/composables/use-group-recipe-actions.ts @@ -1,4 +1,4 @@ -import { computed, reactive, ref, useContext } from "@nuxtjs/composition-api"; +import { computed, reactive, ref } from "@nuxtjs/composition-api"; import { useStoreActions } from "./partials/use-actions-factory"; import { useUserApi } from "~/composables/api"; import { GroupRecipeActionOut, RecipeActionType } from "~/lib/api/types/group"; @@ -32,7 +32,6 @@ export const useGroupRecipeActions = function ( orderBy: string | null = "title", orderDirection: string | null = "asc", ) { - const { $axios } = useContext(); const api = useUserApi(); async function refreshGroupRecipeActions() { @@ -63,7 +62,12 @@ export const useGroupRecipeActions = function ( window.open(url, "_blank")?.focus(); break; case "post": - await $axios.post(url).catch((error) => { + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + }).catch((error) => { console.error(error); }); break; From 660bcb6f14e3eedb5a2ea1c02d8142a2dfe5b771 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:48:12 +0000 Subject: [PATCH 13/26] CSS tweaks --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index 391c9b3fa4e..5e81f4bb5ae 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -111,11 +111,11 @@ - + From 7d4688f1dcb5370973bacc3f0fafa1e5b43ad7be Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:19:05 +0000 Subject: [PATCH 14/26] added integration docs --- .../documentation/getting-started/features.md | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/docs/documentation/getting-started/features.md b/docs/docs/documentation/getting-started/features.md index 463bf18ac30..8c918d5bda4 100644 --- a/docs/docs/documentation/getting-started/features.md +++ b/docs/docs/documentation/getting-started/features.md @@ -81,12 +81,60 @@ The meal planner has the concept of plan rules. These offer a flexible way to us The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week. -!!! warning - At this time there isn't a tight integration between meal-plans and shopping lists; however, it's something we have planned for the future. - [Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary } +## Integrations + +Mealie is designed to integrate with many different external services. There are several ways you can integrate with Mealie to achieve custom IoT automations, data synchronization, and anything else you can think of. [You can work directly with Mealie through the API](./api-usage.md), or leverage other services to make seamless integrations. + +### Notifiers + +Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include: +- creating a recipe +- adding items to a shopping list +- creating a new mealplan + +Notifiers use the [Apprise library](https://github.com/caronc/apprise/wiki), which integrates with a large number of notification services. In addition, certain custom notifiers send basic event data to the consumer (e.g. the `id` of the resource). These include: +- `form` and `forms` +- `json` and `jsons` +- `xml` and `xmls` + +[Notifiers Demo](https://demo.mealie.io/group/notifiers){ .md-button .md-button--primary } + +### Webhooks + +Unlike notifiers, which are event-driven notifications, Webhooks allow you to send scheduled notifications to your desired endpoint. Webhooks are sent on the day of a scheduled mealplan, at the specified time, and contain the mealplan data in the request. + +[Webhooks Demo](https://demo.mealie.io/group/webhooks){ .md-button .md-button--primary } + +### Recipe Actions + +Recipe Actions are custom actions you can add to all recipes in Mealie. This is a great way to add custom integrations that are fired manually. There are two types of recipe actions: +1. link - these actions will take you directly to an external page +2. post - these actions will send a `POST` request to the specified URL. These can be used, for instance, to manually trigger a webhook in Home Assistant + +Recipe Action URLs can include merge fields to inject the current recipe's data. For instance, you can use the following URL to include a Google search with the recipe's slug: +``` +https://www.google.com/search?q=${slug} +``` + +When the action is clicked on, the `${slug}` field is replaced with the recipe's slug value. So, for example, it might take you to this URL on one of your recipes: +``` +https://www.google.com/search?q=pasta-fagioli +``` + +A common use case for "link" recipe actions is to integrate with the Bring! shopping list. Simply add a Recipe Action with the following URL: +``` +https://api.getbring.com/rest/bringrecipes/deeplink?url=${url}&source=web +``` + +Below is a list of all valid merge fields: +- ${id} +- ${slug} +- ${url} + +To add, modify, or delete Recipe Actions, visit the Data Management page (more on that below). ## Data Management From 0093dc6637232b01fc90978d4dfc2faf1e57ad57 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:19:54 +0000 Subject: [PATCH 15/26] lint --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index 5e81f4bb5ae..ccab082b0c8 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -377,7 +377,7 @@ export default defineComponent({ switch (action.actionType) { case "post": - alert.success(i18n.tc("events.message-sent") as string); + alert.success(i18n.tc("events.message-sent")); break; default: break; From 5d47c4d9b74bf0d8c699df04cb8b6cf0fc5a36c9 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:17:09 +0000 Subject: [PATCH 16/26] add recipe JSON to POST request --- frontend/composables/use-group-recipe-actions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts index 3ee105cd137..993166f47e6 100644 --- a/frontend/composables/use-group-recipe-actions.ts +++ b/frontend/composables/use-group-recipe-actions.ts @@ -65,8 +65,12 @@ export const useGroupRecipeActions = function ( await fetch(url, { method: "POST", headers: { + // The "text/plain" content type header is used here to skip the CORS preflight request, + // since it may fail. This is fine, since we don't care about the response, we just want + // the request to get sent. "Content-Type": "text/plain", }, + body: JSON.stringify(recipe), }).catch((error) => { console.error(error); }); From 4425581195c24a1a7cebbf52e879c012b7492c78 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:20:10 +0000 Subject: [PATCH 17/26] updated docs to mention the request body --- docs/docs/documentation/getting-started/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/documentation/getting-started/features.md b/docs/docs/documentation/getting-started/features.md index 8c918d5bda4..75c4b590a08 100644 --- a/docs/docs/documentation/getting-started/features.md +++ b/docs/docs/documentation/getting-started/features.md @@ -112,7 +112,7 @@ Unlike notifiers, which are event-driven notifications, Webhooks allow you to se Recipe Actions are custom actions you can add to all recipes in Mealie. This is a great way to add custom integrations that are fired manually. There are two types of recipe actions: 1. link - these actions will take you directly to an external page -2. post - these actions will send a `POST` request to the specified URL. These can be used, for instance, to manually trigger a webhook in Home Assistant +2. post - these actions will send a `POST` request to the specified URL, with the recipe JSON in the request body. These can be used, for instance, to manually trigger a webhook in Home Assistant Recipe Action URLs can include merge fields to inject the current recipe's data. For instance, you can use the following URL to include a Google search with the recipe's slug: ``` From 9665bd0ede08d1f9b7ba9fe80adac4763f485f9e Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Fri, 12 Apr 2024 01:39:19 +0000 Subject: [PATCH 18/26] removed alembic task in favor of better one from upstream --- Taskfile.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index c9604556cb0..49224d1862f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -93,11 +93,6 @@ tasks: - rm -f ./dev/data/mealie.log - rm -f ./dev/data/.secret - dev:revision: - desc: creates a new alembic revision; optionally set a message with "task dev:revision -- YOUR REVISION MESSAGE" - cmds: - - poetry run alembic revision --autogenerate -m "{{ .CLI_ARGS }}" - py:mypy: desc: runs python type checking cmds: From 6446d1a415a7c821f48d98d5d606c8ec1c3bc96a Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:23:33 +0000 Subject: [PATCH 19/26] update revision tree --- ...24-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py b/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py index 6e1f2556cd6..1a7162c39a9 100644 --- a/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py +++ b/alembic/versions/2024-04-07-01.05.20_7788478a0338_add_group_recipe_actions.py @@ -1,7 +1,7 @@ """add group recipe actions Revision ID: 7788478a0338 -Revises: 09aba125b57a +Revises: d7c6efd2de42 Create Date: 2024-04-07 01:05:20.816270 """ @@ -13,7 +13,7 @@ # revision identifiers, used by Alembic. revision = "7788478a0338" -down_revision = "09aba125b57a" +down_revision = "d7c6efd2de42" branch_labels = None depends_on = None From aa25c9b69c9293d7aac19887b0205a301610696e Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:49:05 -0500 Subject: [PATCH 20/26] Update frontend/pages/group/data.vue Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> --- frontend/pages/group/data.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/pages/group/data.vue b/frontend/pages/group/data.vue index c59b2d1d421..2dfce353179 100644 --- a/frontend/pages/group/data.vue +++ b/frontend/pages/group/data.vue @@ -57,7 +57,6 @@ export default defineComponent({ text: i18n.t("general.recipes"), value: "new", to: "/group/data/recipes", - divider: false, }, { text: i18n.t("recipe.recipe-actions"), From 372225511e919d3d698e79ae7c5e7a278f749c21 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:49:42 -0500 Subject: [PATCH 21/26] Update tests/integration_tests/user_group_tests/test_group_recipe_actions.py Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> --- .../user_group_tests/test_group_recipe_actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration_tests/user_group_tests/test_group_recipe_actions.py b/tests/integration_tests/user_group_tests/test_group_recipe_actions.py index 2df532414e0..036274aef69 100644 --- a/tests/integration_tests/user_group_tests/test_group_recipe_actions.py +++ b/tests/integration_tests/user_group_tests/test_group_recipe_actions.py @@ -1,6 +1,5 @@ import pytest from fastapi.testclient import TestClient -from pydantic import UUID4 from mealie.schema.group.group_recipe_action import CreateGroupRecipeAction, GroupRecipeActionOut, GroupRecipeActionType from tests.utils import api_routes, assert_derserialize From a2615316ae5eded0c727194c8e97976716a0fc2e Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:50:58 -0500 Subject: [PATCH 22/26] Update frontend/components/Domain/Recipe/RecipeContextMenu.vue Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index ccab082b0c8..3d20aad1902 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -375,13 +375,10 @@ export default defineComponent({ async function executeRecipeAction(action: GroupRecipeActionOut) { await groupRecipeActionsStore.execute(action, props.recipe); - switch (action.actionType) { - case "post": - alert.success(i18n.tc("events.message-sent")); - break; - default: - break; + if (action.actionType === "post") { + alert.success(i18n.tc("events.message-sent")); } + break; } async function deleteRecipe() { From b3c554cca3347cf0b3c3f435d7af92133941215b Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:52:08 +0000 Subject: [PATCH 23/26] removed unused break --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index 3d20aad1902..5d7c7032f06 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -378,7 +378,6 @@ export default defineComponent({ if (action.actionType === "post") { alert.success(i18n.tc("events.message-sent")); } - break; } async function deleteRecipe() { From 4a5634676ea4f67c9f10bda40cd093c523e53296 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:21:42 +0000 Subject: [PATCH 24/26] add error message to post if response isn't 2xx --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 8 ++++++-- frontend/composables/use-group-recipe-actions.ts | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index 5d7c7032f06..9b142ceda46 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -373,10 +373,14 @@ export default defineComponent({ const groupRecipeActionsStore = useGroupRecipeActions(); async function executeRecipeAction(action: GroupRecipeActionOut) { - await groupRecipeActionsStore.execute(action, props.recipe); + const response = await groupRecipeActionsStore.execute(action, props.recipe); if (action.actionType === "post") { - alert.success(i18n.tc("events.message-sent")); + if (!response || (200 <= response.status && response.status < 300)) { + alert.success(i18n.tc("events.message-sent")); + } else { + alert.error(i18n.tc("events.something-went-wrong")); + } } } diff --git a/frontend/composables/use-group-recipe-actions.ts b/frontend/composables/use-group-recipe-actions.ts index 993166f47e6..6605baaf227 100644 --- a/frontend/composables/use-group-recipe-actions.ts +++ b/frontend/composables/use-group-recipe-actions.ts @@ -54,7 +54,7 @@ export const useGroupRecipeActions = function ( /* eslint-enable no-template-curly-in-string */ }; - async function execute(action: GroupRecipeActionOut, recipe: Recipe) { + async function execute(action: GroupRecipeActionOut, recipe: Recipe): Promise { const url = parseRecipeActionUrl(action.url, recipe); switch (action.actionType) { @@ -62,7 +62,7 @@ export const useGroupRecipeActions = function ( window.open(url, "_blank")?.focus(); break; case "post": - await fetch(url, { + return await fetch(url, { method: "POST", headers: { // The "text/plain" content type header is used here to skip the CORS preflight request, @@ -74,7 +74,6 @@ export const useGroupRecipeActions = function ( }).catch((error) => { console.error(error); }); - break; default: break; } From 3fa412c9194f95b8414776a23439b7d094a840dd Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:24:12 +0000 Subject: [PATCH 25/26] docs --- docs/docs/overrides/api.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html index 1a9cf805c03..ec16cb46c57 100644 --- a/docs/docs/overrides/api.html +++ b/docs/docs/overrides/api.html @@ -14,7 +14,7 @@
From d509bc298493573c621978055a3f010549195b93 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:27:50 +0000 Subject: [PATCH 26/26] lint --- frontend/components/Domain/Recipe/RecipeContextMenu.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index 9b142ceda46..3e1e27edcf1 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -376,7 +376,7 @@ export default defineComponent({ const response = await groupRecipeActionsStore.execute(action, props.recipe); if (action.actionType === "post") { - if (!response || (200 <= response.status && response.status < 300)) { + if (!response || (response.status >= 200 && response.status < 300)) { alert.success(i18n.tc("events.message-sent")); } else { alert.error(i18n.tc("events.something-went-wrong"));