chore: Add Stricter Frontend Formatting (#6262)

This commit is contained in:
Michael Genson
2025-09-27 13:57:53 -05:00
committed by GitHub
parent ecdf7de386
commit d16a10440d
52 changed files with 945 additions and 818 deletions

View File

@@ -59,8 +59,11 @@
"netlify.toml": "runtime.txt", "netlify.toml": "runtime.txt",
"README.md": "LICENSE, SECURITY.md" "README.md": "LICENSE, SECURITY.md"
}, },
"[typescript]": {
"editor.formatOnSave": true
},
"[vue]": { "[vue]": {
"editor.formatOnSave": false "editor.formatOnSave": true
}, },
"[python]": { "[python]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,

View File

@@ -1,3 +1,4 @@
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -105,12 +106,16 @@ def main():
# Flatten list of lists # Flatten list of lists
all_children = [item for sublist in all_children for item in sublist] all_children = [item for sublist in all_children for item in sublist]
out_path = GENERATED / "__init__.py"
render_python_template( render_python_template(
TEMPLATE, TEMPLATE,
GENERATED / "__init__.py", out_path,
{"children": all_children}, {"children": all_children},
) )
subprocess.run(["poetry", "run", "ruff", "check", str(out_path), "--fix"])
subprocess.run(["poetry", "run", "ruff", "format", str(out_path)])
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,5 +1,6 @@
import pathlib import pathlib
import re import re
import subprocess
from dataclasses import dataclass, field from dataclasses import dataclass, field
from utils import PROJECT_DIR, log, render_python_template from utils import PROJECT_DIR, log, render_python_template
@@ -84,16 +85,23 @@ def find_modules(root: pathlib.Path) -> list[Modules]:
return modules return modules
def main(): def main() -> None:
modules = find_modules(SCHEMA_PATH) modules = find_modules(SCHEMA_PATH)
template_paths: list[pathlib.Path] = []
for module in modules: for module in modules:
log.debug(f"Module: {module.directory.name}") log.debug(f"Module: {module.directory.name}")
for file in module.files: for file in module.files:
log.debug(f" File: {file.import_path}") log.debug(f" File: {file.import_path}")
log.debug(f" Classes: [{', '.join(file.classes)}]") log.debug(f" Classes: [{', '.join(file.classes)}]")
render_python_template(template, module.directory / "__init__.py", {"module": module}) template_path = module.directory / "__init__.py"
template_paths.append(template_path)
render_python_template(template, template_path, {"module": module})
path_args = (str(p) for p in template_paths)
subprocess.run(["poetry", "run", "ruff", "check", *path_args, "--fix"])
subprocess.run(["poetry", "run", "ruff", "format", *path_args])
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,4 +1,5 @@
import re import re
import subprocess
from pathlib import Path from pathlib import Path
from jinja2 import Template from jinja2 import Template
@@ -189,6 +190,7 @@ def generate_typescript_types() -> None: # noqa: C901
skipped_dirs: list[Path] = [] skipped_dirs: list[Path] = []
failed_modules: list[Path] = [] failed_modules: list[Path] = []
out_paths: list[Path] = []
for module in schema_path.iterdir(): for module in schema_path.iterdir():
if module.is_dir() and module.stem in ignore_dirs: if module.is_dir() and module.stem in ignore_dirs:
skipped_dirs.append(module) skipped_dirs.append(module)
@@ -205,10 +207,18 @@ def generate_typescript_types() -> None: # noqa: C901
path_as_module = path_to_module(module) path_as_module = path_to_module(module)
generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore
clean_output_file(out_path) clean_output_file(out_path)
out_paths.append(out_path)
except Exception: except Exception:
failed_modules.append(module) failed_modules.append(module)
log.exception(f"Module Error: {module}") log.exception(f"Module Error: {module}")
# Run ESLint --fix on the files to clean up any formatting issues
subprocess.run(
["yarn", "lint", "--fix", *(str(path) for path in out_paths)],
check=True,
cwd=PROJECT_DIR / "frontend",
)
log.debug("\n📁 Skipped Directories:") log.debug("\n📁 Skipped Directories:")
for skipped_dir in skipped_dirs: for skipped_dir in skipped_dirs:
log.debug(f" 📁 {skipped_dir.name}") log.debug(f" 📁 {skipped_dir.name}")

View File

@@ -1,5 +1,4 @@
import logging import logging
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -23,11 +22,6 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict):
dest.write_text(text) 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 @dataclass
class CodeSlicer: class CodeSlicer:
@@ -37,7 +31,7 @@ class CodeSlicer:
indentation: str | None indentation: str | None
text: list[str] text: list[str]
_next_line = None _next_line: int | None = None
def purge_lines(self) -> None: def purge_lines(self) -> None:
start = self.start + 1 start = self.start + 1

View File

@@ -32,9 +32,9 @@
> >
<div class="d-flex align-center w-100 mb-2"> <div class="d-flex align-center w-100 mb-2">
<v-toolbar-title class="headline mb-0"> <v-toolbar-title class="headline mb-0">
<v-icon size="large" class="mr-3"> <v-icon size="large" class="mr-3">
{{ $globals.icons.pages }} {{ $globals.icons.pages }}
</v-icon> </v-icon>
{{ book.name }} {{ book.name }}
</v-toolbar-title> </v-toolbar-title>
<BaseButton <BaseButton

View File

@@ -1,44 +1,44 @@
<template> <template>
<div v-if="preferences"> <div v-if="preferences">
<BaseCardSectionTitle :title="$t('household.household-preferences')" /> <BaseCardSectionTitle :title="$t('household.household-preferences')" />
<div class="mb-6"> <div class="mb-6">
<v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" /> <v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
<div class="ml-8"> <div class="ml-8">
<p class="text-subtitle-2 my-0 py-0"> <p class="text-subtitle-2 my-0 py-0">
{{ $t("household.private-household-description") }} {{ $t("household.private-household-description") }}
</p> </p>
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" /> <DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
</div> </div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" /> <v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
<div class="ml-8"> <div class="ml-8">
<p class="text-subtitle-2 my-0 py-0"> <p class="text-subtitle-2 my-0 py-0">
{{ $t("household.lock-recipe-edits-from-other-households-description") }} {{ $t("household.lock-recipe-edits-from-other-households-description") }}
</p> </p>
</div> </div>
</div> </div>
<v-select <v-select
v-model="preferences.firstDayOfWeek" v-model="preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin" :prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays" :items="allDays"
item-title="name" item-title="name"
item-value="value" item-value="value"
:label="$t('settings.first-day-of-week')" :label="$t('settings.first-day-of-week')"
variant="underlined" variant="underlined"
flat flat
/> />
<BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')" /> <BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')" />
<div class="preference-container"> <div class="preference-container">
<div v-for="p in recipePreferences" :key="p.key"> <div v-for="p in recipePreferences" :key="p.key">
<v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" /> <v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" />
<p class="ml-8 text-subtitle-2 my-0 py-0"> <p class="ml-8 text-subtitle-2 my-0 py-0">
{{ p.description }} {{ p.description }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -432,9 +432,9 @@ function removeField(index: number) {
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => { const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
/* newFields.forEach((field, index) => { /* newFields.forEach((field, index) => {
const updatedField = getFieldFromFieldDef(field); const updatedField = getFieldFromFieldDef(field);
fields.value[index] = updatedField; // recursive!!! fields.value[index] = updatedField; // recursive!!!
}); */ }); */
const qf = buildQueryFilterString(fields.value, state.showAdvanced); const qf = buildQueryFilterString(fields.value, state.showAdvanced);
if (qf) { if (qf) {

View File

@@ -5,8 +5,14 @@
density="compact" density="compact"
elevation="0" elevation="0"
> >
<BaseDialog v-model="deleteDialog" :title="$t('recipe.delete-recipe')" color="error" <BaseDialog
:icon="$globals.icons.alertCircle" can-confirm @confirm="emitDelete()"> v-model="deleteDialog"
:title="$t('recipe.delete-recipe')"
color="error"
:icon="$globals.icons.alertCircle"
can-confirm
@confirm="emitDelete()"
>
<v-card-text> <v-card-text>
{{ $t("recipe.delete-confirmation") }} {{ $t("recipe.delete-confirmation") }}
</v-card-text> </v-card-text>
@@ -15,7 +21,14 @@
<v-spacer /> <v-spacer />
<div v-if="!open" class="custom-btn-group ma-1"> <div v-if="!open" class="custom-btn-group ma-1">
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always /> <RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" /> <RecipeTimelineBadge
v-if="loggedIn"
class="ml-1"
color="info"
button-style
:slug="recipe.slug"
:recipe-name="recipe.name!"
/>
<div v-if="loggedIn"> <div v-if="loggedIn">
<v-tooltip v-if="canEdit" location="bottom" color="info"> <v-tooltip v-if="canEdit" location="bottom" color="info">
<template #activator="{ props: tooltipProps }"> <template #activator="{ props: tooltipProps }">

View File

@@ -1,101 +1,101 @@
<template> <template>
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition --> <!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
<div> <div>
<v-hover <v-hover
v-slot="{ isHovering, props: hoverProps }" v-slot="{ isHovering, props: hoverProps }"
:open-delay="50" :open-delay="50"
>
<v-card
v-bind="hoverProps"
:class="{ 'on-hover': isHovering }"
:style="{ cursor }"
:elevation="isHovering ? 12 : 2"
:to="recipeRoute"
:min-height="imageHeight + 75"
@click.self="$emit('click')"
> >
<v-card <RecipeCardImage
v-bind="hoverProps" :icon-size="imageHeight"
:class="{ 'on-hover': isHovering }" :height="imageHeight"
:style="{ cursor }" :slug="slug"
:elevation="isHovering ? 12 : 2" :recipe-id="recipeId"
:to="recipeRoute" size="small"
:min-height="imageHeight + 75" :image-version="image"
@click.self="$emit('click')"
> >
<RecipeCardImage <v-expand-transition v-if="description">
:icon-size="imageHeight" <div
:height="imageHeight" v-if="isHovering"
:slug="slug" class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
:recipe-id="recipeId" style="height: 100%"
size="small"
:image-version="image"
>
<v-expand-transition v-if="description">
<div
v-if="isHovering"
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
style="height: 100%"
>
<v-card-text class="v-card--text-show white--text">
<div class="descriptionWrapper">
<SafeMarkdown :source="description" />
</div>
</v-card-text>
</div>
</v-expand-transition>
</RecipeCardImage>
<v-card-title class="mb-n3 px-4">
<div class="headerClass">
{{ name }}
</div>
</v-card-title>
<slot name="actions">
<v-card-actions
v-if="showRecipeContent"
class="px-1"
> >
<RecipeFavoriteBadge <v-card-text class="v-card--text-show white--text">
v-if="isOwnGroup" <div class="descriptionWrapper">
class="absolute" <SafeMarkdown :source="description" />
:recipe-id="recipeId" </div>
show-always </v-card-text>
/> </div>
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent --> </v-expand-transition>
</RecipeCardImage>
<v-card-title class="mb-n3 px-4">
<div class="headerClass">
{{ name }}
</div>
</v-card-title>
<RecipeCardRating <slot name="actions">
:model-value="rating" <v-card-actions
:recipe-id="recipeId" v-if="showRecipeContent"
/> class="px-1"
<v-spacer /> >
<RecipeChips <RecipeFavoriteBadge
:truncate="true" v-if="isOwnGroup"
:items="tags" class="absolute"
:title="false" :recipe-id="recipeId"
:limit="2" show-always
small />
url-prefix="tags" <div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
v-bind="$attrs"
/>
<!-- If we're not logged-in, no items display, so we hide this menu --> <RecipeCardRating
<RecipeContextMenu :model-value="rating"
v-if="isOwnGroup && showRecipeContent" :recipe-id="recipeId"
color="grey-darken-2" />
:slug="slug" <v-spacer />
:menu-icon="$globals.icons.dotsVertical" <RecipeChips
:name="name" :truncate="true"
:recipe-id="recipeId" :items="tags"
:use-items="{ :title="false"
delete: false, :limit="2"
edit: false, small
download: true, url-prefix="tags"
mealplanner: true, v-bind="$attrs"
shoppingList: true, />
print: false,
printPreferences: false, <!-- If we're not logged-in, no items display, so we hide this menu -->
share: true, <RecipeContextMenu
}" v-if="isOwnGroup && showRecipeContent"
@deleted="$emit('delete', slug)" color="grey-darken-2"
/> :slug="slug"
</v-card-actions> :menu-icon="$globals.icons.dotsVertical"
</slot> :name="name"
<slot /> :recipe-id="recipeId"
</v-card> :use-items="{
</v-hover> delete: false,
</div> edit: false,
download: true,
mealplanner: true,
shoppingList: true,
print: false,
printPreferences: false,
share: true,
}"
@deleted="$emit('delete', slug)"
/>
</v-card-actions>
</slot>
<slot />
</v-card>
</v-hover>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -116,7 +116,7 @@
@deleted="$emit('delete', slug)" @deleted="$emit('delete', slug)"
/> />
</v-card-actions> </v-card-actions>
</slot> </slot>
</v-list-item> </v-list-item>
<slot /> <slot />
</v-card> </v-card>

View File

@@ -67,7 +67,7 @@
hide-header hide-header
:first-day-of-week="firstDayOfWeek" :first-day-of-week="firstDayOfWeek"
:local="$i18n.locale" :local="$i18n.locale"
@update:model-value="pickerMenu = false" @update:model-value="pickerMenu = false"
/> />
</v-menu> </v-menu>
<v-select <v-select

View File

@@ -1,8 +1,15 @@
<template> <template>
<div> <div>
<BaseDialog v-model="dialog" :title="$t('data-pages.manage-aliases')" :icon="$globals.icons.edit" <BaseDialog
:submit-icon="$globals.icons.check" :submit-text="$t('general.confirm')" can-submit @submit="saveAliases" v-model="dialog"
@cancel="$emit('cancel')"> :title="$t('data-pages.manage-aliases')"
:icon="$globals.icons.edit"
:submit-icon="$globals.icons.check"
:submit-text="$t('general.confirm')"
can-submit
@submit="saveAliases"
@cancel="$emit('cancel')"
>
<v-card-text> <v-card-text>
<v-container> <v-container>
<v-row v-for="alias, i in aliases" :key="i"> <v-row v-for="alias, i in aliases" :key="i">
@@ -10,13 +17,16 @@
<v-text-field v-model="alias.name" :label="$t('general.name')" :rules="[validators.required]" /> <v-text-field v-model="alias.name" :label="$t('general.name')" :rules="[validators.required]" />
</v-col> </v-col>
<v-col cols="2"> <v-col cols="2">
<BaseButtonGroup :buttons="[ <BaseButtonGroup
{ :buttons="[
icon: $globals.icons.delete, {
text: $t('general.delete'), icon: $globals.icons.delete,
event: 'delete', text: $t('general.delete'),
}, event: 'delete',
]" @delete="deleteAlias(i)" /> },
]"
@delete="deleteAlias(i)"
/>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>

View File

@@ -113,9 +113,13 @@
/> />
<v-divider /> <v-divider />
</v-col> </v-col>
<v-col class="overflow-y-auto" <v-col
:class="$vuetify.display.smAndDown ? 'py-2': 'py-6'" class="overflow-y-auto"
style="height: 100%" cols="12" sm="7"> :class="$vuetify.display.smAndDown ? 'py-2': 'py-6'"
style="height: 100%"
cols="12"
sm="7"
>
<h2 class="text-h5 px-4 font-weight-medium opacity-80"> <h2 class="text-h5 px-4 font-weight-medium opacity-80">
{{ $t('recipe.instructions') }} {{ $t('recipe.instructions') }}
</h2> </h2>

View File

@@ -13,25 +13,25 @@
@upload="uploadImage" @upload="uploadImage"
/> />
<v-spacer /> <v-spacer />
<v-select <v-select
v-model="recipe.userId" v-model="recipe.userId"
class="my-2" class="my-2"
max-width="300" max-width="300"
:items="allUsers" :items="allUsers"
:item-props="itemsProps" :item-props="itemsProps"
:label="$t('general.owner')" :label="$t('general.owner')"
:disabled="!canEditOwner" :disabled="!canEditOwner"
variant="outlined" variant="outlined"
density="compact" density="compact"
> >
<template #prepend> <template #prepend>
<UserAvatar <UserAvatar
:user-id="recipe.userId" :user-id="recipe.userId"
:tooltip="false" :tooltip="false"
/> />
</template> </template>
</v-select> </v-select>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -29,8 +29,8 @@
class="mb-2 mx-n2" class="mb-2 mx-n2"
> >
<v-card-title class="text-h5 font-weight-medium opacity-80"> <v-card-title class="text-h5 font-weight-medium opacity-80">
{{ $t('recipe.api-extras') }} {{ $t('recipe.api-extras') }}
</v-card-title> </v-card-title>
<v-divider class="ml-4" /> <v-divider class="ml-4" />
<v-card-text> <v-card-text>
{{ $t('recipe.api-extras-description') }} {{ $t('recipe.api-extras-description') }}

View File

@@ -12,10 +12,10 @@
> >
<v-card-text class="w-100"> <v-card-text class="w-100">
<div class="d-flex flex-column align-center"> <div class="d-flex flex-column align-center">
<v-card-title class="text-h5 font-weight-regular pa-0 text-wrap text-center opacity-80"> <v-card-title class="text-h5 font-weight-regular pa-0 text-wrap text-center opacity-80">
{{ recipe.name }} {{ recipe.name }}
</v-card-title> </v-card-title>
<RecipeRating <RecipeRating
:key="recipe.slug" :key="recipe.slug"
:value="recipe.rating" :value="recipe.rating"
:recipe-id="recipe.id" :recipe-id="recipe.id"

View File

@@ -1,4 +1,3 @@
<!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<div> <div>
<div class="mb-4"> <div class="mb-4">

View File

@@ -79,8 +79,8 @@
<BaseButton <BaseButton
v-if=" v-if="
currentMissingUnit currentMissingUnit
&& currentIng.ingredient.unit?.id && currentIng.ingredient.unit?.id
&& currentMissingUnit.toLowerCase() != currentIng.ingredient.unit?.name.toLowerCase() && currentMissingUnit.toLowerCase() != currentIng.ingredient.unit?.name.toLowerCase()
" "
color="warning" color="warning"
size="small" size="small"
@@ -99,8 +99,8 @@
<BaseButton <BaseButton
v-if=" v-if="
currentMissingFood currentMissingFood
&& currentIng.ingredient.food?.id && currentIng.ingredient.food?.id
&& currentMissingFood.toLowerCase() != currentIng.ingredient.food?.name.toLowerCase() && currentMissingFood.toLowerCase() != currentIng.ingredient.food?.name.toLowerCase()
" "
color="warning" color="warning"
size="small" size="small"
@@ -176,9 +176,9 @@
:text="$t(currentIngShouldDelete ? 'recipe.parser.delete-item' : 'general.next')" :text="$t(currentIngShouldDelete ? 'recipe.parser.delete-item' : 'general.next')"
@click="nextIngredient" @click="nextIngredient"
/> />
</div> </div>
<!-- Review --> <!-- Review -->
<div v-else> <div v-else>
<BaseButton <BaseButton
create create
:text="$t('general.save')" :text="$t('general.save')"
@@ -186,7 +186,7 @@
:loading="state.loading.save" :loading="state.loading.save"
@click="saveIngs" @click="saveIngs"
/> />
</div> </div>
</template> </template>
</BaseDialog> </BaseDialog>
</template> </template>

View File

@@ -4,20 +4,23 @@
<section> <section>
<v-container class="ma-0 pa-0"> <v-container class="ma-0 pa-0">
<v-row> <v-row>
<v-col v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden" <v-col
:order="preferences.imagePosition == ImagePosition.left ? -1 : 1" v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden"
cols="4" :order="preferences.imagePosition == ImagePosition.left ? -1 : 1"
align-self="center" cols="4"
align-self="center"
> >
<img :key="imageKey" <img
:src="recipeImageUrl" :key="imageKey"
style="min-height: 50; max-width: 100%;" :src="recipeImageUrl"
style="min-height: 50; max-width: 100%;"
> >
</v-col> </v-col>
<v-col order="0"> <v-col order="0">
<v-card-title class="headline pl-0"> <v-card-title class="headline pl-0">
<v-icon start <v-icon
color="primary" start
color="primary"
> >
{{ $globals.icons.primary }} {{ $globals.icons.primary }}
</v-icon> </v-icon>
@@ -36,17 +39,19 @@
</div> </div>
</div> </div>
<v-row class="d-flex justify-start"> <v-row class="d-flex justify-start">
<RecipeTimeCard :prep-time="recipe.prepTime" <RecipeTimeCard
:total-time="recipe.totalTime" :prep-time="recipe.prepTime"
:perform-time="recipe.performTime" :total-time="recipe.totalTime"
small :perform-time="recipe.performTime"
color="white" small
class="ml-4" color="white"
class="ml-4"
/> />
</v-row> </v-row>
<v-card-text v-if="preferences.showDescription" <v-card-text
class="px-0" v-if="preferences.showDescription"
class="px-0"
> >
<SafeMarkdown :source="recipe.description" /> <SafeMarkdown :source="recipe.description" />
</v-card-text> </v-card-text>
@@ -60,24 +65,29 @@
<v-card-title class="headline pl-0"> <v-card-title class="headline pl-0">
{{ $t("recipe.ingredients") }} {{ $t("recipe.ingredients") }}
</v-card-title> </v-card-title>
<div v-for="(ingredientSection, sectionIndex) in ingredientSections" <div
:key="`ingredient-section-${sectionIndex}`" v-for="(ingredientSection, sectionIndex) in ingredientSections"
class="print-section" :key="`ingredient-section-${sectionIndex}`"
class="print-section"
> >
<h4 v-if="ingredientSection.ingredients[0].title" <h4
class="ingredient-title mt-2" v-if="ingredientSection.ingredients[0].title"
class="ingredient-title mt-2"
> >
{{ ingredientSection.ingredients[0].title }} {{ ingredientSection.ingredients[0].title }}
</h4> </h4>
<div class="ingredient-grid" <div
:style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }" class="ingredient-grid"
:style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }"
> >
<template v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients" <template
:key="`ingredient-${ingredientIndex}`" v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients"
:key="`ingredient-${ingredientIndex}`"
> >
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<p class="ingredient-body" <p
v-html="parseText(ingredient)" class="ingredient-body"
v-html="parseText(ingredient)"
/> />
</template> </template>
</div> </div>
@@ -86,22 +96,26 @@
<!-- Instructions --> <!-- Instructions -->
<section> <section>
<div v-for="(instructionSection, sectionIndex) in instructionSections" <div
:key="`instruction-section-${sectionIndex}`" v-for="(instructionSection, sectionIndex) in instructionSections"
:class="{ 'print-section': instructionSection.sectionName }" :key="`instruction-section-${sectionIndex}`"
:class="{ 'print-section': instructionSection.sectionName }"
> >
<v-card-title v-if="!sectionIndex" <v-card-title
class="headline pl-0" v-if="!sectionIndex"
class="headline pl-0"
> >
{{ $t("recipe.instructions") }} {{ $t("recipe.instructions") }}
</v-card-title> </v-card-title>
<div v-for="(step, stepIndex) in instructionSection.instructions" <div
:key="`instruction-${stepIndex}`" v-for="(step, stepIndex) in instructionSection.instructions"
:key="`instruction-${stepIndex}`"
> >
<div class="print-section"> <div class="print-section">
<h4 v-if="step.title" <h4
:key="`instruction-title-${stepIndex}`" v-if="step.title"
class="instruction-title mb-2" :key="`instruction-title-${stepIndex}`"
class="instruction-title mb-2"
> >
{{ step.title }} {{ step.title }}
</h4> </h4>
@@ -112,8 +126,9 @@
+ 1, + 1,
}) }} }) }}
</h5> </h5>
<SafeMarkdown :source="step.text" <SafeMarkdown
class="recipe-step-body" :source="step.text"
class="recipe-step-body"
/> />
</div> </div>
</div> </div>
@@ -122,18 +137,21 @@
<!-- Notes --> <!-- Notes -->
<div v-if="preferences.showNotes"> <div v-if="preferences.showNotes">
<v-divider v-if="hasNotes" <v-divider
class="grey my-4" v-if="hasNotes"
class="grey my-4"
/> />
<section> <section>
<div v-for="(note, index) in recipe.notes" <div
:key="index + 'note'" v-for="(note, index) in recipe.notes"
:key="index + 'note'"
> >
<div class="print-section"> <div class="print-section">
<h4>{{ note.title }}</h4> <h4>{{ note.title }}</h4>
<SafeMarkdown :source="note.text" <SafeMarkdown
class="note-body" :source="note.text"
class="note-body"
/> />
</div> </div>
</div> </div>
@@ -150,8 +168,9 @@
<div class="print-section"> <div class="print-section">
<table class="nutrition-table"> <table class="nutrition-table">
<tbody> <tbody>
<tr v-for="(value, key) in recipe.nutrition" <tr
:key="key" v-for="(value, key) in recipe.nutrition"
:key="key"
> >
<template v-if="value"> <template v-if="value">
<td>{{ labels[key].label }}</td> <td>{{ labels[key].label }}</td>

View File

@@ -2,7 +2,8 @@
<div @click.prevent> <div @click.prevent>
<!-- User Rating --> <!-- User Rating -->
<v-hover v-slot="{ isHovering, props }"> <v-hover v-slot="{ isHovering, props }">
<v-rating v-if="isOwnGroup && (userRating || isHovering || !ratingsLoaded)" <v-rating
v-if="isOwnGroup && (userRating || isHovering || !ratingsLoaded)"
v-bind="props" v-bind="props"
:model-value="userRating" :model-value="userRating"
active-color="secondary" active-color="secondary"
@@ -15,7 +16,8 @@
@update:model-value="updateRating(+$event)" @update:model-value="updateRating(+$event)"
/> />
<!-- Group Rating --> <!-- Group Rating -->
<v-rating v-else <v-rating
v-else
v-bind="props" v-bind="props"
:model-value="groupRating" :model-value="groupRating"
:half-increments="true" :half-increments="true"

View File

@@ -84,12 +84,12 @@
:buttons="[ :buttons="[
...(allowDelete ...(allowDelete
? [ ? [
{ {
icon: $globals.icons.delete, icon: $globals.icons.delete,
text: $t('general.delete'), text: $t('general.delete'),
event: 'delete', event: 'delete',
}, },
] ]
: []), : []),
{ {
icon: $globals.icons.close, icon: $globals.icons.close,

View File

@@ -78,7 +78,7 @@
<v-list-item-subtitle class="font-weight-medium" style="font-size: small;"> <v-list-item-subtitle class="font-weight-medium" style="font-size: small;">
{{ item.subtitle }} {{ item.subtitle }}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
</div> </div>
</template> </template>
</v-list> </v-list>

View File

@@ -33,20 +33,39 @@
<template v-for="nav in topLink"> <template v-for="nav in topLink">
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
<!-- Multi Items --> <!-- Multi Items -->
<v-list-group v-if="nav.children" :key="(nav.key || nav.title) + 'multi-item'" <v-list-group
v-model="dropDowns[nav.title]" color="primary" :prepend-icon="nav.icon" :fluid="true"> v-if="nav.children"
:key="(nav.key || nav.title) + 'multi-item'"
v-model="dropDowns[nav.title]"
color="primary"
:prepend-icon="nav.icon"
:fluid="true"
>
<template #activator="{ props }"> <template #activator="{ props }">
<v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" /> <v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" />
</template> </template>
<v-list-item v-for="child in nav.children" :key="child.key || child.title" exact :to="child.to" <v-list-item
:prepend-icon="child.icon" :title="child.title" class="ml-4" /> v-for="child in nav.children"
:key="child.key || child.title"
exact
:to="child.to"
:prepend-icon="child.icon"
:title="child.title"
class="ml-4"
/>
</v-list-group> </v-list-group>
<!-- Single Item --> <!-- Single Item -->
<template v-else> <template v-else>
<v-list-item :key="(nav.key || nav.title) + 'single-item'" exact link :to="nav.to" <v-list-item
:prepend-icon="nav.icon" :title="nav.title" /> :key="(nav.key || nav.title) + 'single-item'"
exact
link
:to="nav.to"
:prepend-icon="nav.icon"
:title="nav.title"
/>
</template> </template>
</div> </div>
</template> </template>
@@ -60,14 +79,27 @@
<template v-for="nav in secondaryLinks"> <template v-for="nav in secondaryLinks">
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
<!-- Multi Items --> <!-- Multi Items -->
<v-list-group v-if="nav.children" :key="(nav.key || nav.title) + 'multi-item'" <v-list-group
v-model="dropDowns[nav.title]" color="primary" :prepend-icon="nav.icon" fluid> v-if="nav.children"
:key="(nav.key || nav.title) + 'multi-item'"
v-model="dropDowns[nav.title]"
color="primary"
:prepend-icon="nav.icon"
fluid
>
<template #activator="{ props }"> <template #activator="{ props }">
<v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" /> <v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" />
</template> </template>
<v-list-item v-for="child in nav.children" :key="child.key || child.title" exact :to="child.to" <v-list-item
class="ml-2" :prepend-icon="child.icon" :title="child.title" /> v-for="child in nav.children"
:key="child.key || child.title"
exact
:to="child.to"
class="ml-2"
:prepend-icon="child.icon"
:title="child.title"
/>
</v-list-group> </v-list-group>
<!-- Single Item --> <!-- Single Item -->

View File

@@ -41,7 +41,8 @@
:hide-details="!inputField.hint" :hide-details="!inputField.hint"
:persistent-hint="!!inputField.hint" :persistent-hint="!!inputField.hint"
density="comfortable" density="comfortable"
@change="emitBlur"> @change="emitBlur"
>
<template #label> <template #label>
<span class="ml-4"> <span class="ml-4">
{{ inputField.label }} {{ inputField.label }}

View File

@@ -200,8 +200,8 @@ function open() {
} }
/* function close() { /* function close() {
dialog.value = false; dialog.value = false;
logDeprecatedProp("close"); logDeprecatedProp("close");
} */ } */
function logDeprecatedProp(val: string) { function logDeprecatedProp(val: string) {

View File

@@ -1,6 +1,6 @@
<template> <template>
<!-- eslint-disable-next-line vue/no-v-html is safe here because all HTML is sanitized with DOMPurify in setup() --> <!-- eslint-disable-next-line vue/no-v-html is safe here because all HTML is sanitized with DOMPurify in setup() -->
<div v-html="value" /> <div v-html="value" />
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@@ -29,20 +29,20 @@ interface PageState {
editMode: ComputedRef<EditorMode>; editMode: ComputedRef<EditorMode>;
/** /**
* true is the page is in edit mode and the edit mode is in form mode. * true is the page is in edit mode and the edit mode is in form mode.
*/ */
isEditForm: ComputedRef<boolean>; isEditForm: ComputedRef<boolean>;
/** /**
* true is the page is in edit mode and the edit mode is in json mode. * true is the page is in edit mode and the edit mode is in json mode.
*/ */
isEditJSON: ComputedRef<boolean>; isEditJSON: ComputedRef<boolean>;
/** /**
* true is the page is in view mode. * true is the page is in view mode.
*/ */
isEditMode: ComputedRef<boolean>; isEditMode: ComputedRef<boolean>;
/** /**
* true is the page is in cook mode. * true is the page is in cook mode.
*/ */
isCookMode: ComputedRef<boolean>; isCookMode: ComputedRef<boolean>;
/** /**
* true if the recipe is currently being parsed. * true if the recipe is currently being parsed.

View File

@@ -29,8 +29,8 @@ export function useGroupRecipeActionData() {
} }
export const useGroupRecipeActions = function ( export const useGroupRecipeActions = function (
orderBy: string | null = "title", orderBy: string | null = "title",
orderDirection: string | null = "asc", orderDirection: string | null = "asc",
) { ) {
const api = useUserApi(); const api = useUserApi();

View File

@@ -17,19 +17,19 @@ export interface OrganizerBase {
name: string; name: string;
} }
export type FieldType = export type FieldType
| "string" = | "string"
| "number" | "number"
| "boolean" | "boolean"
| "date" | "date"
| RecipeOrganizer; | RecipeOrganizer;
export type FieldValue = export type FieldValue
| string = | string
| number | number
| boolean | boolean
| Date | Date
| Organizer; | Organizer;
export interface SelectableItem { export interface SelectableItem {
label: string; label: string;

View File

@@ -177,8 +177,8 @@ export function useShoppingListItemActions(shoppingListId: string) {
} }
/** /**
* Processes the queue items and returns whether the processing was successful. * Processes the queue items and returns whether the processing was successful.
*/ */
async function processQueueItems( async function processQueueItems(
action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>, action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>,
itemQueueType: ItemQueueType, itemQueueType: ItemQueueType,

View File

@@ -6,21 +6,20 @@ export default withNuxt({
plugins: { plugins: {
"@stylistic": stylistic, "@stylistic": stylistic,
}, },
// Your custom configs here
rules: { rules: {
"@typescript-eslint/no-explicit-any": "off", "@stylistic/no-tabs": ["error"],
"vue/no-mutating-props": "warn",
"vue/no-v-html": "warn",
"object-curly-newline": "off",
"consistent-list-newline": "off",
"vue/first-attribute-linebreak": "off",
"@stylistic/no-tabs": ["error", { allowIndentationTabs: true }],
"@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"], "@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"vue/max-attributes-per-line": "off", "@typescript-eslint/no-explicit-any": "off",
"vue/html-indent": "off", "vue/first-attribute-linebreak": "error",
"vue/html-closing-bracket-newline": "off", "vue/html-closing-bracket-newline": "error",
// TODO: temporarily off to get this PR in without a crazy diff "vue/max-attributes-per-line": [
"@stylistic/indent": "off", "error",
"@stylistic/operator-linebreak": "off", {
singleline: 5,
multiline: 1,
},
],
"vue/no-mutating-props": "error",
"vue/no-v-html": "error",
}, },
}); });

View File

@@ -1,6 +1,7 @@
<template> <template>
<v-app v-if="ready" <v-app
dark v-if="ready"
dark
> >
<v-card-title> <v-card-title>
<slot> <slot>
@@ -14,9 +15,10 @@
<p class="primary--text"> <p class="primary--text">
4 4
</p> </p>
<v-icon color="primary" <v-icon
class="mx-auto mb-0" color="primary"
size="200" class="mx-auto mb-0"
size="200"
> >
{{ $globals.icons.primary }} {{ $globals.icons.primary }}
</v-icon> </v-icon>
@@ -28,11 +30,12 @@
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<slot name="actions"> <slot name="actions">
<v-btn v-for="(button, index) in buttons" <v-btn
:key="index" v-for="(button, index) in buttons"
nuxt :key="index"
:to="button.to" nuxt
color="primary" :to="button.to"
color="primary"
> >
<v-icon start> <v-icon start>
{{ button.icon }} {{ button.icon }}
@@ -121,9 +124,9 @@ export default defineNuxtComponent({
useSeoMeta({ useSeoMeta({
title: title:
props.error.statusCode === 404 props.error.statusCode === 404
? (i18n.t("page.404-not-found") as string) ? (i18n.t("page.404-not-found") as string)
: (i18n.t("page.an-error-occurred") as string), : (i18n.t("page.an-error-occurred") as string),
}); });
const buttons = [ const buttons = [

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -24,12 +24,12 @@ export interface PaginationData<T> {
items: T[]; items: T[];
} }
export type RecipeOrganizer = export type RecipeOrganizer
| "categories" = | "categories"
| "tags" | "tags"
| "tools" | "tools"
| "foods" | "foods"
| "households"; | "households";
export enum Organizer { export enum Organizer {
Category = "categories", Category = "categories",

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */
/** /**
/* This file was automatically generated from pydantic models by running pydantic2ts. /* 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 /* Do not modify it by hand - just update the pydantic models and then re-run the script

View File

@@ -1,8 +1,10 @@
<template> <template>
<v-container fluid class="narrow-container"> <v-container fluid class="narrow-container">
<BaseDialog v-model="state.storageDetails" :title="$t('admin.maintenance.storage-details')" <BaseDialog
v-model="state.storageDetails"
:title="$t('admin.maintenance.storage-details')"
:icon="$globals.icons.folderOutline" :icon="$globals.icons.folderOutline"
> >
<div class="py-2"> <div class="py-2">
<template v-for="(value, key, idx) in storageDetails" :key="`item-${key}`"> <template v-for="(value, key, idx) in storageDetails" :key="`item-${key}`">
<v-list-item> <v-list-item>
@@ -55,9 +57,11 @@
</v-card> </v-card>
</section> </section>
<section> <section>
<BaseCardSectionTitle class="pb-0 mt-8" :icon="$globals.icons.wrench" <BaseCardSectionTitle
class="pb-0 mt-8"
:icon="$globals.icons.wrench"
:title="$t('admin.mainentance.actions-title')" :title="$t('admin.mainentance.actions-title')"
> >
<i18n-t keypath="admin.maintenance.actions-description"> <i18n-t keypath="admin.maintenance.actions-description">
<template #destructive_in_bold> <template #destructive_in_bold>
<b>{{ $t("admin.maintenance.actions-description-destructive") }}</b> <b>{{ $t("admin.maintenance.actions-description-destructive") }}</b>
@@ -78,11 +82,11 @@
</v-list-item-title> </v-list-item-title>
<template #append> <template #append>
<BaseButton color="info" @click="action.handler"> <BaseButton color="info" @click="action.handler">
<template #icon> <template #icon>
{{ $globals.icons.robot }} {{ $globals.icons.robot }}
</template> </template>
{{ $t("general.run") }} {{ $t("general.run") }}
</BaseButton> </BaseButton>
</template> </template>
</v-list-item> </v-list-item>
<v-divider class="mx-2" /> <v-divider class="mx-2" />

View File

@@ -18,218 +18,219 @@
</v-toolbar-title> </v-toolbar-title>
</v-toolbar> </v-toolbar>
<!-- Stepper Wizard --> <!-- Stepper Wizard -->
<v-stepper v-model="currentPage" mobile-breakpoint="sm"> <v-stepper v-model="currentPage" mobile-breakpoint="sm">
<v-stepper-header> <v-stepper-header>
<v-stepper-item <v-stepper-item
:value="Pages.LANDING" :value="Pages.LANDING"
:complete="currentPage > Pages.LANDING" :complete="currentPage > Pages.LANDING"
:title="$t('general.start')" :title="$t('general.start')"
/>
<v-divider />
<v-stepper-item
:value="Pages.USER_INFO"
:complete="currentPage > Pages.USER_INFO"
:title="$t('user-registration.account-details')"
/>
<v-divider />
<v-stepper-item
:value="Pages.PAGE_2"
:complete="currentPage > Pages.PAGE_2"
:title="$t('settings.site-settings')"
/>
<v-divider />
<v-stepper-item
:value="Pages.CONFIRM"
:complete="currentPage > Pages.CONFIRM"
:title="$t('admin.maintenance.summary-title')"
/>
<v-divider />
<v-stepper-item
:value="Pages.END"
:complete="currentPage > Pages.END"
:title="$t('admin.setup.setup-complete')"
/>
</v-stepper-header>
<v-progress-linear
v-if="isSubmitting && currentPage === Pages.CONFIRM"
color="primary"
indeterminate
class="mb-2"
/> />
<v-divider />
<v-stepper-window :transition="false" class="stepper-window"> <v-stepper-item
<!-- LANDING --> :value="Pages.USER_INFO"
<v-stepper-window-item :value="Pages.LANDING"> :complete="currentPage > Pages.USER_INFO"
<v-container class="mb-12"> :title="$t('user-registration.account-details')"
<AppLogo />
<v-card-title class="text-h4 justify-center text-center text-break text-pre-wrap">
{{ $t('admin.setup.welcome-to-mealie-get-started') }}
</v-card-title>
<v-btn
:to="groupSlug ? `/g/${groupSlug}` : '/login'"
rounded
variant="outlined"
color="grey-lighten-1"
class="text-subtitle-2 d-flex mx-auto"
style="width: fit-content;"
>
{{ $t('admin.setup.already-set-up-bring-to-homepage') }}
</v-btn>
</v-container>
<v-card-actions class="justify-center flex-column py-8">
<BaseButton
size="large"
color="primary"
:icon="$globals.icons.translate"
@click="langDialog = true"
>
{{ $t('language-dialog.choose-language') }}
</BaseButton>
</v-card-actions>
<v-stepper-actions
class="justify-end"
:disabled="isSubmitting"
next-text="general.next"
@click:next="onNext"
>
<template #prev />
</v-stepper-actions>
</v-stepper-window-item>
<!-- USER INFO -->
<v-stepper-window-item :value="Pages.USER_INFO" eager>
<v-container max-width="880">
<UserRegistrationForm />
</v-container>
<v-stepper-actions
:disabled="isSubmitting"
prev-text="general.back"
next-text="general.next"
@click:prev="onPrev"
@click:next="onNext"
/> />
</v-stepper-window-item> <v-divider />
<v-stepper-item
:value="Pages.PAGE_2"
:complete="currentPage > Pages.PAGE_2"
:title="$t('settings.site-settings')"
/>
<v-divider />
<v-stepper-item
:value="Pages.CONFIRM"
:complete="currentPage > Pages.CONFIRM"
:title="$t('admin.maintenance.summary-title')"
/>
<v-divider />
<v-stepper-item
:value="Pages.END"
:complete="currentPage > Pages.END"
:title="$t('admin.setup.setup-complete')"
/>
</v-stepper-header>
<v-progress-linear
v-if="isSubmitting && currentPage === Pages.CONFIRM"
color="primary"
indeterminate
class="mb-2"
/>
<!-- COMMON SETTINGS --> <v-stepper-window :transition="false" class="stepper-window">
<v-stepper-window-item :value="Pages.PAGE_2"> <!-- LANDING -->
<v-container max-width="880"> <v-stepper-window-item :value="Pages.LANDING">
<v-card-title class="headline pa-0"> <v-container class="mb-12">
{{ $t('admin.setup.common-settings-for-new-sites') }} <AppLogo />
</v-card-title> <v-card-title class="text-h4 justify-center text-center text-break text-pre-wrap">
<AutoForm {{ $t('admin.setup.welcome-to-mealie-get-started') }}
v-model="commonSettings" </v-card-title>
:items="commonSettingsForm" <v-btn
:to="groupSlug ? `/g/${groupSlug}` : '/login'"
rounded
variant="outlined"
color="grey-lighten-1"
class="text-subtitle-2 d-flex mx-auto"
style="width: fit-content;"
>
{{ $t('admin.setup.already-set-up-bring-to-homepage') }}
</v-btn>
</v-container>
<v-card-actions class="justify-center flex-column py-8">
<BaseButton
size="large"
color="primary"
:icon="$globals.icons.translate"
@click="langDialog = true"
>
{{ $t('language-dialog.choose-language') }}
</BaseButton>
</v-card-actions>
<v-stepper-actions
class="justify-end"
:disabled="isSubmitting"
next-text="general.next"
@click:next="onNext"
>
<template #prev />
</v-stepper-actions>
</v-stepper-window-item>
<!-- USER INFO -->
<v-stepper-window-item :value="Pages.USER_INFO" eager>
<v-container max-width="880">
<UserRegistrationForm />
</v-container>
<v-stepper-actions
:disabled="isSubmitting"
prev-text="general.back"
next-text="general.next"
@click:prev="onPrev"
@click:next="onNext"
/> />
</v-container> </v-stepper-window-item>
<v-stepper-actions
:disabled="isSubmitting"
prev-text="general.back"
next-text="general.next"
@click:prev="onPrev"
@click:next="onNext"
/>
</v-stepper-window-item>
<!-- CONFIRMATION --> <!-- COMMON SETTINGS -->
<v-stepper-window-item :value="Pages.CONFIRM"> <v-stepper-window-item :value="Pages.PAGE_2">
<v-container max-width="880"> <v-container max-width="880">
<v-card-title class="headline pa-0"> <v-card-title class="headline pa-0">
{{ $t('general.confirm-how-does-everything-look') }} {{ $t('admin.setup.common-settings-for-new-sites') }}
</v-card-title> </v-card-title>
<v-list> <AutoForm
<template v-for="(item, idx) in confirmationData"> v-model="commonSettings"
<v-list-item :items="commonSettingsForm"
v-if="item.display" />
:key="idx" </v-container>
class="px-0" <v-stepper-actions
> :disabled="isSubmitting"
<v-list-item-title>{{ item.text }}</v-list-item-title> prev-text="general.back"
<v-list-item-subtitle>{{ item.value }}</v-list-item-subtitle> next-text="general.next"
</v-list-item> @click:prev="onPrev"
<v-divider @click:next="onNext"
v-if="idx !== confirmationData.length - 1" />
:key="`divider-${idx}`" </v-stepper-window-item>
<!-- CONFIRMATION -->
<v-stepper-window-item :value="Pages.CONFIRM">
<v-container max-width="880">
<v-card-title class="headline pa-0">
{{ $t('general.confirm-how-does-everything-look') }}
</v-card-title>
<v-list>
<template v-for="(item, idx) in confirmationData">
<v-list-item
v-if="item.display"
:key="idx"
class="px-0"
>
<v-list-item-title>{{ item.text }}</v-list-item-title>
<v-list-item-subtitle>{{ item.value }}</v-list-item-subtitle>
</v-list-item>
<v-divider
v-if="idx !== confirmationData.length - 1"
:key="`divider-${idx}`"
/>
</template>
</v-list>
</v-container>
<v-stepper-actions
:disabled="isSubmitting"
prev-text="general.back"
@click:prev="onPrev"
>
<template #next>
<BaseButton
create
flat
:disabled="isSubmitting"
:loading="isSubmitting"
:icon="$globals.icons.check"
:text="$t('general.submit')"
@click="onNext"
/> />
</template> </template>
</v-list> </v-stepper-actions>
</v-container> </v-stepper-window-item>
<v-stepper-actions
:disabled="isSubmitting"
prev-text="general.back"
@click:prev="onPrev"
>
<template #next>
<BaseButton
create flat
:disabled="isSubmitting"
:loading="isSubmitting"
:icon="$globals.icons.check"
:text="$t('general.submit')"
@click="onNext"
/>
</template>
</v-stepper-actions>
</v-stepper-window-item>
<!-- END --> <!-- END -->
<v-stepper-window-item :value="Pages.END"> <v-stepper-window-item :value="Pages.END">
<v-container max-width="880"> <v-container max-width="880">
<v-card-title class="text-h4 justify-center"> <v-card-title class="text-h4 justify-center">
{{ $t('admin.setup.setup-complete') }} {{ $t('admin.setup.setup-complete') }}
</v-card-title> </v-card-title>
<v-card-title class="text-h6 justify-center"> <v-card-title class="text-h6 justify-center">
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }} {{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
</v-card-title> </v-card-title>
<div <div
v-for="link, idx in setupCompleteLinks" v-for="link, idx in setupCompleteLinks"
:key="idx" :key="idx"
class="px-4 pt-4" class="px-4 pt-4"
> >
<div v-if="link.section"> <div v-if="link.section">
<v-divider v-if="idx" /> <v-divider v-if="idx" />
<v-card-text class="headline pl-0"> <v-card-text class="headline pl-0">
{{ link.section }} {{ link.section }}
</v-card-text>
</div>
<v-btn
:to="link.to"
color="info"
>
{{ link.text }}
</v-btn>
<v-card-text class="subtitle px-0 py-2">
{{ link.description }}
</v-card-text> </v-card-text>
</div> </div>
<v-btn </v-container>
:to="link.to" <v-stepper-actions
color="info" :disabled="isSubmitting"
> prev-text="general.back"
{{ link.text }} @click:prev="onPrev"
</v-btn> >
<v-card-text class="subtitle px-0 py-2"> <template #next>
{{ link.description }} <BaseButton
</v-card-text> flat
</div> color="primary"
</v-container> :disabled="isSubmitting"
<v-stepper-actions :loading="isSubmitting"
:disabled="isSubmitting" :icon="$globals.icons.home"
prev-text="general.back" :text="$t('general.home')"
@click:prev="onPrev" @click="onFinish"
> />
<template #next> </template>
<BaseButton </v-stepper-actions>
flat </v-stepper-window-item>
color="primary" </v-stepper-window>
:disabled="isSubmitting" </v-stepper>
:loading="isSubmitting"
:icon="$globals.icons.home"
:text="$t('general.home')"
@click="onFinish"
/>
</template>
</v-stepper-actions>
</v-stepper-window-item>
</v-stepper-window>
</v-stepper>
<!-- Dialog Language --> <!-- Dialog Language -->
<LanguageDialog v-model="langDialog" /> <LanguageDialog v-model="langDialog" />
</v-card> </v-card>
</v-container> </v-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -7,151 +7,151 @@
<v-card-text> <v-card-text>
{{ $t('recipe.recipe-bulk-importer-description') }} {{ $t('recipe.recipe-bulk-importer-description') }}
</v-card-text> </v-card-text>
<div class="px-4"> <div class="px-4">
<section class="mt-2"> <section class="mt-2">
<v-row <v-row
v-for="(_, idx) in bulkUrls" v-for="(_, idx) in bulkUrls"
:key="'bulk-url' + idx" :key="'bulk-url' + idx"
class="my-1" class="my-1"
density="compact"
>
<v-col
cols="12"
xs="12"
sm="12"
md="12"
>
<v-text-field
v-model="bulkUrls[idx].url"
:label="$t('new-recipe.recipe-url')"
density="compact" density="compact"
single-line
validate-on="blur"
autofocus
variant="solo-filled"
hide-details
clearable
:prepend-inner-icon="$globals.icons.link"
rounded
class="rounded-lg"
> >
<template #append> <v-col
<v-btn cols="12"
style="margin-top: -2px" xs="12"
icon sm="12"
size="small" md="12"
@click="bulkUrls.splice(idx, 1)" >
<v-text-field
v-model="bulkUrls[idx].url"
:label="$t('new-recipe.recipe-url')"
density="compact"
single-line
validate-on="blur"
autofocus
variant="solo-filled"
hide-details
clearable
:prepend-inner-icon="$globals.icons.link"
rounded
class="rounded-lg"
> >
<v-icon> <template #append>
{{ $globals.icons.delete }} <v-btn
</v-icon> style="margin-top: -2px"
</v-btn> icon
size="small"
@click="bulkUrls.splice(idx, 1)"
>
<v-icon>
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
</template>
</v-text-field>
</v-col>
<template v-if="showCatTags">
<v-col
cols="12"
xs="12"
sm="6"
class="py-0"
>
<RecipeOrganizerSelector
v-model="bulkUrls[idx].categories"
selector-type="categories"
:input-attrs="{
variant: 'filled',
singleLine: true,
density: 'compact',
rounded: true,
class: 'rounded-lg',
hideDetails: true,
clearable: true,
}"
/>
</v-col>
<v-col
cols="12"
xs="12"
sm="6"
class="pt-0 pb-4"
>
<RecipeOrganizerSelector
v-model="bulkUrls[idx].tags"
selector-type="tags"
:input-attrs="{
variant: 'filled',
singleLine: true,
density: 'compact',
rounded: true,
class: 'rounded-lg',
hideDetails: true,
clearable: true,
}"
/>
</v-col>
</template> </template>
</v-text-field> </v-row>
</v-col> <v-card-actions class="justify-end flex-wrap mt-3 pa-0">
<template v-if="showCatTags"> <BaseButton
<v-col class="mt-1 pr-4"
cols="12" delete
xs="12" @click="
sm="6" bulkUrls = [];
class="py-0" lockBulkImport = false;
> "
<RecipeOrganizerSelector >
v-model="bulkUrls[idx].categories" {{ $t('general.clear') }}
selector-type="categories" </BaseButton>
:input-attrs="{ <v-spacer />
variant: 'filled', <BaseButton
singleLine: true, class="mr-1 mb-1"
density: 'compact', color="info"
rounded: true, @click="bulkUrls.push({ url: '', categories: [], tags: [] })"
class: 'rounded-lg', >
hideDetails: true, <template #icon>
clearable: true, {{ $globals.icons.createAlt }}
}" </template>
{{ $t('general.new') }}
</BaseButton>
<RecipeDialogBulkAdd
v-model="bulkDialog"
class="mr-1 mr-sm-0 mb-1"
@bulk-data="assignUrls"
/> />
</v-col> </v-card-actions>
<v-col <div class="px-0">
cols="12" <v-checkbox
xs="12" v-model="showCatTags"
sm="6" hide-details
class="pt-0 pb-4" :label="$t('recipe.set-categories-and-tags')"
>
<RecipeOrganizerSelector
v-model="bulkUrls[idx].tags"
selector-type="tags"
:input-attrs="{
variant: 'filled',
singleLine: true,
density: 'compact',
rounded: true,
class: 'rounded-lg',
hideDetails: true,
clearable: true,
}"
/> />
</v-col> </div>
</template> <v-card-actions class="justify-center">
</v-row> <div style="width: 250px">
<v-card-actions class="justify-end flex-wrap mt-3 pa-0"> <BaseButton
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport"
class="mt-1 pr-4" rounded
delete block
@click=" @click="bulkCreate"
bulkUrls = []; >
lockBulkImport = false; <template #icon>
" {{ $globals.icons.check }}
> </template>
{{ $t('general.clear') }} </BaseButton>
</BaseButton> </div>
<v-spacer /> </v-card-actions>
<BaseButton </section>
class="mr-1 mb-1" <section class="mt-12">
color="info" <BaseCardSectionTitle :title="$t('recipe.bulk-imports')" />
@click="bulkUrls.push({ url: '', categories: [], tags: [] })" <ReportTable
> :items="reports"
<template #icon> @delete="deleteReport"
{{ $globals.icons.createAlt }} />
</template> </section>
{{ $t('general.new') }}
</BaseButton>
<RecipeDialogBulkAdd
v-model="bulkDialog"
class="mr-1 mr-sm-0 mb-1"
@bulk-data="assignUrls"
/>
</v-card-actions>
<div class="px-0">
<v-checkbox
v-model="showCatTags"
hide-details
:label="$t('recipe.set-categories-and-tags')"
/>
</div> </div>
<v-card-actions class="justify-center"> </div>
<div style="width: 250px">
<BaseButton
:disabled="bulkUrls.length === 0 || lockBulkImport"
rounded
block
@click="bulkCreate"
>
<template #icon>
{{ $globals.icons.check }}
</template>
</BaseButton>
</div>
</v-card-actions>
</section>
<section class="mt-12">
<BaseCardSectionTitle :title="$t('recipe.bulk-imports')" />
<ReportTable
:items="reports"
@delete="deleteReport"
/>
</section>
</div> </div>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@@ -1,6 +1,7 @@
<template> <template>
<v-container v-if="household" <v-container
class="narrow-container" v-if="household"
class="narrow-container"
> >
<BasePageTitle class="mb-5"> <BasePageTitle class="mb-5">
<template #header> <template #header>
@@ -53,105 +54,105 @@ export default defineNuxtComponent({
const refHouseholdEditForm = ref<VForm | null>(null); const refHouseholdEditForm = ref<VForm | null>(null);
type Preference = { type Preference = {
key: keyof ReadHouseholdPreferences; key: keyof ReadHouseholdPreferences;
value: boolean; value: boolean;
label: string; label: string;
description: string; description: string;
}; };
const preferencesEditor = computed<Preference[]>(() => { const preferencesEditor = computed<Preference[]>(() => {
if (!household.value || !household.value.preferences) { if (!household.value || !household.value.preferences) {
return []; return [];
} }
return [ return [
{ {
key: "recipePublic", key: "recipePublic",
value: household.value.preferences.recipePublic || false, value: household.value.preferences.recipePublic || false,
label: i18n.t("household.allow-users-outside-of-your-household-to-see-your-recipes"), 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"), description: i18n.t("household.allow-users-outside-of-your-household-to-see-your-recipes-description"),
} as Preference, } as Preference,
{ {
key: "recipeShowNutrition", key: "recipeShowNutrition",
value: household.value.preferences.recipeShowNutrition || false, value: household.value.preferences.recipeShowNutrition || false,
label: i18n.t("group.show-nutrition-information"), label: i18n.t("group.show-nutrition-information"),
description: i18n.t("group.show-nutrition-information-description"), description: i18n.t("group.show-nutrition-information-description"),
} as Preference, } as Preference,
{ {
key: "recipeShowAssets", key: "recipeShowAssets",
value: household.value.preferences.recipeShowAssets || false, value: household.value.preferences.recipeShowAssets || false,
label: i18n.t("group.show-recipe-assets"), label: i18n.t("group.show-recipe-assets"),
description: i18n.t("group.show-recipe-assets-description"), description: i18n.t("group.show-recipe-assets-description"),
} as Preference, } as Preference,
{ {
key: "recipeLandscapeView", key: "recipeLandscapeView",
value: household.value.preferences.recipeLandscapeView || false, value: household.value.preferences.recipeLandscapeView || false,
label: i18n.t("group.default-to-landscape-view"), label: i18n.t("group.default-to-landscape-view"),
description: i18n.t("group.default-to-landscape-view-description"), description: i18n.t("group.default-to-landscape-view-description"),
} as Preference, } as Preference,
{ {
key: "recipeDisableComments", key: "recipeDisableComments",
value: household.value.preferences.recipeDisableComments || false, value: household.value.preferences.recipeDisableComments || false,
label: i18n.t("group.disable-users-from-commenting-on-recipes"), label: i18n.t("group.disable-users-from-commenting-on-recipes"),
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"), description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
} as Preference, } as Preference,
]; ];
}); });
const allDays = [ const allDays = [
{ {
name: i18n.t("general.sunday"), name: i18n.t("general.sunday"),
value: 0, value: 0,
}, },
{ {
name: i18n.t("general.monday"), name: i18n.t("general.monday"),
value: 1, value: 1,
}, },
{ {
name: i18n.t("general.tuesday"), name: i18n.t("general.tuesday"),
value: 2, value: 2,
}, },
{ {
name: i18n.t("general.wednesday"), name: i18n.t("general.wednesday"),
value: 3, value: 3,
}, },
{ {
name: i18n.t("general.thursday"), name: i18n.t("general.thursday"),
value: 4, value: 4,
}, },
{ {
name: i18n.t("general.friday"), name: i18n.t("general.friday"),
value: 5, value: 5,
}, },
{ {
name: i18n.t("general.saturday"), name: i18n.t("general.saturday"),
value: 6, value: 6,
}, },
]; ];
async function handleSubmit() { async function handleSubmit() {
if (!refHouseholdEditForm.value?.validate() || !household.value?.preferences) { if (!refHouseholdEditForm.value?.validate() || !household.value?.preferences) {
console.log(refHouseholdEditForm.value?.validate()); console.log(refHouseholdEditForm.value?.validate());
return; return;
} }
const data = await householdActions.updatePreferences(); const data = await householdActions.updatePreferences();
if (data) { if (data) {
alert.success(i18n.t("settings.settings-updated")); alert.success(i18n.t("settings.settings-updated"));
} }
else { else {
alert.error(i18n.t("settings.settings-update-failed")); alert.error(i18n.t("settings.settings-update-failed"));
} }
} }
return { return {
household, household,
householdActions, householdActions,
allDays, allDays,
preferencesEditor, preferencesEditor,
refHouseholdEditForm, refHouseholdEditForm,
handleSubmit, handleSubmit,
}; };
}, },
}); });
</script> </script>

View File

@@ -14,7 +14,7 @@
if (newMeal.existing) { if (newMeal.existing) {
actions.updateOne({ ...newMeal, date: newMealDateString }); actions.updateOne({ ...newMeal, date: newMealDateString });
} }
else { else {
actions.createOne({ ...newMeal, date: newMealDateString }); actions.createOne({ ...newMeal, date: newMealDateString });
} }
resetDialog(); resetDialog();
@@ -147,7 +147,14 @@
</v-btn> </v-btn>
<v-menu offset-y> <v-menu offset-y>
<template #activator="{ props }"> <template #activator="{ props }">
<v-chip v-bind="props" label variant="elevated" size="small" color="accent" @click.prevent> <v-chip
v-bind="props"
label
variant="elevated"
size="small"
color="accent"
@click.prevent
>
<v-icon start> <v-icon start>
{{ $globals.icons.tags }} {{ $globals.icons.tags }}
</v-icon> </v-icon>

View File

@@ -1,8 +1,16 @@
<template> <template>
<v-container class="mx-0 my-3 pa"> <v-container class="mx-0 my-3 pa">
<v-row> <v-row>
<v-col v-for="(day, index) in plan" :key="index" cols="12" sm="12" md="4" lg="4" xl="2" <v-col
class="col-borders my-1 d-flex flex-column"> v-for="(day, index) in plan"
:key="index"
cols="12"
sm="12"
md="4"
lg="4"
xl="2"
class="col-borders my-1 d-flex flex-column"
>
<v-card class="mb-2 border-left-primary rounded-sm px-2"> <v-card class="mb-2 border-left-primary rounded-sm px-2">
<v-container class="px-0 d-flex align-center" height="56px"> <v-container class="px-0 d-flex align-center" height="56px">
<v-row no-gutters style="width: 100%;"> <v-row no-gutters style="width: 100%;">
@@ -25,13 +33,17 @@
</p> </p>
</div> </div>
<RecipeCardMobile v-for="mealplan in section.meals" :key="mealplan.id" <RecipeCardMobile
:recipe-id="mealplan.recipe ? mealplan.recipe.id! : ''" class="mb-2" v-for="mealplan in section.meals"
:key="mealplan.id"
:recipe-id="mealplan.recipe ? mealplan.recipe.id! : ''"
class="mb-2"
:rating="mealplan.recipe ? mealplan.recipe.rating! : 0" :rating="mealplan.recipe ? mealplan.recipe.rating! : 0"
:slug="mealplan.recipe ? mealplan.recipe.slug! : mealplan.title!" :slug="mealplan.recipe ? mealplan.recipe.slug! : mealplan.title!"
:description="mealplan.recipe ? mealplan.recipe.description! : mealplan.text!" :description="mealplan.recipe ? mealplan.recipe.description! : mealplan.text!"
:name="mealplan.recipe ? mealplan.recipe.name! : mealplan.title!" :name="mealplan.recipe ? mealplan.recipe.name! : mealplan.title!"
:tags="mealplan.recipe ? mealplan.recipe.tags! : []" /> :tags="mealplan.recipe ? mealplan.recipe.tags! : []"
/>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>

View File

@@ -92,32 +92,32 @@
class="my-2 left-border" class="my-2 left-border"
:to="`/shopping-lists/${list.id}`" :to="`/shopping-lists/${list.id}`"
> >
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon class="mr-2"> <v-icon class="mr-2">
{{ $globals.icons.cartCheck }} {{ $globals.icons.cartCheck }}
</v-icon>
<span class="flex-grow-1">
{{ list.name }}
</span>
<v-btn
icon
variant="plain"
@click.prevent="toggleOwnerDialog(list)"
>
<v-icon>
{{ $globals.icons.user }}
</v-icon> </v-icon>
</v-btn> <span class="flex-grow-1">
<v-btn {{ list.name }}
icon </span>
variant="plain" <v-btn
@click.prevent="openDelete(list.id)" icon
> variant="plain"
<v-icon> @click.prevent="toggleOwnerDialog(list)"
{{ $globals.icons.delete }} >
</v-icon> <v-icon>
</v-btn> {{ $globals.icons.user }}
</v-card-title> </v-icon>
</v-btn>
<v-btn
icon
variant="plain"
@click.prevent="openDelete(list.id)"
>
<v-icon>
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
</v-card-title>
</v-card> </v-card>
</section> </section>
</v-container> </v-container>

View File

@@ -80,10 +80,10 @@
/> />
<section class="d-flex flex-column"> <section class="d-flex flex-column">
<v-list> <v-list>
<div <div
v-for="(token, index) in user.tokens" v-for="(token, index) in user.tokens"
:key="index" :key="index"
> >
<v-list-item> <v-list-item>
<v-list-item-title> <v-list-item-title>
{{ token.name }} {{ token.name }}

View File

@@ -1,11 +1,11 @@
import type { UserOut } from "~/lib/api/types/user"; import type { UserOut } from "~/lib/api/types/user";
declare module "#auth-utils" { declare module "#auth-utils" {
type User = UserOut; type User = UserOut;
type UserSession = object; type UserSession = object;
type SecureSessionData = object; type SecureSessionData = object;
} }
export { }; export { };