feat: Add Households to Mealie (#3970)

This commit is contained in:
Michael Genson 2024-08-22 10:14:32 -05:00 committed by GitHub
parent 0c29cef17d
commit eb170cc7e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
315 changed files with 6975 additions and 3577 deletions

View File

@ -13,8 +13,7 @@ from sqlalchemy import orm
import mealie.db.migration_types
from alembic import op
from mealie.db.models.group.shopping_list import ShoppingList
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models._model_utils.guid import GUID
# revision identifiers, used by Alembic.
revision = "b04a08da2108"
@ -23,6 +22,25 @@ branch_labels = None
depends_on = None
# Intermediate table definitions
class SqlAlchemyBase(orm.DeclarativeBase):
pass
class ShoppingList(SqlAlchemyBase):
__tablename__ = "shopping_lists"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
group_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
class MultiPurposeLabel(SqlAlchemyBase):
__tablename__ = "multi_purpose_labels"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
group_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
def populate_shopping_lists_multi_purpose_labels(
shopping_lists_multi_purpose_labels_table: sa.Table, session: orm.Session
):

View File

@ -12,11 +12,11 @@ from typing import Any
import sqlalchemy as sa
from pydantic import UUID4
from sqlalchemy import orm
from sqlalchemy.orm import Session, load_only
from alembic import op
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models.group.shopping_list import ShoppingListItem
from mealie.db.models._model_utils.guid import GUID
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel, RecipeIngredientModel
@ -27,6 +27,27 @@ branch_labels = None
depends_on = None
# Intermediate table definitions
class SqlAlchemyBase(orm.DeclarativeBase):
pass
class ShoppingList(SqlAlchemyBase):
__tablename__ = "shopping_lists"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
group_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
class ShoppingListItem(SqlAlchemyBase):
__tablename__ = "shopping_list_items"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
food_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("ingredient_foods.id"))
unit_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("ingredient_units.id"))
label_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("multi_purpose_labels.id"))
@dataclass
class TableMeta:
tablename: str
@ -42,7 +63,7 @@ def _is_postgres():
return op.get_context().dialect.name == "postgresql"
def _get_duplicates(session: Session, model: SqlAlchemyBase) -> defaultdict[str, list]:
def _get_duplicates(session: Session, model: orm.DeclarativeBase) -> defaultdict[str, list]:
duplicate_map: defaultdict[str, list] = defaultdict(list)
query = session.execute(sa.text(f"SELECT id, group_id, name FROM {model.__tablename__}"))

View File

@ -0,0 +1,321 @@
"""add households
Revision ID: feecc8ffb956
Revises: 32d69327997b
Create Date: 2024-07-12 16:16:29.973929
"""
from datetime import datetime, timezone
from textwrap import dedent
from typing import Any
from uuid import uuid4
import sqlalchemy as sa
from slugify import slugify
from sqlalchemy import orm
import mealie.db.migration_types
from alembic import op
from mealie.core.config import get_app_settings
# revision identifiers, used by Alembic.
revision = "feecc8ffb956"
down_revision = "32d69327997b"
branch_labels = None # type: ignore
depends_on = None # type: ignore
settings = get_app_settings()
def is_postgres():
return op.get_context().dialect.name == "postgresql"
def generate_id() -> str:
"""See GUID.convert_value_to_guid"""
val = uuid4()
if is_postgres():
return str(val)
else:
return f"{val.int:032x}"
def dedupe_cookbook_slugs():
bind = op.get_bind()
session = orm.Session(bind=bind)
with session:
sql = sa.text(
dedent(
"""
SELECT slug, group_id, COUNT(*)
FROM cookbooks
GROUP BY slug, group_id
HAVING COUNT(*) > 1
"""
)
)
rows = session.execute(sql).fetchall()
for slug, group_id, _ in rows:
sql = sa.text(
dedent(
"""
SELECT id
FROM cookbooks
WHERE slug = :slug AND group_id = :group_id
ORDER BY id
"""
)
)
cookbook_ids = session.execute(sql, {"slug": slug, "group_id": group_id}).fetchall()
for i, (cookbook_id,) in enumerate(cookbook_ids):
if i == 0:
continue
sql = sa.text(
dedent(
"""
UPDATE cookbooks
SET slug = :slug || '-' || :i
WHERE id = :id
"""
)
)
session.execute(sql, {"slug": slug, "i": i, "id": cookbook_id})
def create_household(session: orm.Session, group_id: str) -> str:
# create/insert household
household_id = generate_id()
timestamp = datetime.now(timezone.utc).isoformat()
household_data = {
"id": household_id,
"name": settings.DEFAULT_HOUSEHOLD,
"slug": slugify(settings.DEFAULT_HOUSEHOLD),
"group_id": group_id,
"created_at": timestamp,
"update_at": timestamp,
}
columns = ", ".join(household_data.keys())
placeholders = ", ".join(f":{key}" for key in household_data.keys())
sql_statement = f"INSERT INTO households ({columns}) VALUES ({placeholders})"
session.execute(sa.text(sql_statement), household_data)
# fetch group preferences so we can copy them over to household preferences
migrated_field_defaults = {
"private_group": True, # this is renamed later
"first_day_of_week": 0,
"recipe_public": True,
"recipe_show_nutrition": False,
"recipe_show_assets": False,
"recipe_landscape_view": False,
"recipe_disable_comments": False,
"recipe_disable_amount": True,
}
sql_statement = (
f"SELECT {', '.join(migrated_field_defaults.keys())} FROM group_preferences WHERE group_id = :group_id"
)
group_preferences = session.execute(sa.text(sql_statement), {"group_id": group_id}).fetchone()
# build preferences data
if group_preferences:
preferences_data: dict[str, Any] = {}
for i, (field, default_value) in enumerate(migrated_field_defaults.items()):
value = group_preferences[i]
preferences_data[field] = value if value is not None else default_value
else:
preferences_data = migrated_field_defaults
preferences_data["id"] = generate_id()
preferences_data["household_id"] = household_id
preferences_data["created_at"] = timestamp
preferences_data["update_at"] = timestamp
preferences_data["private_household"] = preferences_data.pop("private_group")
# insert preferences data
columns = ", ".join(preferences_data.keys())
placeholders = ", ".join(f":{key}" for key in preferences_data.keys())
sql_statement = f"INSERT INTO household_preferences ({columns}) VALUES ({placeholders})"
session.execute(sa.text(sql_statement), preferences_data)
return household_id
def create_households_for_groups() -> dict[str, str]:
bind = op.get_bind()
session = orm.Session(bind=bind)
group_id_household_id_map: dict[str, str] = {}
with session:
rows = session.execute(sa.text("SELECT id FROM groups")).fetchall()
for row in rows:
group_id = row[0]
group_id_household_id_map[group_id] = create_household(session, group_id)
return group_id_household_id_map
def _do_assignment(session: orm.Session, table: str, group_id: str, household_id: str):
sql = sa.text(
dedent(
f"""
UPDATE {table}
SET household_id = :household_id
WHERE group_id = :group_id
""",
)
)
session.execute(sql, {"group_id": group_id, "household_id": household_id})
def assign_households(group_id_household_id_map: dict[str, str]):
tables = [
"cookbooks",
"group_events_notifiers",
"group_meal_plan_rules",
"invite_tokens",
"recipe_actions",
"users",
"webhook_urls",
]
bind = op.get_bind()
session = orm.Session(bind=bind)
with session:
for table in tables:
for group_id, household_id in group_id_household_id_map.items():
_do_assignment(session, table, group_id, household_id)
def populate_household_data():
group_id_household_id_map = create_households_for_groups()
assign_households(group_id_household_id_map)
def upgrade():
dedupe_cookbook_slugs()
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"households",
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("slug", sa.String(), nullable=True),
sa.Column("group_id", mealie.db.migration_types.GUID(), 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"),
sa.UniqueConstraint("group_id", "name", name="household_name_group_id_key"),
sa.UniqueConstraint("group_id", "slug", name="household_slug_group_id_key"),
)
op.create_index(op.f("ix_households_created_at"), "households", ["created_at"], unique=False)
op.create_index(op.f("ix_households_group_id"), "households", ["group_id"], unique=False)
op.create_index(op.f("ix_households_name"), "households", ["name"], unique=False)
op.create_index(op.f("ix_households_slug"), "households", ["slug"], unique=False)
op.create_table(
"household_preferences",
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=False),
sa.Column("private_household", sa.Boolean(), nullable=True),
sa.Column("first_day_of_week", sa.Integer(), nullable=True),
sa.Column("recipe_public", sa.Boolean(), nullable=True),
sa.Column("recipe_show_nutrition", sa.Boolean(), nullable=True),
sa.Column("recipe_show_assets", sa.Boolean(), nullable=True),
sa.Column("recipe_landscape_view", sa.Boolean(), nullable=True),
sa.Column("recipe_disable_comments", sa.Boolean(), nullable=True),
sa.Column("recipe_disable_amount", sa.Boolean(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("update_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["household_id"],
["households.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_household_preferences_created_at"), "household_preferences", ["created_at"], unique=False)
op.create_index(
op.f("ix_household_preferences_household_id"), "household_preferences", ["household_id"], unique=False
)
with op.batch_alter_table("cookbooks") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_cookbooks_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_cookbooks_household_id", "households", ["household_id"], ["id"])
# not directly related to households, but important for frontend routes
batch_op.create_unique_constraint("cookbook_slug_group_id_key", ["slug", "group_id"])
with op.batch_alter_table("group_events_notifiers") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_group_events_notifiers_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_group_events_notifiers_household_id", "households", ["household_id"], ["id"])
with op.batch_alter_table("group_meal_plan_rules") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_group_meal_plan_rules_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_group_meal_plan_rules_household_id", "households", ["household_id"], ["id"])
with op.batch_alter_table("invite_tokens") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_invite_tokens_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_invite_tokens_household_id", "households", ["household_id"], ["id"])
with op.batch_alter_table("recipe_actions") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_recipe_actions_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_recipe_actions_household_id", "households", ["household_id"], ["id"])
with op.batch_alter_table("users") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_users_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_users_household_id", "households", ["household_id"], ["id"])
with op.batch_alter_table("webhook_urls") as batch_op:
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_webhook_urls_household_id"), ["household_id"], unique=False)
batch_op.create_foreign_key("fk_webhook_urls_household_id", "households", ["household_id"], ["id"])
# ### end Alembic commands ###
populate_household_data()
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "webhook_urls", type_="foreignkey")
op.drop_index(op.f("ix_webhook_urls_household_id"), table_name="webhook_urls")
op.drop_column("webhook_urls", "household_id")
op.drop_constraint(None, "users", type_="foreignkey")
op.drop_index(op.f("ix_users_household_id"), table_name="users")
op.drop_column("users", "household_id")
op.drop_constraint(None, "recipe_actions", type_="foreignkey")
op.drop_index(op.f("ix_recipe_actions_household_id"), table_name="recipe_actions")
op.drop_column("recipe_actions", "household_id")
op.drop_constraint(None, "invite_tokens", type_="foreignkey")
op.drop_index(op.f("ix_invite_tokens_household_id"), table_name="invite_tokens")
op.drop_column("invite_tokens", "household_id")
op.drop_constraint(None, "group_meal_plan_rules", type_="foreignkey")
op.drop_index(op.f("ix_group_meal_plan_rules_household_id"), table_name="group_meal_plan_rules")
op.drop_column("group_meal_plan_rules", "household_id")
op.drop_constraint(None, "group_events_notifiers", type_="foreignkey")
op.drop_index(op.f("ix_group_events_notifiers_household_id"), table_name="group_events_notifiers")
op.drop_column("group_events_notifiers", "household_id")
op.drop_constraint(None, "cookbooks", type_="foreignkey")
op.drop_index(op.f("ix_cookbooks_household_id"), table_name="cookbooks")
op.drop_column("cookbooks", "household_id")
op.drop_constraint("cookbook_slug_group_id_key", "cookbooks", type_="unique")
op.drop_index(op.f("ix_household_preferences_household_id"), table_name="household_preferences")
op.drop_index(op.f("ix_household_preferences_created_at"), table_name="household_preferences")
op.drop_table("household_preferences")
op.drop_index(op.f("ix_households_slug"), table_name="households")
op.drop_index(op.f("ix_households_name"), table_name="households")
op.drop_index(op.f("ix_households_group_id"), table_name="households")
op.drop_index(op.f("ix_households_created_at"), table_name="households")
op.drop_table("households")
# ### end Alembic commands ###

View File

@ -67,7 +67,7 @@ def rename_non_compliant_paths():
kabab case.
"""
ignore_files = ["DS_Store", ".gitkeep"]
ignore_files = ["DS_Store", ".gitkeep", "af-ZA.json", "en-US.json"]
ignore_extensions = [".pyc", ".pyo", ".py"]

View File

@ -1,5 +1,6 @@
import logging
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
@ -23,6 +24,11 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict):
dest.write_text(text)
# lint/format file with Ruff
log.info(f"Formatting {dest}")
subprocess.run(["poetry", "run", "ruff", "check", str(dest), "--fix"])
subprocess.run(["poetry", "run", "ruff", "format", str(dest)])
@dataclass
class CodeSlicer:

View File

@ -173,7 +173,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
"dateAdded": "2022-09-03",
"dateUpdated": "2022-09-10T15:18:19.866085",
"createdAt": "2022-09-03T18:31:17.488118",
"updateAt": "2022-09-10T15:18:19.869630",
"updatedAt": "2022-09-10T15:18:19.869630",
"recipeInstructions": [
{
"id": "60ae53a3-b3ff-40ee-bae3-89fea0b1c637",

View File

@ -24,7 +24,7 @@ Make sure the url and port (`http://mealie:9000` ) matches your installation's a
```yaml
- platform: rest
resource: "http://mealie:9000/api/groups/mealplans/today"
resource: "http://mealie:9000/api/households/mealplans/today"
method: GET
name: Mealie todays meal
headers:
@ -36,7 +36,7 @@ Make sure the url and port (`http://mealie:9000` ) matches your installation's a
```yaml
- platform: rest
resource: "http://mealie:9000/api/groups/mealplans/today"
resource: "http://mealie:9000/api/households/mealplans/today"
method: GET
name: Mealie todays meal ID
headers:

View File

@ -73,13 +73,13 @@ Mealie uses a calendar like view to help you plan your meals. It shows you the p
!!! tip
You can also add a "Note" type entry to your meal-plan when you want to include something that might not have a specific recipes. This is great for leftovers, or for ordering out.
[Mealplanner Demo](https://demo.mealie.io/group/mealplan/planner/view){ .md-button .md-button--primary }
[Mealplanner Demo](https://demo.mealie.io/household/mealplan/planner/view){ .md-button .md-button--primary }
### Planner Rules
The meal planner has the concept of plan rules. These offer a flexible way to use your organizers to customize how a random recipe is inserted into your meal plan. You can set rules to restrict the pool of recipes based on the Tags and/or Categories of a recipe. Additionally, since meal plans have a Breakfast, Lunch, Dinner, and Snack labels, you can specifically set a rule to be active for a **specific meal type** or even a **specific day of the week.**
[Planner Settings Demo](https://demo.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
[Planner Settings Demo](https://demo.mealie.io/household/mealplan/settings){ .md-button .md-button--primary }
## Shopping Lists
@ -105,13 +105,13 @@ Notifiers use the [Apprise library](https://github.com/caronc/apprise/wiki), whi
- `json` and `jsons`
- `xml` and `xmls`
[Notifiers Demo](https://demo.mealie.io/group/notifiers){ .md-button .md-button--primary }
[Notifiers Demo](https://demo.mealie.io/household/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 }
[Webhooks Demo](https://demo.mealie.io/household/webhooks){ .md-button .md-button--primary }
### Recipe Actions

File diff suppressed because one or more lines are too long

View File

@ -2,30 +2,11 @@
<div v-if="preferences">
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" :label="$t('group.private-group')"></v-checkbox>
<v-select
v-model="preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays"
item-text="name"
item-value="value"
:label="$t('settings.first-day-of-week')"
/>
<BaseCardSectionTitle class="mt-5" :title="$tc('group.group-recipe-preferences')"></BaseCardSectionTitle>
<template v-for="(_, key) in preferences">
<v-checkbox
v-if="labels[key]"
:key="key"
v-model="preferences[key]"
class="mt-n4"
:label="labels[key]"
></v-checkbox>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
import { defineComponent, computed } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
@ -35,48 +16,6 @@ export default defineComponent({
},
},
setup(props, context) {
const { i18n } = useContext();
const labels = {
recipePublic: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
recipeShowNutrition: i18n.tc("group.show-nutrition-information"),
recipeShowAssets: i18n.tc("group.show-recipe-assets"),
recipeLandscapeView: i18n.tc("group.default-to-landscape-view"),
recipeDisableComments: i18n.tc("group.disable-users-from-commenting-on-recipes"),
recipeDisableAmount: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
};
const allDays = [
{
name: i18n.t("general.sunday"),
value: 0,
},
{
name: i18n.t("general.monday"),
value: 1,
},
{
name: i18n.t("general.tuesday"),
value: 2,
},
{
name: i18n.t("general.wednesday"),
value: 3,
},
{
name: i18n.t("general.thursday"),
value: 4,
},
{
name: i18n.t("general.friday"),
value: 5,
},
{
name: i18n.t("general.saturday"),
value: 6,
},
];
const preferences = computed({
get() {
return props.value;
@ -87,8 +26,6 @@ export default defineComponent({
});
return {
allDays,
labels,
preferences,
};
},

View File

@ -39,7 +39,7 @@
import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
import { Recipe } from "~/lib/api/types/recipe";
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
import { ShoppingListSummary } from "~/lib/api/types/group";
import { ShoppingListSummary } from "~/lib/api/types/household";
import { useUserApi } from "~/composables/api";
export interface ContextMenuItem {

View File

@ -35,7 +35,7 @@
<script lang="ts">
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
import { ReadWebhook } from "~/lib/api/types/group";
import { ReadWebhook } from "~/lib/api/types/household";
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
export default defineComponent({

View File

@ -0,0 +1,99 @@
<template>
<div v-if="preferences">
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
<v-checkbox v-model="preferences.privateHousehold" class="mt-n4" :label="$t('household.private-household')"></v-checkbox>
<v-select
v-model="preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays"
item-text="name"
item-value="value"
:label="$t('settings.first-day-of-week')"
/>
<BaseCardSectionTitle class="mt-5" :title="$tc('household.household-recipe-preferences')"></BaseCardSectionTitle>
<template v-for="(_, key) in preferences">
<v-checkbox
v-if="labels[key]"
:key="key"
v-model="preferences[key]"
class="mt-n4"
:label="labels[key]"
></v-checkbox>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
value: {
type: Object,
required: true,
},
},
setup(props, context) {
const { i18n } = useContext();
const labels = {
recipePublic: i18n.tc("household.allow-users-outside-of-your-household-to-see-your-recipes"),
recipeShowNutrition: i18n.tc("group.show-nutrition-information"),
recipeShowAssets: i18n.tc("group.show-recipe-assets"),
recipeLandscapeView: i18n.tc("group.default-to-landscape-view"),
recipeDisableComments: i18n.tc("group.disable-users-from-commenting-on-recipes"),
recipeDisableAmount: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
};
const allDays = [
{
name: i18n.t("general.sunday"),
value: 0,
},
{
name: i18n.t("general.monday"),
value: 1,
},
{
name: i18n.t("general.tuesday"),
value: 2,
},
{
name: i18n.t("general.wednesday"),
value: 3,
},
{
name: i18n.t("general.thursday"),
value: 4,
},
{
name: i18n.t("general.friday"),
value: 5,
},
{
name: i18n.t("general.saturday"),
value: 6,
},
];
const preferences = computed({
get() {
return props.value;
},
set(val) {
context.emit("input", val);
},
});
return {
allDays,
labels,
preferences,
};
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -337,7 +337,7 @@ export default defineComponent({
);
break;
case EVENTS.updated:
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
setter("updated_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
break;
case EVENTS.lastMade:
setter(

View File

@ -138,11 +138,11 @@ 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 { useHouseholdSelf } from "~/composables/use-households";
import { alert } from "~/composables/use-toast";
import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
import { Recipe } from "~/lib/api/types/recipe";
import { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/group";
import { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/household";
import { PlanEntryType } from "~/lib/api/types/meal-plan";
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
@ -254,14 +254,14 @@ export default defineComponent({
});
const { i18n, $auth, $globals } = useContext();
const { group } = useGroupSelf();
const { household } = useHouseholdSelf();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const firstDayOfWeek = computed(() => {
return group.value?.preferences?.firstDayOfWeek || 0;
return household.value?.preferences?.firstDayOfWeek || 0;
});
// ===========================================================================

View File

@ -18,7 +18,7 @@
</tr>
</template>
<template #item.name="{ item }">
<a :href="`/r/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a>
<a :href="`/g/${groupSlug}/r/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a>
</template>
<template #item.tags="{ item }">
<RecipeChip small :items="item.tags" :is-category="false" url-prefix="tags" />
@ -48,7 +48,7 @@ import UserAvatar from "../User/UserAvatar.vue";
import RecipeChip from "./RecipeChips.vue";
import { Recipe } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api";
import { UserOut } from "~/lib/api/types/user";
import { UserSummary } from "~/lib/api/types/user";
const INPUT_EVENT = "input";
@ -95,7 +95,8 @@ export default defineComponent({
},
},
setup(props, context) {
const { i18n } = useContext();
const { $auth, i18n } = useContext();
const groupSlug = $auth.user?.groupSlug;
function setValue(value: Recipe[]) {
context.emit(INPUT_EVENT, value);
@ -134,7 +135,7 @@ export default defineComponent({
// ============
// Group Members
const api = useUserApi();
const members = ref<UserOut[]>([]);
const members = ref<UserSummary[]>([]);
async function refreshMembers() {
const { data } = await api.groups.fetchMembers();
@ -149,13 +150,19 @@ export default defineComponent({
function getMember(id: string) {
if (members.value[0]) {
return members.value.find((m) => m.id === id)?.username;
return members.value.find((m) => m.id === id)?.fullName;
}
return i18n.t("general.none");
}
return { setValue, headers, members, getMember };
return {
groupSlug,
setValue,
headers,
members,
getMember,
};
},
data() {

View File

@ -127,14 +127,14 @@
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, useContext, watchEffect } from "@nuxtjs/composition-api"
import { toRefs } from "@vueuse/core"
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue"
import { useUserApi } from "~/composables/api"
import { alert } from "~/composables/use-toast"
import { useShoppingListPreferences } from "~/composables/use-users/preferences"
import { ShoppingListSummary } from "~/lib/api/types/group"
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe"
import { computed, defineComponent, reactive, ref, useContext, watchEffect } from "@nuxtjs/composition-api";
import { toRefs } from "@vueuse/core";
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
import { ShoppingListSummary } from "~/lib/api/types/household";
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
export interface RecipeWithScale extends Recipe {
scale: number;
@ -209,7 +209,8 @@ export default defineComponent({
watchEffect(
() => {
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
openShoppingListIngredientDialog(shoppingListChoices.value[0]);
selectedShoppingList.value = shoppingListChoices.value[0];
openShoppingListIngredientDialog(selectedShoppingList.value);
} else {
ready.value = true;
}
@ -365,12 +366,8 @@ export default defineComponent({
}
})
const successMessage = promises.length === 1
? i18n.t("recipe.successfully-added-to-list") as string
: i18n.t("recipe.failed-to-add-to-list") as string;
success ? alert.success(successMessage)
: alert.error(i18n.t("failed-to-add-recipes-to-list") as string)
success ? alert.success(i18n.tc("recipe.successfully-added-to-list"))
: alert.error(i18n.tc("failed-to-add-recipes-to-list"))
state.shoppingListDialog = false;
state.shoppingListIngredientDialog = false;

View File

@ -66,7 +66,7 @@ import { defineComponent, computed, toRefs, reactive, useContext, useRoute } fro
import { useClipboard, useShare, whenever } from "@vueuse/core";
import { RecipeShareToken } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api";
import { useGroupSelf } from "~/composables/use-groups";
import { useHouseholdSelf } from "~/composables/use-households";
import { alert } from "~/composables/use-toast";
export default defineComponent({
@ -113,12 +113,12 @@ export default defineComponent({
);
const { $auth, i18n } = useContext();
const { group } = useGroupSelf();
const { household } = useHouseholdSelf();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const firstDayOfWeek = computed(() => {
return group.value?.preferences?.firstDayOfWeek || 0;
return household.value?.preferences?.firstDayOfWeek || 0;
});
// ============================================================

View File

@ -349,7 +349,7 @@ export default defineComponent({
{
icon: $globals.icons.update,
name: i18n.tc("general.updated"),
value: "update_at",
value: "updated_at",
},
{
icon: $globals.icons.diceMultiple,

View File

@ -11,7 +11,7 @@
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
import { RecipeIngredient } from "~/lib/api/types/group";
import { RecipeIngredient } from "~/lib/api/types/household";
import { useParsedIngredientText } from "~/composables/recipes";
export default defineComponent({

View File

@ -114,7 +114,7 @@ import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@n
import { whenever } from "@vueuse/core";
import { VForm } from "~/types/vuetify";
import { useUserApi } from "~/composables/api";
import { useGroupSelf } from "~/composables/use-groups";
import { useHouseholdSelf } from "~/composables/use-households";
import { Recipe, RecipeTimelineEventIn } from "~/lib/api/types/recipe";
export default defineComponent({
@ -131,7 +131,7 @@ export default defineComponent({
setup(props, context) {
const madeThisDialog = ref(false);
const userApi = useUserApi();
const { group } = useGroupSelf();
const { household } = useHouseholdSelf();
const { $auth, i18n } = useContext();
const domMadeThisForm = ref<VForm>();
const newTimelineEvent = ref<RecipeTimelineEventIn>({
@ -157,7 +157,7 @@ export default defineComponent({
);
const firstDayOfWeek = computed(() => {
return group.value?.preferences?.firstDayOfWeek || 0;
return household.value?.preferences?.firstDayOfWeek || 0;
});
function clearImage() {

View File

@ -31,7 +31,7 @@
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
import DOMPurify from "dompurify";
import { useFraction } from "~/composables/recipes/use-fraction";
import { ShoppingListItemOut } from "~/lib/api/types/group";
import { ShoppingListItemOut } from "~/lib/api/types/household";
import { RecipeSummary } from "~/lib/api/types/recipe";
export default defineComponent({

View File

@ -29,7 +29,7 @@
<script lang="ts">
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/group";
import { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/household";
interface actions {
text: string;

View File

@ -75,7 +75,7 @@
<v-row v-if="listItem.checked" no-gutters class="mb-2">
<v-col cols="auto">
<div class="text-caption font-weight-light font-italic">
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updateAt || "").toLocaleDateString($i18n.locale)}) }}
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updatedAt || "").toLocaleDateString($i18n.locale)}) }}
</div>
</v-col>
</v-row>
@ -99,7 +99,7 @@ import { defineComponent, computed, ref, useContext } from "@nuxtjs/composition-
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
import MultiPurposeLabel from "./MultiPurposeLabel.vue";
import { ShoppingListItemOut } from "~/lib/api/types/group";
import { ShoppingListItemOut } from "~/lib/api/types/household";
import { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";

View File

@ -111,7 +111,7 @@
<script lang="ts">
import { defineComponent, computed, watch } from "@nuxtjs/composition-api";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/group";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";

View File

@ -183,7 +183,7 @@ export default defineComponent({
{
icon: $globals.icons.calendarMultiselect,
title: i18n.tc("meal-plan.meal-planner"),
to: "/group/mealplan/planner/view",
to: "/household/mealplan/planner/view",
restricted: true,
},
{

View File

@ -1,5 +1,5 @@
<template>
<v-toolbar flat>
<v-toolbar color="transparent" flat>
<BaseButton color="null" rounded secondary @click="$router.go(-1)">
<template #icon> {{ $globals.icons.arrowLeftBold }}</template>
{{ $t('general.back') }}

View File

@ -85,6 +85,8 @@
:label="inputField.label"
:name="inputField.varName"
:items="inputField.options"
:item-text="inputField.itemText"
:item-value="inputField.itemValue"
:return-object="false"
:hint="inputField.hint"
persistent-hint

View File

@ -4,7 +4,7 @@ import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import { QueryValue } from "~/lib/api/base/route";
type BoundT = {
id?: string | number;
id?: string | number | null;
};
interface PublicStoreActions<T extends BoundT> {

View File

@ -160,6 +160,9 @@ export function usePageUser(): { user: UserOut } {
group: "",
groupId: "",
groupSlug: "",
household: "",
householdId: "",
householdSlug: "",
cacheKey: "",
email: "",
},

View File

@ -46,7 +46,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
}
const { quantity, food, unit, note } = ingredient;
const usePluralUnit = quantity !== undefined && (quantity * scale > 1 || quantity * scale === 0);
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
const usePluralFood = (!quantity) || quantity * scale > 1
let returnQty = "";
@ -69,8 +69,8 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
}
}
const unitName = useUnitName(unit, usePluralUnit);
const foodName = useFoodName(food, usePluralFood);
const unitName = useUnitName(unit || undefined, usePluralUnit);
const foodName = useFoodName(food || undefined, usePluralFood);
return {
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,

View File

@ -2,7 +2,7 @@ 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 { RecipeTag } from "~/lib/api/types/admin";
import { RecipeTag } from "~/lib/api/types/recipe";
const items: Ref<RecipeTag[]> = ref([]);
const publicStoreLoading = ref(false);

View File

@ -1,6 +1,7 @@
import { useAsync, ref, Ref, useContext } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { usePublicExploreApi } from "./api/api-client";
import { useHouseholdSelf } from "./use-households";
import { useUserApi } from "~/composables/api";
import { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
@ -67,6 +68,7 @@ export const usePublicCookbooks = function (groupSlug: string) {
export const useCookbooks = function () {
const api = useUserApi();
const { household } = useHouseholdSelf();
const loading = ref(false);
const { i18n } = useContext();
@ -100,7 +102,7 @@ export const useCookbooks = function () {
async createOne() {
loading.value = true;
const { data } = await api.cookbooks.createOne({
name: i18n.t("cookbook.cookbook-with-name", [String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
name: i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
});
if (data && cookbookStore?.value) {
cookbookStore.value.push(data);

View File

@ -1,7 +1,7 @@
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";
import { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
import { Recipe } from "~/lib/api/types/recipe";
const groupRecipeActions = ref<GroupRecipeActionOut[] | null>(null);
@ -10,7 +10,7 @@ const loading = ref(false);
export function useGroupRecipeActionData() {
const data = reactive({
id: "",
actionType: "link" as RecipeActionType,
actionType: "link" as GroupRecipeActionType,
title: "",
url: "",
});

View File

@ -1,7 +1,7 @@
import { useAsync, ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { useUserApi } from "~/composables/api";
import { ReadWebhook } from "~/lib/api/types/group";
import { ReadWebhook } from "~/lib/api/types/household";
export const useGroupWebhooks = function () {
const api = useUserApi();

View File

@ -51,7 +51,7 @@ export const useGroups = function () {
loading.value = true;
const asyncKey = String(Date.now());
const groups = useAsync(async () => {
const { data } = await api.groups.getAll();
const { data } = await api.groups.getAll(1, -1, {orderBy: "name", orderDirection: "asc"});;
if (data) {
return data.items;
@ -66,7 +66,7 @@ export const useGroups = function () {
async function refreshAllGroups() {
loading.value = true;
const { data } = await api.groups.getAll();
const { data } = await api.groups.getAll(1, -1, {orderBy: "name", orderDirection: "asc"});;
if (data) {
groups.value = data.items;

View File

@ -0,0 +1,117 @@
import { computed, ref, Ref, useAsync } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { HouseholdCreate, HouseholdInDB } from "~/lib/api/types/household";
const householdSelfRef = ref<HouseholdInDB | null>(null);
const loading = ref(false);
export const useHouseholdSelf = function () {
const api = useUserApi();
async function refreshHouseholdSelf() {
loading.value = true;
const { data } = await api.households.getCurrentUserHousehold();
householdSelfRef.value = data;
loading.value = false;
}
const actions = {
get() {
if (!(householdSelfRef.value || loading.value)) {
refreshHouseholdSelf();
}
return householdSelfRef;
},
async updatePreferences() {
if (!householdSelfRef.value) {
await refreshHouseholdSelf();
}
if (!householdSelfRef.value?.preferences) {
return;
}
const { data } = await api.households.setPreferences(householdSelfRef.value.preferences);
if (data) {
householdSelfRef.value.preferences = data;
}
},
};
const household = actions.get();
return { actions, household };
};
export const useHouseholds = function () {
const api = useUserApi();
const loading = ref(false);
function getAllHouseholds() {
loading.value = true;
const asyncKey = String(Date.now());
const households = useAsync(async () => {
const { data } = await api.households.getAll(1, -1, {orderBy: "name, group.name", orderDirection: "asc"});
if (data) {
return data.items;
} else {
return null;
}
}, asyncKey);
loading.value = false;
return households;
}
async function refreshAllHouseholds() {
loading.value = true;
const { data } = await api.households.getAll(1, -1, {orderBy: "name, group.name", orderDirection: "asc"});;
if (data) {
households.value = data.items;
} else {
households.value = null;
}
loading.value = false;
}
async function deleteHousehold(id: string | number) {
loading.value = true;
const { data } = await api.households.deleteOne(id);
loading.value = false;
refreshAllHouseholds();
return data;
}
async function createHousehold(payload: HouseholdCreate) {
loading.value = true;
const { data } = await api.households.createOne(payload);
if (data && households.value) {
households.value.push(data);
}
}
const households = getAllHouseholds();
function useHouseholdsInGroup(groupIdRef: Ref<string>) {
return computed(
() => {
return (households.value && groupIdRef.value)
? households.value.filter((h) => h.groupId === groupIdRef.value)
: [];
},
);
}
return {
households,
useHouseholdsInGroup,
getAllHouseholds,
refreshAllHouseholds,
deleteHousehold,
createHousehold,
};
};

View File

@ -1,7 +1,7 @@
import { computed, reactive, watch } from "@nuxtjs/composition-api";
import { useLocalStorage } from "@vueuse/core";
import { useUserApi } from "~/composables/api";
import { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/group";
import { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/household";
import { RequestResponse } from "~/lib/api/types/non-generated";
const localStorageKey = "shopping-list-queue";
@ -144,7 +144,7 @@ export function useShoppingListItemActions(shoppingListId: string) {
function checkUpdateState(list: ShoppingListOut) {
const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString();
if (list.updateAt && list.updateAt > cutoffDate) {
if (list.updatedAt && list.updatedAt > cutoffDate) {
// If the queue is too far behind the shopping list to reliably do updates, we clear the queue
console.log("Out of sync with server; clearing queue");
clearQueueItems("all");

View File

@ -8,6 +8,7 @@
"database-type": "Database Type",
"database-url": "Database URL",
"default-group": "Default Group",
"default-household": "Default Household",
"demo": "Demo",
"demo-status": "Demo Status",
"development": "Development",
@ -238,7 +239,7 @@
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
},
"manage-members": "Manage Members",
"manage-members-description": "Manage the permissions of the members in your groups. {manage} allows the user to access the data-management page {invite} allows the user to generate invitation links for other users. Group owners cannot change their own permissions.",
"manage-members-description": "Manage the permissions of the members in your household. {manage} allows the user to access the data-management page, and {invite} allows the user to generate invitation links for other users. Group owners cannot change their own permissions.",
"manage": "Manage",
"invite": "Invite",
"looking-to-update-your-profile": "Looking to Update Your Profile?",
@ -246,7 +247,7 @@
"default-recipe-preferences": "Default Recipe Preferences",
"group-preferences": "Group Preferences",
"private-group": "Private Group",
"private-group-description": "Setting your group to private will default all public view options to default. This overrides an individual recipes public view settings.",
"private-group-description": "Setting your group to private will default all public view options to default. This overrides any individual households or recipes public view settings.",
"enable-public-access": "Enable Public Access",
"enable-public-access-description": "Make group recipes public by default, and allow visitors to view recipes without logging-in",
"allow-users-outside-of-your-group-to-see-your-recipes": "Allow users outside of your group to see your recipes",
@ -260,7 +261,7 @@
"disable-users-from-commenting-on-recipes": "Disable users from commenting on recipes",
"disable-users-from-commenting-on-recipes-description": "Hides the comment section on the recipe page and disables commenting",
"disable-organizing-recipe-ingredients-by-units-and-food": "Disable organizing recipe ingredients by units and food",
"disable-organizing-recipe-ingredients-by-units-and-food-description": "Hides the Food, Unit, and Amount fields for ingredients and treats ingredients as plain text fields.",
"disable-organizing-recipe-ingredients-by-units-and-food-description": "Hides the Food, Unit, and Amount fields for ingredients and treats ingredients as plain text fields",
"general-preferences": "General Preferences",
"group-recipe-preferences": "Group Recipe Preferences",
"report": "Report",
@ -268,7 +269,28 @@
"group-management": "Group Management",
"admin-group-management": "Admin Group Management",
"admin-group-management-text": "Changes to this group will be reflected immediately.",
"group-id-value": "Group Id: {0}"
"group-id-value": "Group Id: {0}",
"total-households": "Total Households"
},
"household": {
"household": "Household",
"households": "Households",
"user-household": "User Household",
"create-household": "Create Household",
"household-name": "Household Name",
"household-group": "Household Group",
"household-management": "Household Management",
"manage-households": "Manage Households",
"admin-household-management": "Admin Household Management",
"admin-household-management-text": "Changes to this household will be reflected immediately.",
"household-id-value": "Household Id: {0}",
"private-household": "Private Household",
"private-household-description": "Setting your household to private will default all public view options to default. This overrides any individual recipes public view settings.",
"household-recipe-preferences": "Household Recipe Preferences",
"default-recipe-preferences-description": "These are the default settings when a new recipe is created in your household. These can be changed for individual recipes in the recipe settings menu.",
"allow-users-outside-of-your-household-to-see-your-recipes": "Allow users outside of your household to see your recipes",
"allow-users-outside-of-your-household-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your household or with a pre-generated private link",
"household-preferences": "Household Preferences"
},
"meal-plan": {
"create-a-new-meal-plan": "Create a New Meal Plan",
@ -1230,6 +1252,8 @@
"account-summary-description": "Here's a summary of your group's information.",
"group-statistics": "Group Statistics",
"group-statistics-description": "Your Group Statistics provide some insight how you're using Mealie.",
"household-statistics": "Household Statistics",
"household-statistics-description": "Your Household Statistics provide some insight how you're using Mealie.",
"storage-capacity": "Storage Capacity",
"storage-capacity-description": "Your storage capacity is a calculation of the images and assets you have uploaded.",
"personal": "Personal",
@ -1239,10 +1263,13 @@
"api-tokens-description": "Manage your API Tokens for access from external applications.",
"group-description": "These items are shared within your group. Editing one of them will change it for the whole group!",
"group-settings": "Group Settings",
"group-settings-description": "Manage your common group settings like mealplan and privacy settings.",
"group-settings-description": "Manage your common group settings, like privacy settings.",
"household-description": "These items are shared within your household. Editing one of them will change it for the whole household!",
"household-settings": "Household Settings",
"household-settings-description": "Manage your household settings, like mealplan and privacy settings.",
"cookbooks-description": "Manage a collection of recipe categories and generate pages for them.",
"members": "Members",
"members-description": "See who's in your group and manage their permissions.",
"members-description": "See who's in your household and manage their permissions.",
"webhooks-description": "Set up webhooks that trigger on days that you have mealplans scheduled.",
"notifiers": "Notifiers",
"notifiers-description": "Set up email and push notifications that trigger on specific events.",
@ -1277,6 +1304,7 @@
"require-all-tools": "Require All Tools",
"cookbook-name": "Cookbook Name",
"cookbook-with-name": "Cookbook {0}",
"household-cookbook-name": "{0} Cookbook {1}",
"create-a-cookbook": "Create a Cookbook",
"cookbook": "Cookbook"
}

View File

@ -64,6 +64,12 @@ export default defineComponent({
title: i18n.tc("user.users"),
restricted: true,
},
{
icon: $globals.icons.household,
to: "/admin/manage/households",
title: i18n.tc("household.households"),
restricted: true,
},
{
icon: $globals.icons.group,
to: "/admin/manage/groups",

View File

@ -1,5 +1,5 @@
<template>
<v-app dark>
<v-app v-if="ready" dark>
<v-card-title>
<slot>
<h1 class="mx-auto">{{ $t("page.404-page-not-found") }}</h1>
@ -75,9 +75,21 @@ export default defineComponent({
}
}
async function handle404() {
const normalizedRoute = route.value.fullPath.replace(/\/$/, "");
const newRoute = normalizedRoute.replace(/^\/group\/(mealplan|members|notifiers|webhooks)(\/.*)?$/, "/household/$1$2");
if (newRoute !== normalizedRoute) {
await router.replace(newRoute);
} else {
await insertGroupSlugIntoRoute();
}
ready.value = true;
}
if (props.error.statusCode === 404) {
// see if adding the groupSlug fixes the error
insertGroupSlugIntoRoute().then(() => { ready.value = true });
handle404();
} else {
ready.value = true;
}

View File

@ -1,5 +1,6 @@
import { RecipeAPI } from "./user/recipes";
import { UserApi } from "./user/users";
import { HouseholdAPI } from "./user/households";
import { GroupAPI } from "./user/groups";
import { BackupAPI } from "./user/backups";
import { UploadFile } from "./user/upload";
@ -28,6 +29,7 @@ import { ApiRequestInstance } from "~/lib/api/types/non-generated";
export class UserApiClient {
public recipes: RecipeAPI;
public users: UserApi;
public households: HouseholdAPI;
public groups: GroupAPI;
public backups: BackupAPI;
public categories: CategoriesAPI;
@ -63,6 +65,7 @@ export class UserApiClient {
// Users
this.users = new UserApi(requests);
this.households = new HouseholdAPI(requests);
this.groups = new GroupAPI(requests);
this.cookbooks = new CookbookAPI(requests);
this.groupRecipeActions = new GroupRecipeActionsAPI(requests);

View File

@ -3,10 +3,11 @@ import { RecipeCookBook } from "~/lib/api/types/cookbook";
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
const prefix = "/api";
const exploreGroupSlug = (groupSlug: string | number) => `${prefix}/explore/groups/${groupSlug}`
const routes = {
cookbooksGroupSlug: (groupSlug: string | number) => `${prefix}/explore/cookbooks/${groupSlug}`,
cookbooksGroupSlugCookbookId: (groupSlug: string | number, cookbookId: string | number) => `${prefix}/explore/cookbooks/${groupSlug}/${cookbookId}`,
cookbooksGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/cookbooks`,
cookbooksGroupSlugCookbookId: (groupSlug: string | number, cookbookId: string | number) => `${exploreGroupSlug(groupSlug)}/cookbooks/${cookbookId}`,
};
export class PublicCookbooksApi extends BaseCRUDAPIReadOnly<RecipeCookBook> {

View File

@ -3,10 +3,11 @@ import { IngredientFood } from "~/lib/api/types/recipe";
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
const prefix = "/api";
const exploreGroupSlug = (groupSlug: string | number) => `${prefix}/explore/groups/${groupSlug}`
const routes = {
foodsGroupSlug: (groupSlug: string | number) => `${prefix}/explore/foods/${groupSlug}`,
foodsGroupSlugFoodId: (groupSlug: string | number, foodId: string | number) => `${prefix}/explore/foods/${groupSlug}/${foodId}`,
foodsGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/foods`,
foodsGroupSlugFoodId: (groupSlug: string | number, foodId: string | number) => `${exploreGroupSlug(groupSlug)}/foods/${foodId}`,
};
export class PublicFoodsApi extends BaseCRUDAPIReadOnly<IngredientFood> {

View File

@ -3,14 +3,15 @@ import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
const prefix = "/api";
const exploreGroupSlug = (groupSlug: string | number) => `${prefix}/explore/groups/${groupSlug}`
const routes = {
categoriesGroupSlug: (groupSlug: string | number) => `${prefix}/explore/organizers/${groupSlug}/categories`,
categoriesGroupSlugCategoryId: (groupSlug: string | number, categoryId: string | number) => `${prefix}/explore/organizers/${groupSlug}/categories/${categoryId}`,
tagsGroupSlug: (groupSlug: string | number) => `${prefix}/explore/organizers/${groupSlug}/tags`,
tagsGroupSlugTagId: (groupSlug: string | number, tagId: string | number) => `${prefix}/explore/organizers/${groupSlug}/tags/${tagId}`,
toolsGroupSlug: (groupSlug: string | number) => `${prefix}/explore/organizers/${groupSlug}/tools`,
toolsGroupSlugToolId: (groupSlug: string | number, toolId: string | number) => `${prefix}/explore/organizers/${groupSlug}/tools/${toolId}`,
categoriesGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/organizers/categories`,
categoriesGroupSlugCategoryId: (groupSlug: string | number, categoryId: string | number) => `${exploreGroupSlug(groupSlug)}/organizers/categories/${categoryId}`,
tagsGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/organizers/tags`,
tagsGroupSlugTagId: (groupSlug: string | number, tagId: string | number) => `${exploreGroupSlug(groupSlug)}/organizers/tags/${tagId}`,
toolsGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/organizers/tools`,
toolsGroupSlugToolId: (groupSlug: string | number, toolId: string | number) => `${exploreGroupSlug(groupSlug)}/organizers/tools`,
};
export class PublicCategoriesApi extends BaseCRUDAPIReadOnly<RecipeCategory> {

View File

@ -5,10 +5,11 @@ import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generate
import { RecipeSearchQuery } from "../../user/recipes/recipe";
const prefix = "/api";
const exploreGroupSlug = (groupSlug: string | number) => `${prefix}/explore/groups/${groupSlug}`
const routes = {
recipesGroupSlug: (groupSlug: string | number) => `${prefix}/explore/recipes/${groupSlug}`,
recipesGroupSlugRecipeSlug: (groupSlug: string | number, recipeSlug: string | number) => `${prefix}/explore/recipes/${groupSlug}/${recipeSlug}`,
recipesGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/recipes`,
recipesGroupSlugRecipeSlug: (groupSlug: string | number, recipeSlug: string | number) => `${exploreGroupSlug(groupSlug)}/recipes/${recipeSlug}`,
};
export class PublicRecipeApi extends BaseCRUDAPIReadOnly<Recipe> {

View File

@ -10,6 +10,8 @@ export interface AdminAboutInfo {
version: string;
demoStatus: boolean;
allowSignup: boolean;
defaultGroupSlug?: string | null;
defaultHouseholdSlug?: string | null;
enableOidc: boolean;
oidcRedirect: boolean;
oidcProviderName: string;
@ -18,8 +20,9 @@ export interface AdminAboutInfo {
apiPort: number;
apiDocs: boolean;
dbType: string;
dbUrl?: string;
dbUrl?: string | null;
defaultGroup: string;
defaultHousehold: string;
buildId: string;
recipeScraperVersion: string;
}
@ -37,7 +40,8 @@ export interface AppInfo {
version: string;
demoStatus: boolean;
allowSignup: boolean;
defaultGroupSlug?: string;
defaultGroupSlug?: string | null;
defaultHouseholdSlug?: string | null;
enableOidc: boolean;
oidcRedirect: boolean;
oidcProviderName: string;
@ -51,6 +55,7 @@ export interface AppStartupInfo {
export interface AppStatistics {
totalRecipes: number;
totalUsers: number;
totalHouseholds: number;
totalGroups: number;
uncategorizedRecipes: number;
untaggedRecipes: number;
@ -93,16 +98,16 @@ export interface ChowdownURL {
export interface CommentImport {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}
export interface CreateBackup {
tag?: string;
tag?: string | null;
options: BackupOptions;
templates?: string[];
templates?: string[] | null;
}
export interface CustomPageBase {
name: string;
slug?: string;
slug: string | null;
position: number;
categories?: RecipeCategoryResponse[];
}
@ -113,38 +118,41 @@ export interface RecipeCategoryResponse {
recipes?: RecipeSummary[];
}
export interface RecipeSummary {
id?: string;
id?: string | null;
userId?: string;
householdId?: string;
groupId?: string;
name?: string;
name?: string | null;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeCategory[];
tags?: RecipeTag[];
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
cookTime?: string | null;
performTime?: string | null;
description?: string | null;
recipeCategory?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
dateAdded?: string;
dateUpdated?: string;
createdAt?: string;
updateAt?: string;
lastMade?: string;
rating?: number | null;
orgURL?: string | null;
dateAdded?: string | null;
dateUpdated?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
lastMade?: string | null;
}
export interface RecipeCategory {
id?: string;
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTag {
id?: string;
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTool {
id: string;
@ -155,11 +163,11 @@ export interface RecipeTool {
export interface CustomPageImport {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}
export interface CustomPageOut {
name: string;
slug?: string;
slug: string | null;
position: number;
categories?: RecipeCategoryResponse[];
id: number;
@ -169,7 +177,7 @@ export interface EmailReady {
}
export interface EmailSuccess {
success: boolean;
error?: string;
error?: string | null;
}
export interface EmailTest {
email: string;
@ -177,12 +185,12 @@ export interface EmailTest {
export interface GroupImport {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}
export interface ImportBase {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}
export interface ImportJob {
recipes?: boolean;
@ -217,8 +225,8 @@ export interface MigrationFile {
export interface MigrationImport {
name: string;
status: boolean;
exception?: string;
slug?: string;
exception?: string | null;
slug?: string | null;
}
export interface Migrations {
type: string;
@ -227,25 +235,26 @@ export interface Migrations {
export interface NotificationImport {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}
export interface OIDCInfo {
configurationUrl?: string;
clientId?: string;
configurationUrl: string | null;
clientId: string | null;
groupsClaim: string | null;
}
export interface RecipeImport {
name: string;
status: boolean;
exception?: string;
slug?: string;
exception?: string | null;
slug?: string | null;
}
export interface SettingsImport {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}
export interface UserImport {
name: string;
status: boolean;
exception?: string;
exception?: string | null;
}

View File

@ -8,7 +8,7 @@
export interface CreateCookBook {
name: string;
description?: string;
slug?: string;
slug?: string | null;
position?: number;
public?: boolean;
categories?: CategoryBase[];
@ -37,7 +37,7 @@ export interface RecipeTool {
export interface ReadCookBook {
name: string;
description?: string;
slug?: string;
slug?: string | null;
position?: number;
public?: boolean;
categories?: CategoryBase[];
@ -47,12 +47,13 @@ export interface ReadCookBook {
requireAllTags?: boolean;
requireAllTools?: boolean;
groupId: string;
householdId: string;
id: string;
}
export interface RecipeCookBook {
name: string;
description?: string;
slug?: string;
slug?: string | null;
position?: number;
public?: boolean;
categories?: CategoryBase[];
@ -62,47 +63,51 @@ export interface RecipeCookBook {
requireAllTags?: boolean;
requireAllTools?: boolean;
groupId: string;
householdId: string;
id: string;
recipes: RecipeSummary[];
}
export interface RecipeSummary {
id?: string;
id?: string | null;
userId?: string;
householdId?: string;
groupId?: string;
name?: string;
name?: string | null;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeCategory[];
tags?: RecipeTag[];
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
cookTime?: string | null;
performTime?: string | null;
description?: string | null;
recipeCategory?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
dateAdded?: string;
dateUpdated?: string;
createdAt?: string;
updateAt?: string;
lastMade?: string;
rating?: number | null;
orgURL?: string | null;
dateAdded?: string | null;
dateUpdated?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
lastMade?: string | null;
}
export interface RecipeCategory {
id?: string;
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTag {
id?: string;
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface SaveCookBook {
name: string;
description?: string;
slug?: string;
slug?: string | null;
position?: number;
public?: boolean;
categories?: CategoryBase[];
@ -112,11 +117,12 @@ export interface SaveCookBook {
requireAllTags?: boolean;
requireAllTools?: boolean;
groupId: string;
householdId: string;
}
export interface UpdateCookBook {
name: string;
description?: string;
slug?: string;
slug?: string | null;
position?: number;
public?: boolean;
categories?: CategoryBase[];
@ -126,5 +132,6 @@ export interface UpdateCookBook {
requireAllTags?: boolean;
requireAllTools?: boolean;
groupId: string;
householdId: string;
id: string;
}

View File

@ -5,10 +5,6 @@
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export type RecipeActionType =
| "link"
| "post";
export type WebhookType = "mealplan";
export type SupportedMigrations =
| "nextcloud"
| "chowdown"
@ -17,59 +13,23 @@ export type SupportedMigrations =
| "mealie_alpha"
| "tandoor"
| "plantoeat"
| "myrecipebox"
| "recipekeeper";
export interface CreateGroupPreferences {
privateGroup?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
groupId: string;
}
export interface CreateGroupRecipeAction {
actionType: RecipeActionType;
title: string;
url: string;
}
export interface CreateInviteToken {
uses: number;
}
export interface CreateWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
}
export interface DataMigrationCreate {
sourceType: SupportedMigrations;
}
export interface EmailInitationResponse {
success: boolean;
error?: string;
}
export interface EmailInvitation {
email: string;
token: string;
}
export interface GroupAdminUpdate {
id: string;
name: string;
preferences?: UpdateGroupPreferences;
preferences?: UpdateGroupPreferences | null;
}
export interface UpdateGroupPreferences {
privateGroup?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
}
export interface GroupDataExport {
id: string;
@ -80,140 +40,6 @@ export interface GroupDataExport {
size: string;
expires: string;
}
export interface GroupEventNotifierCreate {
name: string;
appriseUrl: string;
}
/**
* These events are in-sync with the EventTypes found in the EventBusService.
* If you modify this, make sure to update the EventBusService as well.
*/
export interface GroupEventNotifierOptions {
testMessage?: boolean;
webhookTask?: boolean;
recipeCreated?: boolean;
recipeUpdated?: boolean;
recipeDeleted?: boolean;
userSignup?: boolean;
dataMigrations?: boolean;
dataExport?: boolean;
dataImport?: boolean;
mealplanEntryCreated?: boolean;
shoppingListCreated?: boolean;
shoppingListUpdated?: boolean;
shoppingListDeleted?: boolean;
cookbookCreated?: boolean;
cookbookUpdated?: boolean;
cookbookDeleted?: boolean;
tagCreated?: boolean;
tagUpdated?: boolean;
tagDeleted?: boolean;
categoryCreated?: boolean;
categoryUpdated?: boolean;
categoryDeleted?: boolean;
}
/**
* These events are in-sync with the EventTypes found in the EventBusService.
* If you modify this, make sure to update the EventBusService as well.
*/
export interface GroupEventNotifierOptionsOut {
testMessage?: boolean;
webhookTask?: boolean;
recipeCreated?: boolean;
recipeUpdated?: boolean;
recipeDeleted?: boolean;
userSignup?: boolean;
dataMigrations?: boolean;
dataExport?: boolean;
dataImport?: boolean;
mealplanEntryCreated?: boolean;
shoppingListCreated?: boolean;
shoppingListUpdated?: boolean;
shoppingListDeleted?: boolean;
cookbookCreated?: boolean;
cookbookUpdated?: boolean;
cookbookDeleted?: boolean;
tagCreated?: boolean;
tagUpdated?: boolean;
tagDeleted?: boolean;
categoryCreated?: boolean;
categoryUpdated?: boolean;
categoryDeleted?: boolean;
id: string;
}
/**
* These events are in-sync with the EventTypes found in the EventBusService.
* If you modify this, make sure to update the EventBusService as well.
*/
export interface GroupEventNotifierOptionsSave {
testMessage?: boolean;
webhookTask?: boolean;
recipeCreated?: boolean;
recipeUpdated?: boolean;
recipeDeleted?: boolean;
userSignup?: boolean;
dataMigrations?: boolean;
dataExport?: boolean;
dataImport?: boolean;
mealplanEntryCreated?: boolean;
shoppingListCreated?: boolean;
shoppingListUpdated?: boolean;
shoppingListDeleted?: boolean;
cookbookCreated?: boolean;
cookbookUpdated?: boolean;
cookbookDeleted?: boolean;
tagCreated?: boolean;
tagUpdated?: boolean;
tagDeleted?: boolean;
categoryCreated?: boolean;
categoryUpdated?: boolean;
categoryDeleted?: boolean;
notifierId: string;
}
export interface GroupEventNotifierOut {
id: string;
name: string;
enabled: boolean;
groupId: string;
options: GroupEventNotifierOptionsOut;
}
export interface GroupEventNotifierPrivate {
id: string;
name: string;
enabled: boolean;
groupId: string;
options: GroupEventNotifierOptionsOut;
appriseUrl: string;
}
export interface GroupEventNotifierSave {
name: string;
appriseUrl: string;
enabled?: boolean;
groupId: string;
options?: GroupEventNotifierOptions;
}
export interface GroupEventNotifierUpdate {
name: string;
appriseUrl?: string;
enabled?: boolean;
groupId: string;
options?: GroupEventNotifierOptions;
id: string;
}
export interface GroupRecipeActionOut {
actionType: RecipeActionType;
title: string;
url: string;
groupId: string;
id: string;
}
export interface GroupStatistics {
totalRecipes: number;
totalUsers: number;
totalCategories: number;
totalTags: number;
totalTools: number;
}
export interface GroupStorage {
usedStorageBytes: number;
usedStorageStr: string;
@ -222,408 +48,9 @@ export interface GroupStorage {
}
export interface ReadGroupPreferences {
privateGroup?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
groupId: string;
id: string;
}
export interface ReadInviteToken {
token: string;
usesLeft: number;
groupId: string;
}
export interface ReadWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
id: string;
}
export interface SaveGroupRecipeAction {
actionType: RecipeActionType;
title: string;
url: string;
groupId: string;
}
export interface SaveInviteToken {
usesLeft: number;
groupId: string;
token: string;
}
export interface SaveWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
}
export interface SeederConfig {
locale: string;
}
export interface SetPermissions {
userId: string;
canManage?: boolean;
canInvite?: boolean;
canOrganize?: boolean;
}
export interface ShoppingListAddRecipeParams {
recipeIncrementQuantity?: number;
recipeIngredients?: RecipeIngredient[];
}
export interface RecipeIngredient {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
title?: string;
originalText?: string;
referenceId?: string;
}
export interface IngredientUnit {
name: string;
pluralName?: string;
description?: string;
extras?: {
[k: string]: unknown;
};
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string;
useAbbreviation?: boolean;
aliases?: IngredientUnitAlias[];
id: string;
createdAt?: string;
updateAt?: string;
}
export interface IngredientUnitAlias {
name: string;
}
export interface CreateIngredientUnit {
name: string;
pluralName?: string;
description?: string;
extras?: {
[k: string]: unknown;
};
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string;
useAbbreviation?: boolean;
aliases?: CreateIngredientUnitAlias[];
}
export interface CreateIngredientUnitAlias {
name: string;
}
export interface IngredientFood {
name: string;
pluralName?: string;
description?: string;
extras?: {
[k: string]: unknown;
};
labelId?: string;
aliases?: IngredientFoodAlias[];
id: string;
label?: MultiPurposeLabelSummary;
createdAt?: string;
updateAt?: string;
}
export interface IngredientFoodAlias {
name: string;
}
export interface MultiPurposeLabelSummary {
name: string;
color?: string;
groupId: string;
id: string;
}
export interface CreateIngredientFood {
name: string;
pluralName?: string;
description?: string;
extras?: {
[k: string]: unknown;
};
labelId?: string;
aliases?: CreateIngredientFoodAlias[];
}
export interface CreateIngredientFoodAlias {
name: string;
}
export interface ShoppingListCreate {
name?: string;
extras?: {
[k: string]: unknown;
};
createdAt?: string;
updateAt?: string;
}
export interface ShoppingListItemBase {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string;
labelId?: string;
unitId?: string;
extras?: {
[k: string]: unknown;
};
}
export interface ShoppingListItemCreate {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string;
labelId?: string;
unitId?: string;
extras?: {
[k: string]: unknown;
};
recipeReferences?: ShoppingListItemRecipeRefCreate[];
}
export interface ShoppingListItemRecipeRefCreate {
recipeId: string;
recipeQuantity?: number;
recipeScale?: number;
recipeNote?: string;
}
export interface ShoppingListItemOut {
quantity?: number;
unit?: IngredientUnit;
food?: IngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string;
labelId?: string;
unitId?: string;
extras?: {
[k: string]: unknown;
};
id: string;
label?: MultiPurposeLabelSummary;
recipeReferences?: ShoppingListItemRecipeRefOut[];
createdAt?: string;
updateAt?: string;
}
export interface ShoppingListItemRecipeRefOut {
recipeId: string;
recipeQuantity?: number;
recipeScale?: number;
recipeNote?: string;
id: string;
shoppingListItemId: string;
}
export interface ShoppingListItemRecipeRefUpdate {
recipeId: string;
recipeQuantity?: number;
recipeScale?: number;
recipeNote?: string;
id: string;
shoppingListItemId: string;
}
export interface ShoppingListItemUpdate {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string;
labelId?: string;
unitId?: string;
extras?: {
[k: string]: unknown;
};
recipeReferences?: (ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate)[];
}
/**
* Only used for bulk update operations where the shopping list item id isn't already supplied
*/
export interface ShoppingListItemUpdateBulk {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string;
labelId?: string;
unitId?: string;
extras?: {
[k: string]: unknown;
};
recipeReferences?: (ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate)[];
id: string;
}
/**
* Container for bulk shopping list item changes
*/
export interface ShoppingListItemsCollectionOut {
createdItems?: ShoppingListItemOut[];
updatedItems?: ShoppingListItemOut[];
deletedItems?: ShoppingListItemOut[];
}
export interface ShoppingListMultiPurposeLabelCreate {
shoppingListId: string;
labelId: string;
position?: number;
}
export interface ShoppingListMultiPurposeLabelOut {
shoppingListId: string;
labelId: string;
position?: number;
id: string;
label: MultiPurposeLabelSummary;
}
export interface ShoppingListMultiPurposeLabelUpdate {
shoppingListId: string;
labelId: string;
position?: number;
id: string;
}
export interface ShoppingListOut {
name?: string;
extras?: {
[k: string]: unknown;
};
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
id: string;
listItems?: ShoppingListItemOut[];
recipeReferences: ShoppingListRecipeRefOut[];
labelSettings: ShoppingListMultiPurposeLabelOut[];
}
export interface ShoppingListRecipeRefOut {
id: string;
shoppingListId: string;
recipeId: string;
recipeQuantity: number;
recipe: RecipeSummary;
}
export interface RecipeSummary {
id?: string;
userId?: string;
groupId?: string;
name?: string;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeCategory[];
tags?: RecipeTag[];
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
dateAdded?: string;
dateUpdated?: string;
createdAt?: string;
updateAt?: string;
lastMade?: string;
}
export interface RecipeCategory {
id?: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
name: string;
slug: string;
}
export interface RecipeTool {
id: string;
name: string;
slug: string;
onHand?: boolean;
}
export interface ShoppingListRemoveRecipeParams {
recipeDecrementQuantity?: number;
}
export interface ShoppingListSave {
name?: string;
extras?: {
[k: string]: unknown;
};
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
}
export interface ShoppingListSummary {
name?: string;
extras?: {
[k: string]: unknown;
};
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
id: string;
recipeReferences: ShoppingListRecipeRefOut[];
labelSettings: ShoppingListMultiPurposeLabelOut[];
}
export interface ShoppingListUpdate {
name?: string;
extras?: {
[k: string]: unknown;
};
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
id: string;
listItems?: ShoppingListItemOut[];
}
export interface RecipeIngredientBase {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
display?: string;
}

View File

@ -0,0 +1,664 @@
/* tslint:disable */
/* eslint-disable */
/**
/* This file was automatically generated from pydantic models by running pydantic2ts.
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export type GroupRecipeActionType = "link" | "post";
export type WebhookType = "mealplan";
export interface CreateGroupRecipeAction {
actionType: GroupRecipeActionType;
title: string;
url: string;
}
export interface CreateHouseholdPreferences {
privateHousehold?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
}
export interface CreateInviteToken {
uses: number;
}
export interface CreateWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
}
export interface EmailInitationResponse {
success: boolean;
error?: string | null;
}
export interface EmailInvitation {
email: string;
token: string;
}
export interface GroupEventNotifierCreate {
name: string;
appriseUrl?: string | null;
}
/**
* These events are in-sync with the EventTypes found in the EventBusService.
* If you modify this, make sure to update the EventBusService as well.
*/
export interface GroupEventNotifierOptions {
testMessage?: boolean;
webhookTask?: boolean;
recipeCreated?: boolean;
recipeUpdated?: boolean;
recipeDeleted?: boolean;
userSignup?: boolean;
dataMigrations?: boolean;
dataExport?: boolean;
dataImport?: boolean;
mealplanEntryCreated?: boolean;
shoppingListCreated?: boolean;
shoppingListUpdated?: boolean;
shoppingListDeleted?: boolean;
cookbookCreated?: boolean;
cookbookUpdated?: boolean;
cookbookDeleted?: boolean;
tagCreated?: boolean;
tagUpdated?: boolean;
tagDeleted?: boolean;
categoryCreated?: boolean;
categoryUpdated?: boolean;
categoryDeleted?: boolean;
}
export interface GroupEventNotifierOptionsOut {
testMessage?: boolean;
webhookTask?: boolean;
recipeCreated?: boolean;
recipeUpdated?: boolean;
recipeDeleted?: boolean;
userSignup?: boolean;
dataMigrations?: boolean;
dataExport?: boolean;
dataImport?: boolean;
mealplanEntryCreated?: boolean;
shoppingListCreated?: boolean;
shoppingListUpdated?: boolean;
shoppingListDeleted?: boolean;
cookbookCreated?: boolean;
cookbookUpdated?: boolean;
cookbookDeleted?: boolean;
tagCreated?: boolean;
tagUpdated?: boolean;
tagDeleted?: boolean;
categoryCreated?: boolean;
categoryUpdated?: boolean;
categoryDeleted?: boolean;
id: string;
}
export interface GroupEventNotifierOptionsSave {
testMessage?: boolean;
webhookTask?: boolean;
recipeCreated?: boolean;
recipeUpdated?: boolean;
recipeDeleted?: boolean;
userSignup?: boolean;
dataMigrations?: boolean;
dataExport?: boolean;
dataImport?: boolean;
mealplanEntryCreated?: boolean;
shoppingListCreated?: boolean;
shoppingListUpdated?: boolean;
shoppingListDeleted?: boolean;
cookbookCreated?: boolean;
cookbookUpdated?: boolean;
cookbookDeleted?: boolean;
tagCreated?: boolean;
tagUpdated?: boolean;
tagDeleted?: boolean;
categoryCreated?: boolean;
categoryUpdated?: boolean;
categoryDeleted?: boolean;
notifierId: string;
}
export interface GroupEventNotifierOut {
id: string;
name: string;
enabled: boolean;
groupId: string;
householdId: string;
options: GroupEventNotifierOptionsOut;
}
export interface GroupEventNotifierPrivate {
id: string;
name: string;
enabled: boolean;
groupId: string;
householdId: string;
options: GroupEventNotifierOptionsOut;
appriseUrl: string;
}
export interface GroupEventNotifierSave {
name: string;
appriseUrl?: string | null;
enabled?: boolean;
groupId: string;
householdId: string;
options?: GroupEventNotifierOptions;
}
export interface GroupEventNotifierUpdate {
name: string;
appriseUrl?: string | null;
enabled?: boolean;
groupId: string;
householdId: string;
options?: GroupEventNotifierOptions;
id: string;
}
export interface GroupRecipeActionOut {
actionType: GroupRecipeActionType;
title: string;
url: string;
groupId: string;
householdId: string;
id: string;
}
export interface HouseholdCreate {
groupId?: string | null;
name: string;
}
export interface HouseholdInDB {
groupId: string;
name: string;
id: string;
slug: string;
preferences?: ReadHouseholdPreferences | null;
group: string;
users?: HouseholdUserSummary[] | null;
webhooks?: ReadWebhook[];
}
export interface ReadHouseholdPreferences {
privateHousehold?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
id: string;
}
export interface HouseholdUserSummary {
id: string;
fullName: string;
}
export interface ReadWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
householdId: string;
id: string;
}
export interface HouseholdSave {
groupId: string;
name: string;
}
export interface HouseholdStatistics {
totalRecipes: number;
totalUsers: number;
totalCategories: number;
totalTags: number;
totalTools: number;
}
export interface HouseholdSummary {
groupId: string;
name: string;
id: string;
slug: string;
preferences?: ReadHouseholdPreferences | null;
}
export interface ReadInviteToken {
token: string;
usesLeft: number;
groupId: string;
householdId: string;
}
export interface SaveGroupRecipeAction {
actionType: GroupRecipeActionType;
title: string;
url: string;
groupId: string;
householdId: string;
}
export interface SaveHouseholdPreferences {
privateHousehold?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
householdId: string;
}
export interface SaveInviteToken {
usesLeft: number;
groupId: string;
householdId: string;
token: string;
}
export interface SaveWebhook {
enabled?: boolean;
name?: string;
url?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
householdId: string;
}
export interface SetPermissions {
userId: string;
canManage?: boolean;
canInvite?: boolean;
canOrganize?: boolean;
}
export interface ShoppingListAddRecipeParams {
recipeIncrementQuantity?: number;
recipeIngredients?: RecipeIngredient[] | null;
}
export interface RecipeIngredient {
quantity?: number | null;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean;
display?: string;
title?: string | null;
originalText?: string | null;
referenceId?: string;
}
export interface IngredientUnit {
id: string;
name: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
} | null;
onHand?: boolean;
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string | null;
useAbbreviation?: boolean;
aliases?: IngredientUnitAlias[];
createdAt?: string | null;
updatedAt?: string | null;
}
export interface IngredientUnitAlias {
name: string;
[k: string]: unknown;
}
export interface CreateIngredientUnit {
id?: string | null;
name: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
} | null;
onHand?: boolean;
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string | null;
useAbbreviation?: boolean;
aliases?: CreateIngredientUnitAlias[];
[k: string]: unknown;
}
export interface CreateIngredientUnitAlias {
name: string;
[k: string]: unknown;
}
export interface IngredientFood {
id: string;
name: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
} | null;
onHand?: boolean;
labelId?: string | null;
aliases?: IngredientFoodAlias[];
label?: MultiPurposeLabelSummary | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface IngredientFoodAlias {
name: string;
[k: string]: unknown;
}
export interface MultiPurposeLabelSummary {
name: string;
color?: string;
groupId: string;
id: string;
}
export interface CreateIngredientFood {
id?: string | null;
name: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
} | null;
onHand?: boolean;
labelId?: string | null;
aliases?: CreateIngredientFoodAlias[];
[k: string]: unknown;
}
export interface CreateIngredientFoodAlias {
name: string;
[k: string]: unknown;
}
export interface ShoppingListCreate {
name?: string | null;
extras?: {
[k: string]: unknown;
} | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface ShoppingListItemBase {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string | null;
labelId?: string | null;
unitId?: string | null;
extras?: {
[k: string]: unknown;
} | null;
}
export interface ShoppingListItemCreate {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string | null;
labelId?: string | null;
unitId?: string | null;
extras?: {
[k: string]: unknown;
} | null;
id?: string | null;
recipeReferences?: ShoppingListItemRecipeRefCreate[];
}
export interface ShoppingListItemRecipeRefCreate {
recipeId: string;
recipeQuantity?: number;
recipeScale?: number | null;
recipeNote?: string | null;
}
export interface ShoppingListItemOut {
quantity?: number;
unit?: IngredientUnit | null;
food?: IngredientFood | null;
note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string | null;
labelId?: string | null;
unitId?: string | null;
extras?: {
[k: string]: unknown;
} | null;
id: string;
groupId: string;
householdId: string;
label?: MultiPurposeLabelSummary | null;
recipeReferences?: ShoppingListItemRecipeRefOut[];
createdAt?: string | null;
updatedAt?: string | null;
}
export interface ShoppingListItemRecipeRefOut {
recipeId: string;
recipeQuantity?: number;
recipeScale?: number | null;
recipeNote?: string | null;
id: string;
shoppingListItemId: string;
}
export interface ShoppingListItemRecipeRefUpdate {
recipeId: string;
recipeQuantity?: number;
recipeScale?: number | null;
recipeNote?: string | null;
id: string;
shoppingListItemId: string;
}
export interface ShoppingListItemUpdate {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string | null;
labelId?: string | null;
unitId?: string | null;
extras?: {
[k: string]: unknown;
} | null;
recipeReferences?: (ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate)[];
}
/**
* Only used for bulk update operations where the shopping list item id isn't already supplied
*/
export interface ShoppingListItemUpdateBulk {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean;
disableAmount?: boolean | null;
display?: string;
shoppingListId: string;
checked?: boolean;
position?: number;
foodId?: string | null;
labelId?: string | null;
unitId?: string | null;
extras?: {
[k: string]: unknown;
} | null;
recipeReferences?: (ShoppingListItemRecipeRefCreate | ShoppingListItemRecipeRefUpdate)[];
id: string;
}
/**
* Container for bulk shopping list item changes
*/
export interface ShoppingListItemsCollectionOut {
createdItems?: ShoppingListItemOut[];
updatedItems?: ShoppingListItemOut[];
deletedItems?: ShoppingListItemOut[];
}
export interface ShoppingListMultiPurposeLabelCreate {
shoppingListId: string;
labelId: string;
position?: number;
}
export interface ShoppingListMultiPurposeLabelOut {
shoppingListId: string;
labelId: string;
position?: number;
id: string;
label: MultiPurposeLabelSummary;
}
export interface ShoppingListMultiPurposeLabelUpdate {
shoppingListId: string;
labelId: string;
position?: number;
id: string;
}
export interface ShoppingListOut {
name?: string | null;
extras?: {
[k: string]: unknown;
} | null;
createdAt?: string | null;
updatedAt?: string | null;
groupId: string;
userId: string;
id: string;
listItems?: ShoppingListItemOut[];
householdId: string;
recipeReferences?: ShoppingListRecipeRefOut[];
labelSettings?: ShoppingListMultiPurposeLabelOut[];
}
export interface ShoppingListRecipeRefOut {
id: string;
shoppingListId: string;
recipeId: string;
recipeQuantity: number;
recipe: RecipeSummary;
}
export interface RecipeSummary {
id?: string | null;
userId?: string;
householdId?: string;
groupId?: string;
name?: string | null;
slug?: string;
image?: unknown;
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
cookTime?: string | null;
performTime?: string | null;
description?: string | null;
recipeCategory?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
tools?: RecipeTool[];
rating?: number | null;
orgURL?: string | null;
dateAdded?: string | null;
dateUpdated?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
lastMade?: string | null;
}
export interface RecipeCategory {
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTag {
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTool {
id: string;
name: string;
slug: string;
onHand?: boolean;
}
export interface ShoppingListRemoveRecipeParams {
recipeDecrementQuantity?: number;
}
export interface ShoppingListSave {
name?: string | null;
extras?: {
[k: string]: unknown;
} | null;
createdAt?: string | null;
updatedAt?: string | null;
groupId: string;
userId: string;
}
export interface ShoppingListSummary {
name?: string | null;
extras?: {
[k: string]: unknown;
} | null;
createdAt?: string | null;
updatedAt?: string | null;
groupId: string;
userId: string;
id: string;
householdId: string;
recipeReferences: ShoppingListRecipeRefOut[];
labelSettings: ShoppingListMultiPurposeLabelOut[];
}
export interface ShoppingListUpdate {
name?: string | null;
extras?: {
[k: string]: unknown;
} | null;
createdAt?: string | null;
updatedAt?: string | null;
groupId: string;
userId: string;
id: string;
listItems?: ShoppingListItemOut[];
}
export interface UpdateHousehold {
groupId: string;
name: string;
id: string;
slug: string;
}
export interface UpdateHouseholdAdmin {
groupId: string;
name: string;
id: string;
preferences?: UpdateHouseholdPreferences | null;
}
export interface UpdateHouseholdPreferences {
privateHousehold?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
}
export interface RecipeIngredientBase {
quantity?: number | null;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean | null;
display?: string;
}

View File

@ -19,46 +19,18 @@ export interface CreatePlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: string;
recipeId?: string | null;
}
export interface CreateRandomEntry {
date: string;
entryType?: PlanEntryType & string;
}
export interface ListItem {
title?: string;
title?: string | null;
text?: string;
quantity?: number;
checked?: boolean;
}
export interface MealDayIn {
date?: string;
meals: MealIn[];
}
export interface MealIn {
slug?: string;
name?: string;
description?: string;
}
export interface MealDayOut {
date?: string;
meals: MealIn[];
id: number;
}
export interface MealPlanIn {
group: string;
startDate: string;
endDate: string;
planDays: MealDayIn[];
}
export interface MealPlanOut {
group: string;
startDate: string;
endDate: string;
planDays: MealDayIn[];
id: number;
shoppingList?: number;
}
export interface PlanRulesCreate {
day?: PlanRulesDay & string;
entryType?: PlanRulesType & string;
@ -76,6 +48,7 @@ export interface PlanRulesOut {
categories?: Category[];
tags?: Tag[];
groupId: string;
householdId: string;
id: string;
}
export interface PlanRulesSave {
@ -84,51 +57,56 @@ export interface PlanRulesSave {
categories?: Category[];
tags?: Tag[];
groupId: string;
householdId: string;
}
export interface ReadPlanEntry {
date: string;
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: string;
recipeId?: string | null;
id: number;
groupId: string;
userId?: string;
recipe?: RecipeSummary;
userId?: string | null;
householdId: string;
recipe?: RecipeSummary | null;
}
export interface RecipeSummary {
id?: string;
id?: string | null;
userId?: string;
householdId?: string;
groupId?: string;
name?: string;
name?: string | null;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeCategory[];
tags?: RecipeTag[];
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
cookTime?: string | null;
performTime?: string | null;
description?: string | null;
recipeCategory?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
dateAdded?: string;
dateUpdated?: string;
createdAt?: string;
updateAt?: string;
lastMade?: string;
rating?: number | null;
orgURL?: string | null;
dateAdded?: string | null;
dateUpdated?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
lastMade?: string | null;
}
export interface RecipeCategory {
id?: string;
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTag {
id?: string;
id?: string | null;
name: string;
slug: string;
[k: string]: unknown;
}
export interface RecipeTool {
id: string;
@ -141,18 +119,18 @@ export interface SavePlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: string;
recipeId?: string | null;
groupId: string;
userId?: string;
userId?: string | null;
}
export interface ShoppingListIn {
name: string;
group?: string;
group?: string | null;
items: ListItem[];
}
export interface ShoppingListOut {
name: string;
group?: string;
group?: string | null;
items: ListItem[];
id: number;
}
@ -161,8 +139,8 @@ export interface UpdatePlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: string;
recipeId?: string | null;
id: number;
groupId: string;
userId?: string;
userId?: string | null;
}

View File

@ -0,0 +1,65 @@
/* tslint:disable */
/* eslint-disable */
/**
/* This file was automatically generated from pydantic models by running pydantic2ts.
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export interface OpenAIIngredient {
/**
*
* The input is simply the ingredient string you are processing as-is. It is forbidden to
* modify this at all, you must provide the input exactly as you received it.
*
*/
input: string;
/**
*
* This value is a float between 0 - 100, where 100 is full confidence that the result is correct,
* and 0 is no confidence that the result is correct. If you're unable to parse anything,
* and you put the entire string in the notes, you should return 0 confidence. If you can easily
* parse the string into each component, then you should return a confidence of 100. If you have to
* guess which part is the unit and which part is the food, your confidence should be lower, such as 60.
* Even if there is no unit or note, if you're able to determine the food, you may use a higher confidence.
* If the entire ingredient consists of only a food, you can use a confidence of 100.
*
*/
confidence?: number | null;
/**
*
* The numerical representation of how much of this ingredient. For instance, if you receive
* "3 1/2 grams of minced garlic", the quantity is "3 1/2". Quantity may be represented as a whole number
* (integer), a float or decimal, or a fraction. You should output quantity in only whole numbers or
* floats, converting fractions into floats. Floats longer than 10 decimal places should be
* rounded to 10 decimal places.
*
*/
quantity?: number | null;
/**
*
* The unit of measurement for this ingredient. For instance, if you receive
* "2 lbs chicken breast", the unit is "lbs" (short for "pounds").
*
*/
unit?: string | null;
/**
*
* The actual physical ingredient used in the recipe. For instance, if you receive
* "3 cups of onions, chopped", the food is "onions".
*
*/
food?: string | null;
/**
*
* The rest of the text that represents more detail on how to prepare the ingredient.
* Anything that is not one of the above should be the note. For instance, if you receive
* "one can of butter beans, drained" the note would be "drained". If you receive
* "3 cloves of garlic peeled and finely chopped", the note would be "peeled and finely chopped".
*
*/
note?: string | null;
}
export interface OpenAIIngredients {
ingredients?: OpenAIIngredient[];
}
export interface OpenAIBase {}

View File

@ -55,29 +55,32 @@ export interface CategorySave {
groupId: string;
}
export interface CreateIngredientFood {
id?: string | null;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
labelId?: string;
aliases?: CreateIngredientFoodAlias[];
} | null;
onHand?: boolean;
labelId?: string | null;
aliases?: CreateIngredientFoodAlias[];
}
export interface CreateIngredientFoodAlias {
name: string;
}
export interface CreateIngredientUnit {
id?: string | null;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
} | null;
onHand?: boolean;
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string;
pluralAbbreviation?: string | null;
useAbbreviation?: boolean;
aliases?: CreateIngredientUnitAlias[];
}
@ -89,16 +92,16 @@ export interface CreateRecipe {
}
export interface CreateRecipeBulk {
url: string;
categories?: RecipeCategory[];
tags?: RecipeTag[];
categories?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
}
export interface RecipeCategory {
id?: string;
id?: string | null;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
id?: string | null;
name: string;
slug: string;
}
@ -116,27 +119,27 @@ export interface ExportRecipes {
exportType?: ExportTypes & string;
}
export interface IngredientConfidence {
average?: number;
comment?: number;
name?: number;
unit?: number;
quantity?: number;
food?: number;
average?: number | null;
comment?: number | null;
name?: number | null;
unit?: number | null;
quantity?: number | null;
food?: number | null;
}
export interface IngredientFood {
id: string;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
labelId?: string;
aliases?: IngredientFoodAlias[];
id: string;
label?: MultiPurposeLabelSummary;
createdAt?: string;
updateAt?: string;
} | null;
onHand?: boolean;
labelId?: string | null;
aliases?: IngredientFoodAlias[];
label?: MultiPurposeLabelSummary | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface IngredientFoodAlias {
name: string;
@ -151,27 +154,28 @@ export interface MultiPurposeLabelSummary {
* A list of ingredient references.
*/
export interface IngredientReferences {
referenceId?: string;
referenceId?: string | null;
}
export interface IngredientRequest {
parser?: RegisteredParser & string;
ingredient: string;
}
export interface IngredientUnit {
id: string;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
} | null;
onHand?: boolean;
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string;
pluralAbbreviation?: string | null;
useAbbreviation?: boolean;
aliases?: IngredientUnitAlias[];
id: string;
createdAt?: string;
updateAt?: string;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface IngredientUnitAlias {
name: string;
@ -189,64 +193,65 @@ export interface MergeUnit {
toUnit: string;
}
export interface Nutrition {
calories?: string;
fatContent?: string;
proteinContent?: string;
carbohydrateContent?: string;
fiberContent?: string;
sodiumContent?: string;
sugarContent?: string;
calories?: string | null;
fatContent?: string | null;
proteinContent?: string | null;
carbohydrateContent?: string | null;
fiberContent?: string | null;
sodiumContent?: string | null;
sugarContent?: string | null;
}
export interface ParsedIngredient {
input?: string;
input?: string | null;
confidence?: IngredientConfidence;
ingredient: RecipeIngredient;
}
export interface RecipeIngredient {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
quantity?: number | null;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean;
display?: string;
title?: string;
originalText?: string;
title?: string | null;
originalText?: string | null;
referenceId?: string;
}
export interface Recipe {
id?: string;
id?: string | null;
userId?: string;
householdId?: string;
groupId?: string;
name?: string;
name?: string | null;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeCategory[];
tags?: RecipeTag[];
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
cookTime?: string | null;
performTime?: string | null;
description?: string | null;
recipeCategory?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
dateAdded?: string;
dateUpdated?: string;
createdAt?: string;
updateAt?: string;
lastMade?: string;
rating?: number | null;
orgURL?: string | null;
dateAdded?: string | null;
dateUpdated?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
lastMade?: string | null;
recipeIngredient?: RecipeIngredient[];
recipeInstructions?: RecipeStep[];
nutrition?: Nutrition;
settings?: RecipeSettings;
assets?: RecipeAsset[];
notes?: RecipeNote[];
recipeInstructions?: RecipeStep[] | null;
nutrition?: Nutrition | null;
settings?: RecipeSettings | null;
assets?: RecipeAsset[] | null;
notes?: RecipeNote[] | null;
extras?: {
[k: string]: unknown;
};
comments?: RecipeCommentOut[];
} | null;
comments?: RecipeCommentOut[] | null;
}
export interface RecipeTool {
id: string;
@ -255,15 +260,15 @@ export interface RecipeTool {
onHand?: boolean;
}
export interface RecipeStep {
id?: string;
title?: string;
id?: string | null;
title?: string | null;
text: string;
ingredientReferences?: IngredientReferences[];
}
export interface RecipeAsset {
name: string;
icon: string;
fileName?: string;
fileName?: string | null;
}
export interface RecipeNote {
title: string;
@ -274,13 +279,13 @@ export interface RecipeCommentOut {
text: string;
id: string;
createdAt: string;
updateAt: string;
updatedAt: string;
userId: string;
user: UserBase;
}
export interface UserBase {
id: string;
username?: string;
username?: string | null;
admin: boolean;
}
export interface RecipeCategoryResponse {
@ -290,28 +295,29 @@ export interface RecipeCategoryResponse {
recipes?: RecipeSummary[];
}
export interface RecipeSummary {
id?: string;
id?: string | null;
userId?: string;
householdId?: string;
groupId?: string;
name?: string;
name?: string | null;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeCategory[];
tags?: RecipeTag[];
recipeYield?: string | null;
totalTime?: string | null;
prepTime?: string | null;
cookTime?: string | null;
performTime?: string | null;
description?: string | null;
recipeCategory?: RecipeCategory[] | null;
tags?: RecipeTag[] | null;
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
dateAdded?: string;
dateUpdated?: string;
createdAt?: string;
updateAt?: string;
lastMade?: string;
rating?: number | null;
orgURL?: string | null;
dateAdded?: string | null;
dateUpdated?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
lastMade?: string | null;
}
export interface RecipeCommentCreate {
recipeId: string;
@ -327,15 +333,15 @@ export interface RecipeCommentUpdate {
text: string;
}
export interface RecipeDuplicate {
name?: string;
name?: string | null;
}
export interface RecipeIngredientBase {
quantity?: number;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
note?: string;
isFood?: boolean;
disableAmount?: boolean;
quantity?: number | null;
unit?: IngredientUnit | CreateIngredientUnit | null;
food?: IngredientFood | CreateIngredientFood | null;
note?: string | null;
isFood?: boolean | null;
disableAmount?: boolean | null;
display?: string;
}
export interface RecipeLastMade {
@ -379,17 +385,17 @@ export interface RecipeTimelineEventCreate {
userId: string;
subject: string;
eventType: TimelineEventType;
eventMessage?: string;
image?: TimelineEventImage & string;
eventMessage?: string | null;
image?: TimelineEventImage | null;
timestamp?: string;
}
export interface RecipeTimelineEventIn {
recipeId: string;
userId?: string;
userId?: string | null;
subject: string;
eventType: TimelineEventType;
eventMessage?: string;
image?: TimelineEventImage & string;
eventMessage?: string | null;
image?: TimelineEventImage | null;
timestamp?: string;
}
export interface RecipeTimelineEventOut {
@ -397,17 +403,19 @@ export interface RecipeTimelineEventOut {
userId: string;
subject: string;
eventType: TimelineEventType;
eventMessage?: string;
image?: TimelineEventImage & string;
eventMessage?: string | null;
image?: TimelineEventImage | null;
timestamp?: string;
id: string;
groupId: string;
householdId: string;
createdAt: string;
updateAt: string;
updatedAt: string;
}
export interface RecipeTimelineEventUpdate {
subject: string;
eventMessage?: string;
image?: TimelineEventImage;
eventMessage?: string | null;
image?: TimelineEventImage | null;
}
export interface RecipeToolCreate {
name: string;
@ -435,26 +443,30 @@ export interface RecipeZipTokenResponse {
token: string;
}
export interface SaveIngredientFood {
id?: string | null;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
labelId?: string;
} | null;
onHand?: boolean;
labelId?: string | null;
aliases?: CreateIngredientFoodAlias[];
groupId: string;
}
export interface SaveIngredientUnit {
id?: string | null;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
} | null;
onHand?: boolean;
fraction?: boolean;
abbreviation?: string;
pluralAbbreviation?: string;
pluralAbbreviation?: string | null;
useAbbreviation?: boolean;
aliases?: CreateIngredientUnitAlias[];
groupId: string;
@ -465,8 +477,9 @@ export interface ScrapeRecipe {
}
export interface ScrapeRecipeTest {
url: string;
useOpenAI?: boolean;
}
export interface SlugResponse { }
export interface SlugResponse {}
export interface TagIn {
name: string;
}
@ -481,12 +494,14 @@ export interface TagSave {
groupId: string;
}
export interface UnitFoodBase {
id?: string | null;
name: string;
pluralName?: string;
pluralName?: string | null;
description?: string;
extras?: {
[k: string]: unknown;
};
} | null;
onHand?: boolean;
}
export interface UpdateImageResponse {
image: string;

View File

@ -11,7 +11,7 @@ export type OrderDirection = "asc" | "desc";
export interface ErrorResponse {
message: string;
error?: boolean;
exception?: string;
exception?: string | null;
}
export interface FileTokenResponse {
fileToken: string;
@ -19,19 +19,19 @@ export interface FileTokenResponse {
export interface PaginationQuery {
page?: number;
perPage?: number;
orderBy?: string;
orderByNullPosition?: OrderByNullPosition;
orderBy?: string | null;
orderByNullPosition?: OrderByNullPosition | null;
orderDirection?: OrderDirection & string;
queryFilter?: string;
paginationSeed?: string;
queryFilter?: string | null;
paginationSeed?: string | null;
}
export interface RecipeSearchQuery {
cookbook?: string;
cookbook?: string | null;
requireAllCategories?: boolean;
requireAllTags?: boolean;
requireAllTools?: boolean;
requireAllFoods?: boolean;
search?: string;
search?: string | null;
}
export interface SuccessResponse {
message: string;

View File

@ -19,8 +19,9 @@ export interface CreateToken {
token: string;
}
export interface CreateUserRegistration {
group?: string;
groupToken?: string;
group?: string | null;
household?: string | null;
groupToken?: string | null;
email: string;
username: string;
fullName: string;
@ -45,21 +46,19 @@ export interface ForgotPassword {
export interface GroupBase {
name: string;
}
export interface GroupHouseholdSummary {
id: string;
name: string;
}
export interface GroupInDB {
name: string;
id: string;
slug: string;
categories?: CategoryBase[];
categories?: CategoryBase[] | null;
webhooks?: ReadWebhook[];
users?: UserOut[];
preferences?: ReadGroupPreferences;
}
export interface GroupSummary {
name: string;
id: string;
slug: string;
preferences?: ReadGroupPreferences;
households?: GroupHouseholdSummary[] | null;
users?: UserSummary[] | null;
preferences?: ReadGroupPreferences | null;
}
export interface CategoryBase {
name: string;
@ -73,43 +72,24 @@ export interface ReadWebhook {
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
householdId: string;
id: string;
}
export interface UserOut {
export interface UserSummary {
id: string;
username?: string;
fullName?: string;
email: string;
authMethod?: AuthMethod & string;
admin?: boolean;
group: string;
advanced?: boolean;
canInvite?: boolean;
canManage?: boolean;
canOrganize?: boolean;
groupId: string;
groupSlug: string;
tokens?: LongLiveTokenOut[];
cacheKey: string;
}
export interface LongLiveTokenOut {
token: string;
name: string;
id: number;
createdAt?: string;
fullName: string;
}
export interface ReadGroupPreferences {
privateGroup?: boolean;
firstDayOfWeek?: number;
recipePublic?: boolean;
recipeShowNutrition?: boolean;
recipeShowAssets?: boolean;
recipeLandscapeView?: boolean;
recipeDisableComments?: boolean;
recipeDisableAmount?: boolean;
groupId: string;
id: string;
}
export interface GroupSummary {
name: string;
id: string;
slug: string;
preferences?: ReadGroupPreferences | null;
}
export interface LongLiveTokenIn {
name: string;
integrationId?: string;
@ -124,23 +104,32 @@ export interface LongLiveTokenInDB {
}
export interface PrivateUser {
id: string;
username?: string;
fullName?: string;
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
admin?: boolean;
group: string;
household: string;
advanced?: boolean;
canInvite?: boolean;
canManage?: boolean;
canOrganize?: boolean;
groupId: string;
groupSlug: string;
tokens?: LongLiveTokenOut[];
householdId: string;
householdSlug: string;
tokens?: LongLiveTokenOut[] | null;
cacheKey: string;
password: string;
loginAttemps?: number;
lockedAt?: string;
lockedAt?: string | null;
}
export interface LongLiveTokenOut {
token: string;
name: string;
id: number;
createdAt?: string | null;
}
export interface OIDCRequest {
id_token: string;
@ -168,8 +157,8 @@ export interface Token {
token_type: string;
}
export interface TokenData {
user_id?: string;
username?: string;
user_id?: string | null;
username?: string | null;
}
export interface UnlockResults {
unlocked?: number;
@ -178,7 +167,7 @@ export interface UpdateGroup {
name: string;
id: string;
slug: string;
categories?: CategoryBase[];
categories?: CategoryBase[] | null;
webhooks?: CreateWebhook[];
}
export interface CreateWebhook {
@ -189,53 +178,75 @@ export interface CreateWebhook {
scheduledTime: string;
}
export interface UserBase {
id?: string;
username?: string;
fullName?: string;
id?: string | null;
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
admin?: boolean;
group?: string;
group?: string | null;
household?: string | null;
advanced?: boolean;
canInvite?: boolean;
canManage?: boolean;
canOrganize?: boolean;
}
export interface UserIn {
id?: string;
username?: string;
fullName?: string;
id?: string | null;
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
admin?: boolean;
group?: string;
group?: string | null;
household?: string | null;
advanced?: boolean;
canInvite?: boolean;
canManage?: boolean;
canOrganize?: boolean;
password: string;
}
export interface UserOut {
id: string;
username?: string | null;
fullName?: string | null;
email: string;
authMethod?: AuthMethod & string;
admin?: boolean;
group: string;
household: string;
advanced?: boolean;
canInvite?: boolean;
canManage?: boolean;
canOrganize?: boolean;
groupId: string;
groupSlug: string;
householdId: string;
householdSlug: string;
tokens?: LongLiveTokenOut[] | null;
cacheKey: string;
}
export interface UserRatingCreate {
recipeId: string;
rating?: number;
rating?: number | null;
isFavorite?: boolean;
userId: string;
}
export interface UserRatingOut {
recipeId: string;
rating?: number;
rating?: number | null;
isFavorite?: boolean;
userId: string;
id: string;
}
export interface UserRatingSummary {
recipeId: string;
rating?: number;
rating?: number | null;
isFavorite?: boolean;
}
export interface UserSummary {
id: string;
fullName: string;
export interface UserRatingUpdate {
rating?: number | null;
isFavorite?: boolean | null;
}
export interface ValidateResetToken {
token: string;

View File

@ -1,5 +1,5 @@
import { BaseAPI } from "../base/base-clients";
import { EmailInitationResponse, EmailInvitation } from "~/lib/api/types/group";
import { EmailInitationResponse, EmailInvitation } from "~/lib/api/types/household";
import { ForgotPassword } from "~/lib/api/types/user";
import { EmailTest } from "~/lib/api/types/admin";
@ -7,7 +7,7 @@ const routes = {
base: "/api/admin/email",
forgotPassword: "/api/users/forgot-password",
invitation: "/api/groups/invitations/email",
invitation: "/api/households/invitations/email",
};
export class EmailAPI extends BaseAPI {

View File

@ -4,8 +4,8 @@ import { CreateCookBook, RecipeCookBook, UpdateCookBook } from "~/lib/api/types/
const prefix = "/api";
const routes = {
cookbooks: `${prefix}/groups/cookbooks`,
cookbooksId: (id: number) => `${prefix}/groups/cookbooks/${id}`,
cookbooks: `${prefix}/households/cookbooks`,
cookbooksId: (id: number) => `${prefix}/households/cookbooks/${id}`,
};
export class CookbookAPI extends BaseCRUDAPI<CreateCookBook, RecipeCookBook, UpdateCookBook> {

View File

@ -1,11 +1,11 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { GroupEventNotifierCreate, GroupEventNotifierOut, GroupEventNotifierUpdate } from "~/lib/api/types/group";
import { GroupEventNotifierCreate, GroupEventNotifierOut, GroupEventNotifierUpdate } from "~/lib/api/types/household";
const prefix = "/api";
const routes = {
eventNotifier: `${prefix}/groups/events/notifications`,
eventNotifierId: (id: string | number) => `${prefix}/groups/events/notifications/${id}`,
eventNotifier: `${prefix}/households/events/notifications`,
eventNotifierId: (id: string | number) => `${prefix}/households/events/notifications/${id}`,
};
export class GroupEventNotifierApi extends BaseCRUDAPI<

View File

@ -4,8 +4,8 @@ import { PlanRulesCreate, PlanRulesOut } from "~/lib/api/types/meal-plan";
const prefix = "/api";
const routes = {
rule: `${prefix}/groups/mealplans/rules`,
ruleId: (id: string | number) => `${prefix}/groups/mealplans/rules/${id}`,
rule: `${prefix}/households/mealplans/rules`,
ruleId: (id: string | number) => `${prefix}/households/mealplans/rules/${id}`,
};
export class MealPlanRulesApi extends BaseCRUDAPI<PlanRulesCreate, PlanRulesOut> {

View File

@ -4,9 +4,9 @@ import { CreatePlanEntry, CreateRandomEntry, ReadPlanEntry, UpdatePlanEntry } fr
const prefix = "/api";
const routes = {
mealplan: `${prefix}/groups/mealplans`,
random: `${prefix}/groups/mealplans/random`,
mealplanId: (id: string | number) => `${prefix}/groups/mealplans/${id}`,
mealplan: `${prefix}/households/mealplans`,
random: `${prefix}/households/mealplans/random`,
mealplanId: (id: string | number) => `${prefix}/households/mealplans/${id}`,
};
export class MealPlanAPI extends BaseCRUDAPI<CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry> {

View File

@ -1,11 +1,11 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { CreateGroupRecipeAction, GroupRecipeActionOut } from "~/lib/api/types/group";
import { CreateGroupRecipeAction, GroupRecipeActionOut } from "~/lib/api/types/household";
const prefix = "/api";
const routes = {
groupRecipeActions: `${prefix}/groups/recipe-actions`,
groupRecipeActionsId: (id: string | number) => `${prefix}/groups/recipe-actions/${id}`,
groupRecipeActions: `${prefix}/households/recipe-actions`,
groupRecipeActionsId: (id: string | number) => `${prefix}/households/recipe-actions/${id}`,
};
export class GroupRecipeActionsAPI extends BaseCRUDAPI<CreateGroupRecipeAction, GroupRecipeActionOut> {

View File

@ -9,20 +9,20 @@ import {
ShoppingListMultiPurposeLabelUpdate,
ShoppingListOut,
ShoppingListUpdate,
} from "~/lib/api/types/group";
} from "~/lib/api/types/household";
const prefix = "/api";
const routes = {
shoppingLists: `${prefix}/groups/shopping/lists`,
shoppingListsId: (id: string) => `${prefix}/groups/shopping/lists/${id}`,
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
shoppingListIdRemoveRecipe: (id: string, recipeId: string) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}/delete`,
shoppingListIdUpdateLabelSettings: (id: string) => `${prefix}/groups/shopping/lists/${id}/label-settings`,
shoppingLists: `${prefix}/households/shopping/lists`,
shoppingListsId: (id: string) => `${prefix}/households/shopping/lists/${id}`,
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}`,
shoppingListIdRemoveRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}/delete`,
shoppingListIdUpdateLabelSettings: (id: string) => `${prefix}/households/shopping/lists/${id}/label-settings`,
shoppingListItems: `${prefix}/groups/shopping/items`,
shoppingListItemsCreateBulk: `${prefix}/groups/shopping/items/create-bulk`,
shoppingListItemsId: (id: string) => `${prefix}/groups/shopping/items/${id}`,
shoppingListItems: `${prefix}/households/shopping/items`,
shoppingListItemsCreateBulk: `${prefix}/households/shopping/items/create-bulk`,
shoppingListItemsId: (id: string) => `${prefix}/households/shopping/items/${id}`,
};
export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListCreate, ShoppingListOut, ShoppingListUpdate> {

View File

@ -1,12 +1,12 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { CreateWebhook, ReadWebhook } from "~/lib/api/types/group";
import { CreateWebhook, ReadWebhook } from "~/lib/api/types/household";
const prefix = "/api";
const routes = {
webhooks: `${prefix}/groups/webhooks`,
webhooksId: (id: string | number) => `${prefix}/groups/webhooks/${id}`,
webhooksIdTest: (id: string | number) => `${prefix}/groups/webhooks/${id}/test`,
webhooks: `${prefix}/households/webhooks`,
webhooksId: (id: string | number) => `${prefix}/households/webhooks/${id}`,
webhooksIdTest: (id: string | number) => `${prefix}/households/webhooks/${id}/test`,
};
export class WebhooksAPI extends BaseCRUDAPI<CreateWebhook, ReadWebhook> {

View File

@ -1,13 +1,10 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { CategoryBase, GroupBase, GroupInDB, GroupSummary, UserOut } from "~/lib/api/types/user";
import { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
import { HouseholdSummary } from "~/lib/api/types/household";
import {
CreateInviteToken,
GroupAdminUpdate,
GroupStatistics,
GroupStorage,
ReadGroupPreferences,
ReadInviteToken,
SetPermissions,
UpdateGroupPreferences,
} from "~/lib/api/types/group";
@ -16,16 +13,14 @@ const prefix = "/api";
const routes = {
groups: `${prefix}/admin/groups`,
groupsSelf: `${prefix}/groups/self`,
categories: `${prefix}/groups/categories`,
members: `${prefix}/groups/members`,
permissions: `${prefix}/groups/permissions`,
preferences: `${prefix}/groups/preferences`,
statistics: `${prefix}/groups/statistics`,
storage: `${prefix}/groups/storage`,
invitation: `${prefix}/groups/invitations`,
households: `${prefix}/households`,
membersHouseholdId: (householdId: string | number | null) => {
return householdId ?
`${prefix}/households/members?householdId=${householdId}` :
`${prefix}/groups/members`;
},
groupsId: (id: string | number) => `${prefix}/admin/groups/${id}`,
};
@ -38,14 +33,6 @@ export class GroupAPI extends BaseCRUDAPI<GroupBase, GroupInDB, GroupAdminUpdate
return await this.requests.get<GroupSummary>(routes.groupsSelf);
}
async getCategories() {
return await this.requests.get<CategoryBase[]>(routes.categories);
}
async setCategories(payload: CategoryBase[]) {
return await this.requests.put<CategoryBase[]>(routes.categories, payload);
}
async getPreferences() {
return await this.requests.get<ReadGroupPreferences>(routes.preferences);
}
@ -55,21 +42,12 @@ export class GroupAPI extends BaseCRUDAPI<GroupBase, GroupInDB, GroupAdminUpdate
return await this.requests.put<ReadGroupPreferences, UpdateGroupPreferences>(routes.preferences, payload);
}
async createInvitation(payload: CreateInviteToken) {
return await this.requests.post<ReadInviteToken>(routes.invitation, payload);
async fetchMembers(householdId: string | number | null = null) {
return await this.requests.get<UserSummary[]>(routes.membersHouseholdId(householdId));
}
async fetchMembers() {
return await this.requests.get<UserOut[]>(routes.members);
}
async setMemberPermissions(payload: SetPermissions) {
// TODO: This should probably be a patch request, which isn't offered by the API currently
return await this.requests.put<UserOut, SetPermissions>(routes.permissions, payload);
}
async statistics() {
return await this.requests.get<GroupStatistics>(routes.statistics);
async fetchHouseholds() {
return await this.requests.get<HouseholdSummary[]>(routes.households);
}
async storage() {

View File

@ -0,0 +1,64 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { UserOut } from "~/lib/api/types/user";
import {
HouseholdCreate,
HouseholdInDB,
UpdateHouseholdAdmin,
HouseholdStatistics,
ReadHouseholdPreferences,
SetPermissions,
UpdateHouseholdPreferences,
CreateInviteToken,
ReadInviteToken,
} from "~/lib/api/types/household";
const prefix = "/api";
const routes = {
households: `${prefix}/admin/households`,
householdsSelf: `${prefix}/households/self`,
members: `${prefix}/households/members`,
permissions: `${prefix}/households/permissions`,
preferences: `${prefix}/households/preferences`,
statistics: `${prefix}/households/statistics`,
invitation: `${prefix}/households/invitations`,
householdsId: (id: string | number) => `${prefix}/admin/households/${id}`,
};
export class HouseholdAPI extends BaseCRUDAPI<HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin> {
baseRoute = routes.households;
itemRoute = routes.householdsId;
/** Returns the Group Data for the Current User
*/
async getCurrentUserHousehold() {
return await this.requests.get<HouseholdInDB>(routes.householdsSelf);
}
async getPreferences() {
return await this.requests.get<ReadHouseholdPreferences>(routes.preferences);
}
async setPreferences(payload: UpdateHouseholdPreferences) {
// TODO: This should probably be a patch request, which isn't offered by the API currently
return await this.requests.put<ReadHouseholdPreferences, UpdateHouseholdPreferences>(routes.preferences, payload);
}
async createInvitation(payload: CreateInviteToken) {
return await this.requests.post<ReadInviteToken>(routes.invitation, payload);
}
async fetchMembers() {
return await this.requests.get<UserOut[]>(routes.members);
}
async setMemberPermissions(payload: SetPermissions) {
// TODO: This should probably be a patch request, which isn't offered by the API currently
return await this.requests.put<UserOut, SetPermissions>(routes.permissions, payload);
}
async statistics() {
return await this.requests.get<HouseholdStatistics>(routes.statistics);
}
}

View File

@ -1,6 +1,4 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { QueryValue, route } from "~/lib/api/base/route";
import { PaginationData } from "~/lib/api/types/non-generated";
import {
ChangePassword,
DeleteTokenResponse,
@ -12,7 +10,6 @@ import {
UserOut,
UserRatingOut,
UserRatingSummary,
UserSummary,
} from "~/lib/api/types/user";
export interface UserRatingsSummaries {
@ -26,7 +23,6 @@ export interface UserRatingsOut {
const prefix = "/api";
const routes = {
groupUsers: `${prefix}/users/group-users`,
usersSelf: `${prefix}/users/self`,
ratingsSelf: `${prefix}/users/self/ratings`,
passwordReset: `${prefix}/users/reset-password`,
@ -51,10 +47,6 @@ export class UserApi extends BaseCRUDAPI<UserIn, UserOut, UserBase> {
baseRoute: string = routes.users;
itemRoute = (itemid: string) => routes.usersId(itemid);
async getGroupUsers(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
return await this.requests.get<PaginationData<UserSummary>>(route(routes.groupUsers, { page, perPage, ...params }));
}
async addFavorite(id: string, slug: string) {
return await this.requests.post(routes.usersIdFavoritesSlug(id, slug), {});
}

View File

@ -17,6 +17,7 @@ import {
mdiAccountGroup,
mdiSlotMachine,
mdiHome,
mdiHomeAccount,
mdiMagnify,
mdiPotSteamOutline,
mdiTranslate,
@ -226,6 +227,7 @@ export const icons = {
heart: mdiHeart,
heartOutline: mdiHeartOutline,
home: mdiHome,
household: mdiHomeAccount,
import: mdiImport,
information: mdiInformation,
informationVariant: mdiInformationVariant,

View File

@ -477,7 +477,7 @@ export default {
"name": "Meal Planner",
"short_name": "Meal Planner",
"description": "Open the meal planner",
"url": "/group/mealplan/planner/view",
"url": "/household/mealplan/planner/view",
"icons": [
{
"src": "/icons/mdiCalendarMultiselect-192x192.png",

View File

@ -60,7 +60,7 @@
<i18n path="settings.backup.experimental-description" />
</v-card-text>
</BaseCardSectionTitle>
<v-toolbar color="background" flat class="justify-between">
<v-toolbar color="transparent" flat class="justify-between">
<BaseButton class="mr-2" @click="createBackup"> {{ $t("settings.backup.create-heading") }} </BaseButton>
<AppButtonUpload
:text-btn="false"

View File

@ -1,15 +1,14 @@
// TODO: Edit Group
<template>
<v-container fluid>
<BaseDialog
v-model="createDialog"
:title="$t('group.create-group')"
:icon="$globals.icons.group"
@submit="createGroup(createUserForm.data)"
@submit="createGroup(createGroupForm.data)"
>
<template #activator> </template>
<v-card-text>
<AutoForm v-model="createUserForm.data" :update-mode="updateMode" :items="createUserForm.items" />
<AutoForm v-model="createGroupForm.data" :update-mode="updateMode" :items="createGroupForm.items" />
</v-card-text>
</BaseDialog>
@ -27,7 +26,7 @@
<BaseCardSectionTitle :title="$tc('group.group-management')"> </BaseCardSectionTitle>
<section>
<v-toolbar flat color="background" class="justify-between">
<v-toolbar flat color="transparent" class="justify-between">
<BaseButton @click="openDialog"> {{ $t("general.create") }} </BaseButton>
</v-toolbar>
@ -41,15 +40,15 @@
:search="search"
@click:row="handleRowClick"
>
<template #item.households="{ item }">
{{ item.households.length }}
</template>
<template #item.users="{ item }">
{{ item.users.length }}
</template>
<template #item.webhookEnable="{ item }">
{{ item.webhooks.length > 0 ? $t("general.yes") : $t("general.no") }}
</template>
<template #item.actions="{ item }">
<v-btn
:disabled="item && item.users.length > 0"
:disabled="item && (item.households.length > 0 || item.users.length > 0)"
class="mr-1"
icon
color="error"
@ -94,12 +93,12 @@ export default defineComponent({
value: "id",
},
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("group.total-households"), value: "households" },
{ text: i18n.t("user.total-users"), value: "users" },
{ text: i18n.t("user.webhooks-enabled"), value: "webhookEnable" },
{ text: i18n.t("general.delete"), value: "actions" },
],
updateMode: false,
createUserForm: {
createGroupForm: {
items: [
{
label: i18n.t("group.group-name"),
@ -116,7 +115,7 @@ export default defineComponent({
function openDialog() {
state.createDialog = true;
state.createUserForm.data.name = "";
state.createGroupForm.data.name = "";
}
const router = useRouter();

View File

@ -0,0 +1,117 @@
<template>
<v-container v-if="household" class="narrow-container">
<BasePageTitle>
<template #header>
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
</template>
<template #title> {{ $t('household.admin-household-management') }} </template>
{{ $t('household.admin-household-management-text') }}
</BasePageTitle>
<AppToolbar back> </AppToolbar>
<v-card-text> {{ $t('household.household-id-value', [household.id]) }} </v-card-text>
<v-form v-if="!userError" ref="refHouseholdEditForm" @submit.prevent="handleSubmit">
<v-card outlined>
<v-card-text>
<v-select
v-if="groups"
v-model="household.groupId"
disabled
:items="groups"
rounded
class="rounded-lg"
item-text="name"
item-value="id"
:return-object="false"
filled
:label="$tc('group.user-group')"
:rules="[validators.required]"
/>
<v-text-field
v-model="household.name"
:label="$t('household.household-name')"
:rules="[validators.required]"
/>
<HouseholdPreferencesEditor v-if="household.preferences" v-model="household.preferences" />
</v-card-text>
</v-card>
<div class="d-flex pa-2">
<BaseButton type="submit" edit class="ml-auto"> {{ $t("general.update") }}</BaseButton>
</div>
</v-form>
</v-container>
</template>
<script lang="ts">
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 { alert } from "~/composables/use-toast";
import { validators } from "~/composables/use-validators";
import { HouseholdInDB } from "~/lib/api/types/household";
import { VForm } from "~/types/vuetify";
export default defineComponent({
components: {
HouseholdPreferencesEditor,
},
layout: "admin",
setup() {
const route = useRoute();
const { i18n } = useContext();
const { groups } = useGroups();
const householdId = route.value.params.id;
// ==============================================
// New User Form
const refHouseholdEditForm = ref<VForm | null>(null);
const userApi = useUserApi();
const household = ref<HouseholdInDB | null>(null);
const userError = ref(false);
onMounted(async () => {
const { data, error } = await userApi.households.getOne(householdId);
if (error?.response?.status === 404) {
alert.error(i18n.tc("user.user-not-found"));
userError.value = true;
}
if (data) {
household.value = data;
}
});
async function handleSubmit() {
if (!refHouseholdEditForm.value?.validate() || household.value === null) {
return;
}
const { response, data } = await userApi.households.updateOne(household.value.id, household.value);
if (response?.status === 200 && data) {
if (household.value.slug !== data.slug) {
// the slug updated, which invalidates the nav URLs
window.location.reload();
}
household.value = data;
} else {
alert.error(i18n.tc("settings.settings-update-failed"));
}
}
return {
groups,
household,
validators,
userError,
refHouseholdEditForm,
handleSubmit,
};
},
});
</script>

View File

@ -0,0 +1,167 @@
<template>
<v-container fluid>
<BaseDialog
v-model="createDialog"
:title="$t('household.create-household')"
:icon="$globals.icons.household"
@submit="createHousehold(createHouseholdForm.data)"
>
<template #activator> </template>
<v-card-text>
<v-select
v-if="groups"
v-model="createHouseholdForm.data.groupId"
:items="groups"
rounded
class="rounded-lg"
item-text="name"
item-value="id"
:return-object="false"
filled
:label="$tc('household.household-group')"
:rules="[validators.required]"
/>
<AutoForm v-model="createHouseholdForm.data" :update-mode="updateMode" :items="createHouseholdForm.items" />
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="confirmDialog"
:title="$t('general.confirm')"
color="error"
@confirm="deleteHousehold(deleteTarget)"
>
<template #activator> </template>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<BaseCardSectionTitle :title="$tc('household.household-management')"> </BaseCardSectionTitle>
<section>
<v-toolbar flat color="transparent" class="justify-between">
<BaseButton @click="openDialog"> {{ $t("general.create") }} </BaseButton>
</v-toolbar>
<v-data-table
:headers="headers"
:items="households || []"
item-key="id"
class="elevation-0"
hide-default-footer
disable-pagination
:search="search"
@click:row="handleRowClick"
>
<template #item.users="{ item }">
{{ item.users.length }}
</template>
<template #item.group="{ item }">
{{ item.group }}
</template>
<template #item.webhookEnable="{ item }">
{{ item.webhooks.length > 0 ? $t("general.yes") : $t("general.no") }}
</template>
<template #item.actions="{ item }">
<v-btn
:disabled="item && item.users.length > 0"
class="mr-1"
icon
color="error"
@click.stop="
confirmDialog = true;
deleteTarget = item.id;
"
>
<v-icon>
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
</template>
</v-data-table>
<v-divider></v-divider>
</section>
</v-container>
</template>
<script lang="ts">
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 { validators } from "~/composables/use-validators";
import { HouseholdInDB } from "~/lib/api/types/household";
export default defineComponent({
layout: "admin",
setup() {
const { i18n } = useContext();
const { groups } = useGroups();
const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useHouseholds();
const state = reactive({
createDialog: false,
confirmDialog: false,
deleteTarget: 0,
search: "",
headers: [
{
text: i18n.t("household.household"),
align: "start",
sortable: false,
value: "id",
},
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("group.group"), value: "group" },
{ text: i18n.t("user.total-users"), value: "users" },
{ text: i18n.t("user.webhooks-enabled"), value: "webhookEnable" },
{ text: i18n.t("general.delete"), value: "actions" },
],
updateMode: false,
createHouseholdForm: {
items: [
{
label: i18n.t("household.household-name"),
varName: "name",
type: fieldTypes.TEXT,
rules: ["required"],
},
],
data: {
groupId: "",
name: "",
},
},
});
function openDialog() {
state.createDialog = true;
state.createHouseholdForm.data.name = "";
state.createHouseholdForm.data.groupId = "";
}
const router = useRouter();
function handleRowClick(item: HouseholdInDB) {
router.push(`/admin/manage/households/${item.id}`);
}
return {
...toRefs(state),
groups,
households,
validators,
refreshAllHouseholds,
deleteHousehold,
createHousehold,
openDialog,
handleRowClick,
};
},
head() {
return {
title: this.$t("household.manage-households") as string,
};
},
});
</script>

View File

@ -14,9 +14,11 @@
<div class="d-flex">
<p> {{ $t("user.user-id-with-value", {id: user.id} ) }}</p>
</div>
<!-- This is disabled since we can't properly handle changing the user's group in most scenarios -->
<v-select
v-if="groups"
v-model="user.group"
disabled
:items="groups"
rounded
class="rounded-lg"
@ -26,7 +28,20 @@
filled
:label="$tc('group.user-group')"
:rules="[validators.required]"
></v-select>
/>
<v-select
v-if="households"
v-model="user.household"
:items="households"
rounded
class="rounded-lg"
item-text="name"
item-value="name"
:return-object="false"
filled
:label="$tc('household.user-household')"
:rules="[validators.required]"
/>
<div class="d-flex py-2 pr-2">
<BaseButton type="button" :loading="generatingToken" create @click.prevent="handlePasswordReset">
{{ $t("user.generate-password-reset-link") }}
@ -65,6 +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 { alert } from "~/composables/use-toast";
import { useUserForm } from "~/composables/use-users";
import { validators } from "~/composables/use-validators";
@ -76,6 +92,7 @@ export default defineComponent({
setup() {
const { userForm } = useUserForm();
const { groups } = useGroups();
const { useHouseholdsInGroup } = useHouseholds();
const { i18n } = useContext();
const route = useRoute();
@ -89,6 +106,8 @@ export default defineComponent({
const adminApi = useAdminApi();
const user = ref<UserOut | null>(null);
const households = useHouseholdsInGroup(computed(() => user.value?.groupId || ""));
const disabledFields = computed(() => {
return user.value?.authMethod !== "Mealie" ? ["admin"] : [];
})
@ -154,6 +173,7 @@ export default defineComponent({
refNewUserForm,
handleSubmit,
groups,
households,
validators,
handlePasswordReset,
resetUrl,

View File

@ -12,17 +12,30 @@
<v-card-text>
<v-select
v-if="groups"
v-model="newUserData.group"
v-model="selectedGroupId"
:items="groups"
rounded
class="rounded-lg"
item-text="name"
item-value="id"
:return-object="false"
filled
:label="$t('group.user-group')"
:rules="[validators.required]"
/>
<v-select
v-if="households"
v-model="newUserData.household"
:items="households"
rounded
class="rounded-lg"
item-text="name"
item-value="name"
:return-object="false"
filled
:label="$t('group.user-group')"
:label="$t('household.user-household')"
:rules="[validators.required]"
></v-select>
/>
<AutoForm v-model="newUserData" :items="userForm" />
</v-card-text>
</v-card>
@ -34,9 +47,10 @@
</template>
<script lang="ts">
import { defineComponent, useRouter, reactive, ref, toRefs } from "@nuxtjs/composition-api";
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 { useUserForm } from "~/composables/use-users";
import { validators } from "~/composables/use-validators";
import { VForm } from "~/types/vuetify";
@ -46,6 +60,7 @@ export default defineComponent({
setup() {
const { userForm } = useUserForm();
const { groups } = useGroups();
const { useHouseholdsInGroup } = useHouseholds();
const router = useRouter();
// ==============================================
@ -55,13 +70,20 @@ export default defineComponent({
const adminApi = useAdminApi();
const selectedGroupId = ref<string>("");
const households = useHouseholdsInGroup(selectedGroupId);
const selectedGroup = computed(() => {
return groups.value?.find((group) => group.id === selectedGroupId.value);
});
const state = reactive({
newUserData: {
username: "",
fullName: "",
email: "",
admin: false,
group: "",
group: selectedGroup.value?.name || "",
household: "",
advanced: false,
canInvite: false,
canManage: false,
@ -70,6 +92,10 @@ export default defineComponent({
authMethod: "Mealie",
},
});
watch(selectedGroup, (newGroup) => {
state.newUserData.group = newGroup?.name || "";
state.newUserData.household = "";
});
async function handleSubmit() {
if (!refNewUserForm.value?.validate()) return;
@ -87,6 +113,8 @@ export default defineComponent({
refNewUserForm,
handleSubmit,
groups,
selectedGroupId,
households,
validators,
};
},

View File

@ -18,7 +18,7 @@
<BaseCardSectionTitle :title="$tc('user.user-management')"> </BaseCardSectionTitle>
<section>
<v-toolbar color="background" flat class="justify-between">
<v-toolbar color="transparent" flat class="justify-between">
<BaseButton to="/admin/manage/users/create" class="mr-2">
{{ $t("general.create") }}
</BaseButton>
@ -129,6 +129,7 @@ export default defineComponent({
{ text: i18n.t("user.full-name"), value: "fullName" },
{ text: i18n.t("user.email"), value: "email" },
{ text: i18n.t("group.group"), value: "group" },
{ text: i18n.t("household.household"), value: "household" },
{ text: i18n.t("user.auth-method"), value: "authMethod" },
{ text: i18n.t("user.admin"), value: "admin" },
{ text: i18n.t("general.delete"), value: "actions", sortable: false, align: "center" },

View File

@ -312,7 +312,6 @@ export default defineComponent({
const preferences = {
...data.preferences,
privateGroup: !commonSettings.value.makeGroupRecipesPublic,
recipePublic: commonSettings.value.makeGroupRecipesPublic,
}
const payload = {
@ -327,6 +326,32 @@ 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);
if (!data || !data.preferences) {
alert.error(i18n.tc("events.something-went-wrong"));
return;
}
const preferences = {
...data.preferences,
privateHousehold: !commonSettings.value.makeGroupRecipesPublic,
recipePublic: commonSettings.value.makeGroupRecipesPublic,
}
const payload = {
...data,
preferences,
}
// @ts-ignore-next-line user will never be null here
const { response } = await api.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 })
if (!response || response.status !== 200) {
@ -365,6 +390,7 @@ export default defineComponent({
async function submitCommonSettings() {
const tasks = [
updateGroup(),
updateHousehold(),
seedData(),
]

View File

@ -367,6 +367,11 @@ export default defineComponent({
icon: $globals.icons.group,
value: data.defaultGroup,
},
{
name: i18n.t("about.default-household"),
icon: $globals.icons.household,
value: data.defaultHousehold,
},
{
slot: "recipe-scraper",
name: i18n.t("settings.recipe-scraper-version"),

View File

@ -24,17 +24,18 @@ export default defineComponent({
const groupName = ref<string>("");
const queryFilter = ref<string>("");
async function fetchGroup() {
const { data } = await api.groups.getCurrentUserGroup();
async function fetchHousehold() {
const { data } = await api.households.getCurrentUserHousehold();
if (data) {
queryFilter.value = `recipe.group_id="${data.id}"`;
groupName.value = data.name;
// 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}"`;
groupName.value = data.group;
}
ready.value = true;
}
fetchGroup();
fetchHousehold();
return {
groupName,
queryFilter,

View File

@ -121,7 +121,7 @@
import { defineComponent, reactive, ref, useContext } from "@nuxtjs/composition-api";
import { validators } from "~/composables/use-validators";
import { useGroupRecipeActions, useGroupRecipeActionData } from "~/composables/use-group-recipe-actions";
import { GroupRecipeActionOut } from "~/lib/api/types/group";
import { GroupRecipeActionOut } from "~/lib/api/types/household";
export default defineComponent({
setup() {

View File

@ -25,139 +25,22 @@
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
</div>
</div>
<v-select
v-model="group.preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays"
item-text="name"
item-value="value"
:label="$t('settings.first-day-of-week')"
@change="groupActions.updatePreferences()"
/>
</section>
<section v-if="group">
<BaseCardSectionTitle class="mt-10" :title="$tc('group.default-recipe-preferences')">
{{ $t("group.default-recipe-preferences-description") }}
</BaseCardSectionTitle>
<div class="preference-container">
<div v-for="p in preferencesEditor" :key="p.key">
<v-checkbox
v-model="group.preferences[p.key]"
hide-details
dense
:label="p.label"
@change="groupActions.updatePreferences()"
/>
<p class="ml-8 text-subtitle-2 my-0 py-0">
{{ p.description }}
</p>
</div>
</div>
</section>
</v-container>
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { defineComponent } from "@nuxtjs/composition-api";
import { useGroupSelf } from "~/composables/use-groups";
import { ReadGroupPreferences } from "~/lib/api/types/group";
export default defineComponent({
middleware: ["auth", "can-manage-only"],
setup() {
const { group, actions: groupActions } = useGroupSelf();
const { i18n } = useContext();
type Preference = {
key: keyof ReadGroupPreferences;
value: boolean;
label: string;
description: string;
};
const preferencesEditor = computed<Preference[]>(() => {
if (!group.value || !group.value.preferences) {
return [];
}
return [
{
key: "recipePublic",
value: group.value.preferences.recipePublic || false,
label: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes"),
description: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes-description"),
} as Preference,
{
key: "recipeShowNutrition",
value: group.value.preferences.recipeShowNutrition || false,
label: i18n.t("group.show-nutrition-information"),
description: i18n.t("group.show-nutrition-information-description"),
} as Preference,
{
key: "recipeShowAssets",
value: group.value.preferences.recipeShowAssets || false,
label: i18n.t("group.show-recipe-assets"),
description: i18n.t("group.show-recipe-assets-description"),
} as Preference,
{
key: "recipeLandscapeView",
value: group.value.preferences.recipeLandscapeView || false,
label: i18n.t("group.default-to-landscape-view"),
description: i18n.t("group.default-to-landscape-view-description"),
} as Preference,
{
key: "recipeDisableComments",
value: group.value.preferences.recipeDisableComments || false,
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
} as Preference,
{
key: "recipeDisableAmount",
value: group.value.preferences.recipeDisableAmount || false,
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
} as Preference,
];
});
const allDays = [
{
name: i18n.t("general.sunday"),
value: 0,
},
{
name: i18n.t("general.monday"),
value: 1,
},
{
name: i18n.t("general.tuesday"),
value: 2,
},
{
name: i18n.t("general.wednesday"),
value: 3,
},
{
name: i18n.t("general.thursday"),
value: 4,
},
{
name: i18n.t("general.friday"),
value: 5,
},
{
name: i18n.t("general.saturday"),
value: 6,
},
];
return {
group,
groupActions,
allDays,
preferencesEditor,
};
},
head() {

View File

@ -0,0 +1,178 @@
<template>
<v-container class="narrow-container">
<BasePageTitle class="mb-5">
<template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
</template>
<template #title> {{ $t("profile.household-settings") }} </template>
{{ $t("profile.household-description") }}
</BasePageTitle>
<section v-if="household">
<BaseCardSectionTitle class="mt-10" :title="$tc('household.household-preferences')"></BaseCardSectionTitle>
<div class="mb-6">
<v-checkbox
v-model="household.preferences.privateHousehold"
hide-details
dense
:label="$t('household.private-household')"
@change="householdActions.updatePreferences()"
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("household.private-household-description") }}
</p>
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
</div>
</div>
<v-select
v-model="household.preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays"
item-text="name"
item-value="value"
:label="$t('settings.first-day-of-week')"
@change="householdActions.updatePreferences()"
/>
</section>
<section v-if="household">
<BaseCardSectionTitle class="mt-10" :title="$tc('group.default-recipe-preferences')">
{{ $t("household.default-recipe-preferences-description") }}
</BaseCardSectionTitle>
<div class="preference-container">
<div v-for="p in preferencesEditor" :key="p.key">
<v-checkbox
v-model="household.preferences[p.key]"
hide-details
dense
:label="p.label"
@change="householdActions.updatePreferences()"
/>
<p class="ml-8 text-subtitle-2 my-0 py-0">
{{ p.description }}
</p>
</div>
</div>
</section>
</v-container>
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useHouseholdSelf } from "~/composables/use-households";
import { ReadHouseholdPreferences } from "~/lib/api/types/household";
export default defineComponent({
middleware: ["auth", "can-manage-only"],
setup() {
const { household, actions: householdActions } = useHouseholdSelf();
const { i18n } = useContext();
type Preference = {
key: keyof ReadHouseholdPreferences;
value: boolean;
label: string;
description: string;
};
const preferencesEditor = computed<Preference[]>(() => {
if (!household.value || !household.value.preferences) {
return [];
}
return [
{
key: "recipePublic",
value: household.value.preferences.recipePublic || false,
label: i18n.t("household.allow-users-outside-of-your-household-to-see-your-recipes"),
description: i18n.t("household.allow-users-outside-of-your-household-to-see-your-recipes-description"),
} as Preference,
{
key: "recipeShowNutrition",
value: household.value.preferences.recipeShowNutrition || false,
label: i18n.t("group.show-nutrition-information"),
description: i18n.t("group.show-nutrition-information-description"),
} as Preference,
{
key: "recipeShowAssets",
value: household.value.preferences.recipeShowAssets || false,
label: i18n.t("group.show-recipe-assets"),
description: i18n.t("group.show-recipe-assets-description"),
} as Preference,
{
key: "recipeLandscapeView",
value: household.value.preferences.recipeLandscapeView || false,
label: i18n.t("group.default-to-landscape-view"),
description: i18n.t("group.default-to-landscape-view-description"),
} as Preference,
{
key: "recipeDisableComments",
value: household.value.preferences.recipeDisableComments || false,
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
} as Preference,
{
key: "recipeDisableAmount",
value: household.value.preferences.recipeDisableAmount || false,
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
} as Preference,
];
});
const allDays = [
{
name: i18n.t("general.sunday"),
value: 0,
},
{
name: i18n.t("general.monday"),
value: 1,
},
{
name: i18n.t("general.tuesday"),
value: 2,
},
{
name: i18n.t("general.wednesday"),
value: 3,
},
{
name: i18n.t("general.thursday"),
value: 4,
},
{
name: i18n.t("general.friday"),
value: 5,
},
{
name: i18n.t("general.saturday"),
value: 6,
},
];
return {
household,
householdActions,
allDays,
preferencesEditor,
};
},
head() {
return {
title: this.$t("household.household") as string,
};
},
});
</script>
<style lang="css">
.preference-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 600px;
}
</style>

View File

@ -39,10 +39,10 @@
<div class="d-flex flex-wrap align-center justify-space-between mb-2">
<v-tabs style="width: fit-content;">
<v-tab :to="`/group/mealplan/planner/view`">{{ $t('meal-plan.meal-planner') }}</v-tab>
<v-tab :to="`/group/mealplan/planner/edit`">{{ $t('general.edit') }}</v-tab>
<v-tab :to="`/household/mealplan/planner/view`">{{ $t('meal-plan.meal-planner') }}</v-tab>
<v-tab :to="`/household/mealplan/planner/edit`">{{ $t('general.edit') }}</v-tab>
</v-tabs>
<ButtonLink :icon="$globals.icons.calendar" :to="`/group/mealplan/settings`" :text="$tc('general.settings')" />
<ButtonLink :icon="$globals.icons.calendar" :to="`/household/mealplan/settings`" :text="$tc('general.settings')" />
</div>
<div>
@ -56,7 +56,7 @@
<script lang="ts">
import { computed, defineComponent, ref, useRoute, useRouter, watch } from "@nuxtjs/composition-api";
import { isSameDay, addDays, parseISO } from "date-fns";
import { useGroupSelf } from "~/composables/use-groups";
import { useHouseholdSelf } from "~/composables/use-households";
import { useMealplans } from "~/composables/use-group-mealplan";
import { useUserMealPlanPreferences } from "~/composables/use-users/preferences";
@ -65,7 +65,7 @@ export default defineComponent({
setup() {
const route = useRoute();
const router = useRouter();
const { group } = useGroupSelf();
const { household } = useHouseholdSelf();
const mealPlanPreferences = useUserMealPlanPreferences();
const numberOfDays = ref<number>(mealPlanPreferences.value.numberOfDays || 7);
@ -74,8 +74,8 @@ export default defineComponent({
});
// Force to /view if current route is /planner
if (route.value.path === "/group/mealplan/planner") {
router.push("/group/mealplan/planner/view");
if (route.value.path === "/household/mealplan/planner") {
router.push("/household/mealplan/planner/view");
}
function fmtYYYYMMDD(date: Date) {
@ -95,7 +95,7 @@ export default defineComponent({
});
const firstDayOfWeek = computed(() => {
return group.value?.preferences?.firstDayOfWeek || 0;
return household.value?.preferences?.firstDayOfWeek || 0;
});
const weekRange = computed(() => {

View File

@ -229,7 +229,7 @@ import { useMealplans, usePlanTypeOptions, getEntryTypeText } from "~/composable
import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue";
import { PlanEntryType, UpdatePlanEntry } from "~/lib/api/types/meal-plan";
import { useUserApi } from "~/composables/api";
import { useGroupSelf } from "~/composables/use-groups";
import { useHouseholdSelf } from "~/composables/use-households";
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
export default defineComponent({
@ -249,7 +249,7 @@ export default defineComponent({
},
setup(props) {
const api = useUserApi();
const { group } = useGroupSelf();
const { household } = useHouseholdSelf();
const state = ref({
dialog: false,
@ -257,7 +257,7 @@ export default defineComponent({
});
const firstDayOfWeek = computed(() => {
return group.value?.preferences?.firstDayOfWeek || 0;
return household.value?.preferences?.firstDayOfWeek || 0;
});
function onMoveCallback(evt: SortableEvent) {
@ -308,7 +308,7 @@ export default defineComponent({
entryType: "dinner" as PlanEntryType,
existing: false,
id: 0,
groupId: ""
groupId: "",
});
function openDialog(date: Date) {

View File

@ -54,7 +54,7 @@
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { MealsByDate } from "./types";
import { ReadPlanEntry } from "~/lib/api/types/meal-plan";
import GroupMealPlanDayContextMenu from "~/components/Domain/Group/GroupMealPlanDayContextMenu.vue";
import GroupMealPlanDayContextMenu from "~/components/Domain/Household/GroupMealPlanDayContextMenu.vue";
import RecipeCardMobile from "~/components/Domain/Recipe/RecipeCardMobile.vue";
import { RecipeSummary } from "~/lib/api/types/recipe";

View File

@ -89,7 +89,7 @@
import { defineComponent, ref, useAsync } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { PlanRulesCreate, PlanRulesOut } from "~/lib/api/types/meal-plan";
import GroupMealPlanRuleForm from "~/components/Domain/Group/GroupMealPlanRuleForm.vue";
import GroupMealPlanRuleForm from "~/components/Domain/Household/GroupMealPlanRuleForm.vue";
import { useAsyncKey } from "~/composables/use-utils";
import RecipeChips from "~/components/Domain/Recipe/RecipeChips.vue";

View File

@ -97,7 +97,7 @@ export default defineComponent({
];
async function refreshMembers() {
const { data } = await api.groups.fetchMembers();
const { data } = await api.households.fetchMembers();
if (data) {
members.value = data;
}
@ -111,7 +111,7 @@ export default defineComponent({
canOrganize: user.canOrganize,
};
await api.groups.setMemberPermissions(payload);
await api.households.setMemberPermissions(payload);
}
onMounted(async () => {

View File

@ -109,7 +109,7 @@
import { defineComponent, useAsync, reactive, useContext, toRefs } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/lib/api/types/group";
import { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/lib/api/types/household";
interface OptionKey {
text: string;

View File

@ -45,7 +45,7 @@
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import { useGroupWebhooks, timeUTC } from "~/composables/use-group-webhooks";
import GroupWebhookEditor from "~/components/Domain/Group/GroupWebhookEditor.vue";
import GroupWebhookEditor from "~/components/Domain/Household/GroupWebhookEditor.vue";
import { alert } from "~/composables/use-toast";
export default defineComponent({

View File

@ -294,8 +294,8 @@ import { useCopyList } from "~/composables/use-copy";
import { useUserApi } from "~/composables/api";
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue"
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
import { ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/group";
import { UserSummary } from "~/lib/api/types/user";
import { ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
import { UserOut } from "~/lib/api/types/user";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
@ -444,7 +444,7 @@ export default defineComponent({
unchecked: shoppingList.value?.listItems?.filter((item) => !item.checked) ?? [],
checked: shoppingList.value?.listItems
?.filter((item) => item.checked)
.sort((a, b) => (a.updateAt < b.updateAt ? 1 : -1))
.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1))
?? [],
};
});
@ -863,7 +863,7 @@ export default defineComponent({
item.position = shoppingList.value.listItems.length;
// set a temporary updatedAt timestamp prior to refresh so it appears at the top of the checked items
item.updateAt = new Date().toISOString();
item.updatedAt = new Date().toISOString();
}
// make updates reflect immediately
@ -934,7 +934,7 @@ export default defineComponent({
: 0;
createListItemData.value.createdAt = new Date().toISOString();
createListItemData.value.updateAt = createListItemData.value.createdAt;
createListItemData.value.updatedAt = createListItemData.value.createdAt;
updateListItemOrder();
@ -1020,16 +1020,16 @@ export default defineComponent({
// ===============================================================
// Shopping List Settings
const allUsers = ref<UserSummary[]>([]);
const allUsers = ref<UserOut[]>([]);
const currentUserId = ref<string | undefined>();
async function fetchAllUsers() {
const { data } = await userApi.users.getGroupUsers(1, -1, { orderBy: "full_name", orderDirection: "asc" });
const { data } = await userApi.households.fetchMembers();
if (!data) {
return;
}
// update current user
allUsers.value = data.items;
allUsers.value = data.sort((a, b) => ((a.fullName || "") < (b.fullName || "") ? -1 : 1));
currentUserId.value = shoppingList.value?.userId;
}

View File

@ -7,7 +7,7 @@
<p class="subtitle-1 mb-0 text-center">
{{ $t('profile.description') }}
</p>
<v-card flat color="background" width="100%" max-width="600px">
<v-card flat color="transparent" width="100%" max-width="600px">
<v-card-actions class="d-flex justify-center my-4">
<v-btn v-if="$auth.user.canInvite" outlined rounded @click="getSignupLink()">
<v-icon left>
@ -57,9 +57,9 @@
<v-row tag="section">
<v-col cols="12" sm="12" md="12">
<v-card outlined>
<v-card-title class="headline pb-0"> {{ $t('profile.group-statistics') }} </v-card-title>
<v-card-title class="headline pb-0"> {{ $t('profile.household-statistics') }} </v-card-title>
<v-card-text class="py-0">
{{ $t('profile.group-statistics-description') }}
{{ $t('profile.household-statistics-description') }}
</v-card-text>
<v-card-text class="d-flex flex-wrap justify-center align-center" style="gap: 0.8rem">
<StatsCards
@ -106,8 +106,66 @@
</AdvancedOnly>
</v-row>
</section>
<v-divider class="my-7"></v-divider>
<v-divider class="my-7" />
<section>
<div>
<h3 class="headline">{{ $t('household.household') }}</h3>
<p>{{ $t('profile.household-description') }}</p>
</div>
<v-row tag="section">
<v-col v-if="$auth.user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.household-settings'), to: `/household` }"
:image="require('~/static/svgs/manage-group-settings.svg')"
>
<template #title> {{ $t('profile.household-settings') }} </template>
{{ $t('profile.household-settings-description') }}
</UserProfileLinkCard>
</v-col>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-cookbooks'), to: `/g/${groupSlug}/cookbooks` }"
:image="require('~/static/svgs/manage-cookbooks.svg')"
>
<template #title> {{ $t('sidebar.cookbooks') }} </template>
{{ $t('profile.cookbooks-description') }}
</UserProfileLinkCard>
</v-col>
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-members'), to: `/household/members` }"
:image="require('~/static/svgs/manage-members.svg')"
>
<template #title> {{ $t('profile.members') }} </template>
{{ $t('profile.members-description') }}
</UserProfileLinkCard>
</v-col>
<AdvancedOnly>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-webhooks'), to: `/household/webhooks` }"
:image="require('~/static/svgs/manage-webhooks.svg')"
>
<template #title> {{ $t('settings.webhooks.webhooks') }} </template>
{{ $t('profile.webhooks-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
<AdvancedOnly>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-notifiers'), to: `/household/notifiers` }"
:image="require('~/static/svgs/manage-notifiers.svg')"
>
<template #title> {{ $t('profile.notifiers') }} </template>
{{ $t('profile.notifiers-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
</v-row>
</section>
<v-divider class="my-7" />
<section v-if="$auth.user.canManage || $auth.user.canOrganize || $auth.user.advanced">
<div>
<h3 class="headline">{{ $t('group.group') }}</h3>
<p>{{ $t('profile.group-description') }}</p>
@ -122,46 +180,6 @@
{{ $t('profile.group-settings-description') }}
</UserProfileLinkCard>
</v-col>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-cookbooks'), to: `/g/${groupSlug}/cookbooks` }"
:image="require('~/static/svgs/manage-cookbooks.svg')"
>
<template #title> {{ $t('sidebar.cookbooks') }} </template>
{{ $t('profile.cookbooks-description') }}
</UserProfileLinkCard>
</v-col>
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-members'), to: `/group/members` }"
:image="require('~/static/svgs/manage-members.svg')"
>
<template #title> {{ $t('profile.members') }} </template>
{{ $t('profile.members-description') }}
</UserProfileLinkCard>
</v-col>
<AdvancedOnly>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-webhooks'), to: `/group/webhooks` }"
:image="require('~/static/svgs/manage-webhooks.svg')"
>
<template #title> {{ $t('settings.webhooks.webhooks') }} </template>
{{ $t('profile.webhooks-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
<AdvancedOnly>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-notifiers'), to: `/group/notifiers` }"
:image="require('~/static/svgs/manage-notifiers.svg')"
>
<template #title> {{ $t('profile.notifiers') }} </template>
{{ $t('profile.notifiers-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
<!-- $auth.user.canOrganize should not be null because of the auth middleware -->
<v-col v-if="$auth.user.canOrganize" cols="12" sm="12" md="6">
<UserProfileLinkCard
@ -224,7 +242,7 @@ export default defineComponent({
const api = useUserApi();
async function getSignupLink() {
const { data } = await api.groups.createInvitation({ uses: 1 });
const { data } = await api.households.createInvitation({ uses: 1 });
if (data) {
token.value = data.token;
generatedSignupLink.value = constructLink(data.token);
@ -272,7 +290,7 @@ export default defineComponent({
});
const stats = useAsync(async () => {
const { data } = await api.groups.statistics();
const { data } = await api.households.statistics();
if (data) {
return data;
@ -306,7 +324,7 @@ export default defineComponent({
const statsTo = computed<{ [key: string]: string }>(() => { return {
totalRecipes: `/g/${groupSlug.value}/`,
totalUsers: "/group/members",
totalUsers: "/household/members",
totalCategories: `/g/${groupSlug.value}/recipes/categories`,
totalTags: `/g/${groupSlug.value}/recipes/tags`,
totalTools: `/g/${groupSlug.value}/recipes/tools`,

View File

@ -68,7 +68,7 @@ async def get_public_group(group_slug: str = fastapi.Path(...), session=Depends(
repos = get_repositories(session)
group = repos.groups.get_by_slug_or_id(group_slug)
if not group or group.preferences.private_group or not group.preferences.recipe_public:
if not group or group.preferences.private_group:
raise HTTPException(404, "group not found")
else:
return group
@ -111,7 +111,7 @@ async def get_current_user(
except PyJWTError as e:
raise credentials_exception from e
repos = get_repositories(session)
repos = get_repositories(session, group_id=None, household_id=None)
user = repos.users.get_one(token_data.user_id, "id", any_case=False)
@ -139,7 +139,7 @@ async def get_admin_user(current_user: PrivateUser = Depends(get_current_user))
def validate_long_live_token(session: Session, client_token: str, user_id: str) -> PrivateUser:
repos = get_repositories(session)
repos = get_repositories(session, group_id=None, household_id=None)
token = repos.api_tokens.multi_query({"token": client_token, "user_id": user_id})

View File

@ -56,7 +56,7 @@ class AuthProvider(Generic[T], metaclass=abc.ABCMeta):
if self.__has_tried_user:
return self.user
db = get_repositories(self.session)
db = get_repositories(self.session, group_id=None, household_id=None)
user = user = db.users.get_one(username, "username", any_case=True)
if not user:

View File

@ -23,7 +23,7 @@ class CredentialsProvider(AuthProvider[CredentialsRequest]):
async def authenticate(self) -> tuple[str, timedelta] | None:
"""Attempt to authenticate a user given a username and password"""
settings = get_app_settings()
db = get_repositories(self.session)
db = get_repositories(self.session, group_id=None, household_id=None)
user = self.try_get_user(self.data.username)
if not user:

View File

@ -95,7 +95,7 @@ class LDAPProvider(CredentialsProvider):
"""
settings = get_app_settings()
db = get_repositories(self.session)
db = get_repositories(self.session, group_id=None, household_id=None)
if not self.data:
return None
data = self.data

View File

@ -33,7 +33,7 @@ class OpenIDProvider(AuthProvider[OIDCRequest]):
if not claims:
return None
repos = get_repositories(self.session)
repos = get_repositories(self.session, group_id=None, household_id=None)
user = self.try_get_user(claims.get(settings.OIDC_USER_CLAIM))
is_admin = False

View File

@ -179,6 +179,7 @@ class AppSettings(AppLoggingSettings):
return self.DB_PROVIDER.db_url_public if self.DB_PROVIDER else None
DEFAULT_GROUP: str = "Home"
DEFAULT_HOUSEHOLD: str = "Family"
_DEFAULT_EMAIL: str = "changeme@example.com"
"""

View File

@ -26,7 +26,7 @@ SessionLocal, engine = sql_global_init(settings.DB_URL) # type: ignore
@contextmanager
def session_context() -> Session:
def session_context() -> Generator[Session, None, None]:
"""
session_context() provides a managed session to the database that is automatically
closed when the context is exited. This is the preferred method of accessing the

View File

@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from mealie.core import root_logger
from mealie.db.models.group.group import Group
from mealie.db.models.group.shopping_list import ShoppingList, ShoppingListMultiPurposeLabel
from mealie.db.models.household.shopping_list import ShoppingList, ShoppingListMultiPurposeLabel
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
from mealie.db.models.recipe.recipe import RecipeModel

Some files were not shown because too many files have changed in this diff Show More