mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-20 20:05:37 -04:00
Compare commits
105 Commits
v3.14.0
...
mealie-nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83bc2f3889 | ||
|
|
0ffc1e7bf7 | ||
|
|
e166baa33c | ||
|
|
3e25005ea6 | ||
|
|
c92ebf2099 | ||
|
|
8e429834af | ||
|
|
2ca5694391 | ||
|
|
8d8987ab05 | ||
|
|
372474ea2b | ||
|
|
5b93129368 | ||
|
|
ffeb4dceaf | ||
|
|
5fc4851ef5 | ||
|
|
d9e933d5ae | ||
|
|
0a07835338 | ||
|
|
7a85ea6ae9 | ||
|
|
c4c60f1645 | ||
|
|
9f7ba8dc08 | ||
|
|
c4799ceb9e | ||
|
|
828be095a2 | ||
|
|
18718fb647 | ||
|
|
fb545962dd | ||
|
|
781a08ef54 | ||
|
|
a7a08b6b11 | ||
|
|
bd296c3eaf | ||
|
|
8aa016e57b | ||
|
|
480574eb3d | ||
|
|
0573d6fc9c | ||
|
|
f8d08c6785 | ||
|
|
e6368174f0 | ||
|
|
2252875050 | ||
|
|
54c62ec491 | ||
|
|
af79a751fb | ||
|
|
6e2c849412 | ||
|
|
76dbf4df45 | ||
|
|
4e5a2f9fb5 | ||
|
|
daa0b9728b | ||
|
|
0986ce2ca1 | ||
|
|
4972143004 | ||
|
|
499c42a52a | ||
|
|
92cf84f615 | ||
|
|
54511779a2 | ||
|
|
b72ccb8d29 | ||
|
|
9fb3bce792 | ||
|
|
32141187ba | ||
|
|
30014f53de | ||
|
|
d2b0681dbb | ||
|
|
306f2dcfc6 | ||
|
|
0fb5d31a22 | ||
|
|
1d5b263262 | ||
|
|
731e3aef37 | ||
|
|
fb04602a8e | ||
|
|
157b8d2937 | ||
|
|
6b28bb8eb0 | ||
|
|
124d10963e | ||
|
|
7c2ec93d13 | ||
|
|
d3e41582ae | ||
|
|
70a251a331 | ||
|
|
4fd224ade7 | ||
|
|
89694f7e54 | ||
|
|
7a60ad2227 | ||
|
|
eb71b962bc | ||
|
|
fe491bbe56 | ||
|
|
27f2dc1bf6 | ||
|
|
b3ea916192 | ||
|
|
240d681057 | ||
|
|
6932c9ef2d | ||
|
|
1438ba82d5 | ||
|
|
7a5032bf23 | ||
|
|
c3d1cf4c37 | ||
|
|
135a9ca684 | ||
|
|
ef90515ae8 | ||
|
|
a853e445ac | ||
|
|
7dad3777d3 | ||
|
|
a6ab0befba | ||
|
|
2c6997a601 | ||
|
|
9c3b94c019 | ||
|
|
5ce3099cfa | ||
|
|
0d1349cc7f | ||
|
|
7e7d1622dd | ||
|
|
d24aa7f65a | ||
|
|
5172571b2e | ||
|
|
bb278aac35 | ||
|
|
4ee97e5348 | ||
|
|
bac00a30a4 | ||
|
|
1123ec848d | ||
|
|
0f767f2e25 | ||
|
|
058dbdc9d6 | ||
|
|
94cf825a28 | ||
|
|
6ee69b7b3e | ||
|
|
f36c892bb7 | ||
|
|
f6305b785e | ||
|
|
1512a9e555 | ||
|
|
690b6aa57b | ||
|
|
c57af78f8f | ||
|
|
0775156aeb | ||
|
|
3356ebc0b8 | ||
|
|
1b59073dc4 | ||
|
|
ea3856b620 | ||
|
|
4f5d1cf1b4 | ||
|
|
626dee9500 | ||
|
|
1162c700cd | ||
|
|
7b3651d138 | ||
|
|
1a3676c36d | ||
|
|
17d9be3b15 | ||
|
|
7a8a511d48 |
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
Mealie is a self-hosted recipe manager, meal planner, and shopping list application with a FastAPI backend (Python 3.12) and Nuxt 3 frontend (Vue 3 + TypeScript). It uses SQLAlchemy ORM with support for SQLite and PostgreSQL databases.
|
||||
Mealie is a self-hosted recipe manager, meal planner, and shopping list application with a FastAPI backend (Python 3.12) and Nuxt 4 frontend (Vue 3 + TypeScript). It uses SQLAlchemy ORM with support for SQLite and PostgreSQL databases.
|
||||
|
||||
**Development vs Production:**
|
||||
- **Development:** Frontend (port 3000) and backend (port 9000) run as separate processes
|
||||
@@ -28,7 +28,7 @@ Mealie is a self-hosted recipe manager, meal planner, and shopping list applicat
|
||||
**Schemas & Type Generation:**
|
||||
- Pydantic schemas in `mealie/schema/` with strict separation: `*In`, `*Out`, `*Create`, `*Update` suffixes
|
||||
- Auto-exported from submodules via `__init__.py` files (generated by `task dev:generate`)
|
||||
- TypeScript types auto-generated from Pydantic schemas - **never manually edit** `frontend/lib/api/types/`
|
||||
- TypeScript types auto-generated from Pydantic schemas - **never manually edit** `frontend/app/lib/api/types/`
|
||||
|
||||
**Database & Sessions:**
|
||||
- Session management via `Depends(generate_session)` in FastAPI routes
|
||||
@@ -45,13 +45,13 @@ Mealie is a self-hosted recipe manager, meal planner, and shopping list applicat
|
||||
- **Page Components** (`components/` with page prefix): Last resort for breaking up complex pages
|
||||
|
||||
**API Client Pattern:**
|
||||
- API clients in `frontend/lib/api/` extend `BaseAPI`, `BaseCRUDAPI`, or `BaseCRUDAPIReadOnly`
|
||||
- Types imported from auto-generated `frontend/lib/api/types/` (DO NOT EDIT MANUALLY)
|
||||
- Composables in `frontend/composables/` for shared state and API logic (e.g., `use-mealie-auth.ts`)
|
||||
- API clients in `frontend/app/lib/api/` extend `BaseAPI`, `BaseCRUDAPI`, or `BaseCRUDAPIReadOnly`
|
||||
- Types imported from auto-generated `frontend/app/lib/api/types/` (DO NOT EDIT MANUALLY)
|
||||
- Composables in `frontend/app/composables/` for shared state and API logic (e.g., `use-mealie-auth.ts`)
|
||||
- Use `useAuthBackend()` for authentication state, `useMealieAuth()` for user management
|
||||
|
||||
**State Management:**
|
||||
- Nuxt 3 composables for state (no Vuex)
|
||||
- Nuxt 4 composables for state (no Vuex)
|
||||
- Auth state via `use-mealie-auth.ts` composable
|
||||
- Prefer composables over global state stores
|
||||
|
||||
@@ -148,7 +148,7 @@ task docker:prod # Build and run production Docker compose
|
||||
### Cross-Cutting Concerns
|
||||
|
||||
1. **Code generation is source of truth:** After Pydantic schema changes, run `task dev:generate` to update:
|
||||
- TypeScript types (`frontend/lib/api/types/`)
|
||||
- TypeScript types (`frontend/app/lib/api/types/`)
|
||||
- Schema exports (`mealie/schema/*/__init__.py`)
|
||||
- Test data paths and routes
|
||||
|
||||
@@ -189,7 +189,7 @@ task docker:prod # Build and run production Docker compose
|
||||
- For frontend, does TypeScript code pass strict type checking?
|
||||
|
||||
**Generated Files:**
|
||||
- Verify `frontend/lib/api/types/` files weren't manually edited (they're auto-generated)
|
||||
- Verify `frontend/app/lib/api/types/` files weren't manually edited (they're auto-generated)
|
||||
- Check that `mealie/schema/*/__init__.py` exports match actual schema files (auto-generated)
|
||||
- If schemas changed, confirm generated files were updated via `task dev:generate`
|
||||
|
||||
@@ -216,7 +216,7 @@ task docker:prod # Build and run production Docker compose
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
- **Don't manually edit generated files:** `frontend/lib/api/types/`, schema `__init__.py` files
|
||||
- **Don't manually edit generated files:** `frontend/app/lib/api/types/`, schema `__init__.py` files
|
||||
- **Repository context:** Repos are group/household-scoped - passing wrong IDs causes 404s
|
||||
- **Session handling:** Don't create sessions manually, use dependency injection or `session_context()`
|
||||
- **Schema changes require codegen:** After changing Pydantic models, run `task dev:generate`
|
||||
@@ -229,7 +229,7 @@ task docker:prod # Build and run production Docker compose
|
||||
- `Taskfile.yml` - All development commands and workflows
|
||||
- `mealie/routes/_base/base_controllers.py` - Controller base classes and patterns
|
||||
- `mealie/repos/repository_factory.py` - Repository factory and available repos
|
||||
- `frontend/lib/api/base/base-clients.ts` - API client base classes
|
||||
- `frontend/app/lib/api/base/base-clients.ts` - API client base classes
|
||||
- `tests/conftest.py` - Test fixtures and setup
|
||||
- `dev/code-generation/main.py` - Code generation entry point
|
||||
|
||||
|
||||
8
.github/workflows/auto-merge-l10n.yml
vendored
8
.github/workflows/auto-merge-l10n.yml
vendored
@@ -55,8 +55,8 @@ jobs:
|
||||
|
||||
for file in $FILES; do
|
||||
# Check if file matches any allowed path
|
||||
if [[ "$file" == "frontend/composables/use-locales/available-locales.ts" ]] || \
|
||||
[[ "$file" =~ ^frontend/lang/ ]] || \
|
||||
if [[ "$file" == "frontend/app/composables/use-locales/available-locales.ts" ]] || \
|
||||
[[ "$file" =~ ^frontend/app/lang/ ]] || \
|
||||
[[ "$file" =~ ^mealie/lang/ ]] || \
|
||||
[[ "$file" =~ ^mealie/repos/seed/resources/[^/]+/locales/ ]]; then
|
||||
continue
|
||||
@@ -65,8 +65,8 @@ jobs:
|
||||
# File doesn't match allowed paths
|
||||
echo "::error::Invalid file path: $file"
|
||||
echo "Only the following paths are allowed:"
|
||||
echo " - frontend/composables/use-locales/available-locales.ts"
|
||||
echo " - frontend/lang/"
|
||||
echo " - frontend/app/composables/use-locales/available-locales.ts"
|
||||
echo " - frontend/app/lang/"
|
||||
echo " - mealie/lang/"
|
||||
echo " - mealie/repos/seed/resources/*/locales/"
|
||||
exit 1
|
||||
|
||||
16
.github/workflows/build-package.yml
vendored
16
.github/workflows/build-package.yml
vendored
@@ -17,12 +17,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
check-latest: true
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Archive built frontend
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: frontend/dist
|
||||
@@ -68,12 +68,12 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
run: pip install uv
|
||||
|
||||
- name: Retrieve built frontend
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: mealie/frontend
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
task py:package
|
||||
|
||||
- name: Archive built package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: backend-dist
|
||||
path: dist
|
||||
|
||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -44,11 +44,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
10
.github/workflows/e2e.yml
vendored
10
.github/workflows/e2e.yml
vendored
@@ -10,21 +10,21 @@ jobs:
|
||||
run:
|
||||
working-directory: ./tests/e2e
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./tests/e2e/yarn.lock
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Retrieve Python package
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: backend-dist
|
||||
path: dist
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
file: ./docker/Dockerfile
|
||||
context: .
|
||||
|
||||
6
.github/workflows/locale-sync.yml
vendored
6
.github/workflows/locale-sync.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-python-dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build Dockerfile
|
||||
run: |
|
||||
@@ -28,6 +28,6 @@ jobs:
|
||||
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
||||
|
||||
11
.github/workflows/publish.yml
vendored
11
.github/workflows/publish.yml
vendored
@@ -23,19 +23,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Log in to the Container registry (ghcr.io)
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry (dockerhub)
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
hkotel/mealie
|
||||
@@ -52,9 +52,10 @@ jobs:
|
||||
# Overwrite the image.version label with our tag
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ inputs.tag }}
|
||||
org.opencontainers.image.revision=${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Retrieve Python package
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: backend-dist
|
||||
path: dist
|
||||
|
||||
2
.github/workflows/pull-request-lint.yml
vendored
2
.github/workflows/pull-request-lint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# https://github.com/amannn/action-semantic-pull-request
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
28
.github/workflows/release-drafter.yml
vendored
28
.github/workflows/release-drafter.yml
vendored
@@ -5,26 +5,28 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
# pull_request event is required for autolabeler
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, reopened, synchronize]
|
||||
# pull_request_target event is required for autolabeler to support PRs from forks
|
||||
pull_request_target:
|
||||
types: [opened, labeled, unlabeled, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
# write permission is required to create a github release
|
||||
contents: write
|
||||
# write permission is required for autolabeler
|
||||
# otherwise, read permission is required at least
|
||||
pull-requests: write
|
||||
name: ✏️ Draft release
|
||||
draft_release:
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🚀 Run Release Drafter
|
||||
uses: release-drafter/release-drafter@v6.0.0
|
||||
- uses: release-drafter/release-drafter@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
auto_label:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter/autolabeler@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
6
.github/workflows/scheduled-checks.yml
vendored
6
.github/workflows/scheduled-checks.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pre-commit
|
||||
|
||||
6
.github/workflows/test-backend.yml
vendored
6
.github/workflows/test-backend.yml
vendored
@@ -46,12 +46,12 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-python-dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
|
||||
|
||||
6
.github/workflows/test-frontend.yml
vendored
6
.github/workflows/test-frontend.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
check-latest: true
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.7
|
||||
rev: v0.15.8
|
||||
hooks:
|
||||
# Linter
|
||||
- id: ruff-check
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -17,6 +17,8 @@
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.workingDirectories": [
|
||||
@@ -30,11 +32,12 @@
|
||||
"**/.svn": true,
|
||||
"**/CVS": true
|
||||
},
|
||||
"files.insertFinalNewline": true,
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"vue"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": "frontend/lang/messages",
|
||||
"i18n-ally.localesPaths": "frontend/app/lang/messages",
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||
@@ -67,6 +70,7 @@
|
||||
},
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ pull_request_labels: [
|
||||
"l10n"
|
||||
]
|
||||
files:
|
||||
- source: /frontend/lang/messages/en-US.json
|
||||
translation: /frontend/lang/messages/%locale%.json
|
||||
- source: /frontend/app/lang/messages/en-US.json
|
||||
translation: /frontend/app/lang/messages/%locale%.json
|
||||
- source: /mealie/lang/messages/en-US.json
|
||||
translation: /mealie/lang/messages/%locale%.json
|
||||
- source: /mealie/repos/seed/resources/foods/locales/en-US.json
|
||||
|
||||
@@ -84,10 +84,10 @@ class CrowdinApi:
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
|
||||
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
|
||||
datetime_dir = PROJECT_DIR / "frontend" / "app" / "lang" / "dateTimeFormats"
|
||||
locales_dir = PROJECT_DIR / "frontend" / "app" / "lang" / "messages"
|
||||
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.ts"
|
||||
i18n_config = PROJECT_DIR / "frontend" / "i18n.config.ts"
|
||||
i18n_config = PROJECT_DIR / "frontend" / "app" / "i18n.config.ts"
|
||||
reg_valid = PROJECT_DIR / "mealie" / "schema" / "_mealie" / "validators.py"
|
||||
|
||||
"""
|
||||
|
||||
@@ -33,11 +33,11 @@ PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
def generate_global_components_types() -> None:
|
||||
destination_file = PROJECT_DIR / "frontend" / "types" / "components.d.ts"
|
||||
destination_file = PROJECT_DIR / "frontend" / "app" / "types" / "components.d.ts"
|
||||
|
||||
component_paths = {
|
||||
"global": PROJECT_DIR / "frontend" / "components" / "global",
|
||||
"layout": PROJECT_DIR / "frontend" / "components" / "Layout",
|
||||
"global": PROJECT_DIR / "frontend" / "app" / "components" / "global",
|
||||
"layout": PROJECT_DIR / "frontend" / "app" / "components" / "Layout",
|
||||
}
|
||||
|
||||
def render_template(template: str, data: dict) -> str | None:
|
||||
@@ -182,7 +182,7 @@ def generate_typescript_types() -> None: # noqa: C901
|
||||
return str_path
|
||||
|
||||
schema_path = PROJECT_DIR / "mealie" / "schema"
|
||||
types_dir = PROJECT_DIR / "frontend" / "lib" / "api" / "types"
|
||||
types_dir = PROJECT_DIR / "frontend" / "app" / "lib" / "api" / "types"
|
||||
|
||||
ignore_dirs = ["__pycache__", "static", "_mealie"]
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class CodeTemplates:
|
||||
class CodeDest:
|
||||
interface = PARENT / "generated" / "interface.js"
|
||||
pytest_routes = PARENT / "generated" / "test_routes.py"
|
||||
use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts"
|
||||
use_locales = PROJECT_DIR / "frontend" / "app" / "composables" / "use-locales" / "available-locales.ts"
|
||||
|
||||
|
||||
class CodeKeys:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
###############################################
|
||||
# Frontend Build
|
||||
###############################################
|
||||
FROM node:24@sha256:bb20cf73b3ad7212834ec48e2174cdcb5775f6550510a5336b842ae32741ce6c \
|
||||
FROM node:24@sha256:33cf7f057918860b043c307751ef621d74ac96f875b79b6724dcebf2dfd0db6d \
|
||||
AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -77,7 +77,7 @@ Now you're ready to start the servers. You'll need two shells open, One for the
|
||||
|
||||
### Frontend
|
||||
|
||||
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/lang/messages](https://github.com/mealie-recipes/mealie/tree/mealie-next/frontend/lang/messages).
|
||||
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/app/lang/messages](https://github.com/mealie-recipes/mealie/tree/mealie-next/frontend/app/lang/messages).
|
||||
|
||||
### Backend
|
||||
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
|
||||
### Creating Recipes
|
||||
Mealie offers several ways to create recipes:
|
||||
|
||||
- **Recipe Scraper:** Create recipes from hundreds of websites by simply providing a URL.
|
||||
- **Image Import:** Upload an image of a written or typed recipe and Mealie will use OCR to import it.
|
||||
- **Video URL Import:** Provide a video URL (e.g., YouTube) and Mealie will transcribe the audio and parse the recipe.
|
||||
- **Recipe HTML or JSON:** Copy/paste structured HTML or JSON and Mealie can import it.
|
||||
- **Manual Editor:** Create recipes from scratch using the integrated editor.
|
||||
|
||||
Mealie's [AI integration](./installation/open-ai.md) greatly expands the ways you can create recipes:
|
||||
|
||||
- **Image Import:** Upload an image of a written or typed recipe and Mealie will use OCR and AI to import it.
|
||||
- **Video URL Import:** Provide a video URL (e.g., YouTube) and Mealie will transcribe the audio and turn it into a recipe.
|
||||
|
||||
[Creation Demo](https://demo.mealie.io/g/home/r/create/url){ .md-button .md-button--primary .align-right }
|
||||
|
||||
### Importing Recipes
|
||||
|
||||
@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
|
||||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
||||
|
||||
1. Take a backup just in case!
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.14.0`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.16.0`
|
||||
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
||||
4. Restart the container
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.14.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.16.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.14.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.16.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -355,20 +355,20 @@
|
||||
title="github.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z">
|
||||
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6.0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6.0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3.0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1.0-6.2-.3-40.4-.3-61.4.0.0-70 15-84.7-29.8.0.0-11.4-29.1-27.8-36.6.0.0-22.9-15.7 1.6-15.4.0.0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5.0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9.0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4.0 33.7-.3 75.4-.3 83.6.0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6.0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9.0-6.2-1.4-2.3-4-3.3-5.6-2z">
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="md-footer-social__link" href="https://twitter.com/kot_hay" rel="noopener" target="_blank"
|
||||
title="twitter.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<a class="md-footer-social__link" href="https://bsky.app/profile/haykot.dev" rel="noopener" target="_blank"
|
||||
title="bsky.app">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z">
|
||||
d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.204-.659-.299-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z">
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="md-footer-social__link" href="https://www.linkedin.com/in/hay-kot" rel="noopener" target="_blank"
|
||||
title="www.linkedin.com">
|
||||
title="linkedin.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z">
|
||||
|
||||
@@ -19,6 +19,7 @@ theme:
|
||||
custom_dir: docs/overrides
|
||||
features:
|
||||
- content.code.annotate
|
||||
- content.code.copy
|
||||
- navigation.top
|
||||
- navigation.instant
|
||||
- navigation.expand
|
||||
|
||||
@@ -20,16 +20,12 @@
|
||||
max-width: 1100px !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-application {
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
.v-theme--dark.v-application {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-navigation-drawer {
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-card {
|
||||
background-color: #1e1e1e !important;
|
||||
.v-theme--dark .v-navigation-drawer {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
|
||||
.left-border {
|
||||
@@ -61,10 +57,6 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.fill-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -72,3 +64,8 @@ a {
|
||||
.vue-simple-handler {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
v-if="currentAnnouncement"
|
||||
v-model="dialog"
|
||||
:title="$t('announcements.announcements')"
|
||||
:icon="$globals.icons.bullhornVariant"
|
||||
:cancel-text="$t('general.done')"
|
||||
width="100%"
|
||||
max-width="1200"
|
||||
>
|
||||
<div class="d-flex" :style="{ height: useMobile ? '100%' : '60vh', minHeight: '60vh' }">
|
||||
<!-- Nav list -->
|
||||
<v-list
|
||||
v-show="!useMobile || navOpen"
|
||||
nav
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="overflow-y-auto border-e flex-shrink-0"
|
||||
style="width: 200px; max-height: 60vh"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="announcement in allAnnouncements.toReversed()"
|
||||
:key="announcement.key"
|
||||
:active="currentAnnouncement.key === announcement.key"
|
||||
rounded
|
||||
@click="setCurrentAnnouncement(announcement); navOpen = false"
|
||||
>
|
||||
<v-list-item-title class="text-body-2">
|
||||
{{ announcement.meta?.title }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="announcement.date">
|
||||
{{ $d(announcement.date) }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-if="newAnnouncements.some(a => a.key === announcement.key)" #append>
|
||||
<v-icon size="x-small" color="info">
|
||||
{{ $globals.icons.alertCircle }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- Main content -->
|
||||
<div
|
||||
class="flex-grow-1 overflow-y-auto"
|
||||
>
|
||||
<v-btn
|
||||
v-if="useMobile"
|
||||
:prepend-icon="navOpen ? $globals.icons.chevronLeft : $globals.icons.chevronRight"
|
||||
density="compact"
|
||||
variant="text"
|
||||
class="mt-2 ms-2"
|
||||
@click="navOpen = !navOpen"
|
||||
>
|
||||
{{ $t("announcements.all-announcements") }}
|
||||
</v-btn>
|
||||
<v-card-title>
|
||||
<v-chip v-if="currentAnnouncement.date" label large class="me-1">
|
||||
<v-icon class="me-1">
|
||||
{{ $globals.icons.calendar }}
|
||||
</v-icon>
|
||||
{{ $d(currentAnnouncement.date) }}
|
||||
</v-chip>
|
||||
{{ currentAnnouncement.meta?.title }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<component :is="currentAnnouncement.component" />
|
||||
</v-card-text>
|
||||
</div>
|
||||
</div>
|
||||
<template #custom-card-action>
|
||||
<BaseButton
|
||||
v-if="newAnnouncements.length"
|
||||
color="success"
|
||||
:icon="$globals.icons.textBoxCheckOutline"
|
||||
:text="$t('announcements.mark-all-as-read')"
|
||||
@click="markAllAsRead"
|
||||
/>
|
||||
<BaseButton
|
||||
:disabled="isLastAnnouncement(currentAnnouncement.key)"
|
||||
color="info"
|
||||
:icon="$globals.icons.arrowRightBold"
|
||||
icon-right
|
||||
:text="$t('general.next')"
|
||||
@click="nextAnnouncement"
|
||||
/>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAnnouncements } from "~/composables/use-announcements";
|
||||
import type { Announcement } from "~/composables/use-announcements";
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const display = useDisplay();
|
||||
const useMobile = computed(() => display.smAndDown.value);
|
||||
const navOpen = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
watch(() => route.fullPath, () => { dialog.value = false; });
|
||||
|
||||
const { newAnnouncements, allAnnouncements, setLastRead, markAllAsRead } = useAnnouncements();
|
||||
|
||||
const currentAnnouncement = shallowRef<Announcement | undefined>();
|
||||
|
||||
watch(dialog, () => {
|
||||
if (!dialog.value || currentAnnouncement.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show first unread on open, or fall back to the newest
|
||||
const next = newAnnouncements.value.at(0) || allAnnouncements.at(-1)!;
|
||||
setCurrentAnnouncement(next);
|
||||
});
|
||||
|
||||
function setCurrentAnnouncement(announcement: Announcement) {
|
||||
currentAnnouncement.value = announcement;
|
||||
setLastRead(announcement.key);
|
||||
}
|
||||
|
||||
function nextAnnouncement() {
|
||||
// Find the first unread announcement after the current one (current is already removed from newAnnouncements)
|
||||
const next = newAnnouncements.value.find(a => a.key > currentAnnouncement.value!.key);
|
||||
if (next) {
|
||||
setCurrentAnnouncement(next);
|
||||
}
|
||||
}
|
||||
|
||||
function isLastAnnouncement(key: string) {
|
||||
if (!newAnnouncements.value.length) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return key >= newAnnouncements.value.at(-1)!.key;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
Welcome to Mealie! If this is your first time seeing announcements, here's what to expect.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
Announcements are reserved for things like:
|
||||
<ul class="ml-6">
|
||||
<li>Important new features</li>
|
||||
<li>Major changes</li>
|
||||
<li>Anything that might require additional user actions (such as migration scripts)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
While we generally keep everything in our <a class="text-primary" href="https://github.com/mealie-recipes/mealie/releases" target="_blank">GitHub release notes</a>,
|
||||
sometimes certain changes require some extra attention.
|
||||
</p>
|
||||
<p>
|
||||
Announcements are English-only; they're one-off messages from the maintainers, not a replacement for our release notes. Some elements may still be translated.
|
||||
</p>
|
||||
<hr class="mt-2 mb-4">
|
||||
<p>
|
||||
You can opt out of announcements in your user settings:
|
||||
<br>
|
||||
<v-btn class="mt-2" color="primary" to="/user/profile/edit">
|
||||
{{ $t("profile.user-settings") }}
|
||||
</v-btn>
|
||||
</p>
|
||||
<p v-if="user?.canManageHousehold" class="mt-3">
|
||||
As {{ user?.admin ? "an admin" : "a household manager" }}, you can disable announcements for your entire household:
|
||||
<br>
|
||||
<v-btn class="mt-2" color="primary" to="/household">
|
||||
{{ $t("profile.household-settings") }}
|
||||
</v-btn>
|
||||
</p>
|
||||
<p v-if="user?.canManage" class="mt-3">
|
||||
{{ user?.admin ? "You can also" : "As a group manager, you can" }} disable announcements for your entire group:
|
||||
<br>
|
||||
<v-btn class="mt-2" color="primary" to="/group">
|
||||
{{ $t("profile.group-settings") }}
|
||||
</v-btn>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AnnouncementMeta } from "~/composables/use-announcements";
|
||||
|
||||
const { user } = useMealieAuth();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const meta: AnnouncementMeta = {
|
||||
title: "Welcome to Mealie 🎉",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
p {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
const announcementFiles = import.meta.glob<{ default: unknown }>(
|
||||
"~/components/Domain/Announcement/Announcements/*.vue",
|
||||
);
|
||||
|
||||
// Expected format: YYYY-MM-DD_N_slug e.g. 2026-03-27_1_welcome
|
||||
const FILE_FORMAT = /^\d{4}-\d{2}-\d{2}_\d+_.+$/;
|
||||
|
||||
describe("Announcement files", () => {
|
||||
const filenames = Object.keys(announcementFiles).map(path =>
|
||||
path.split("/").at(-1)!.replace(".vue", ""),
|
||||
);
|
||||
|
||||
test("directory is not empty", () => {
|
||||
expect(filenames.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("all filenames match YYYY-MM-DD_N_slug format", () => {
|
||||
for (const name of filenames) {
|
||||
expect(name, `"${name}" does not match the expected format`).toMatch(FILE_FORMAT);
|
||||
}
|
||||
});
|
||||
|
||||
test("all date prefixes are valid dates", () => {
|
||||
for (const name of filenames) {
|
||||
const datePart = name.split("_", 1)[0]!;
|
||||
const date = new Date(datePart);
|
||||
expect(isNaN(date.getTime()), `"${name}" has an invalid date prefix "${datePart}"`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("all filenames are unique", () => {
|
||||
const unique = new Set(filenames);
|
||||
expect(unique.size).toBe(filenames.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle :title="$t('group.group-preferences')" />
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="local.privateGroup"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
:label="$t('group.private-group')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("group.private-group-description") }}
|
||||
</p>
|
||||
<DocLink
|
||||
class="mt-2"
|
||||
link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="local.showAnnouncements"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
:label="$t('announcements.show-announcements-from-mealie')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("announcements.show-announcements-setting-description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||
|
||||
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||
const local = reactive({ ...preferences.value });
|
||||
watch(local, (newVal) => { preferences.value = { ...newVal }; });
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle :title="$t('household.household-preferences')" />
|
||||
<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="local.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.private-household-description") }}
|
||||
@@ -11,15 +11,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<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="local.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="local.showAnnouncements"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
:label="$t('announcements.show-announcements-from-mealie')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("announcements.show-announcements-setting-description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
v-model="local.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-title="name"
|
||||
@@ -34,7 +48,7 @@
|
||||
</BaseCardSectionTitle>
|
||||
<div class="preference-container">
|
||||
<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="local[p.key]" hide-details density="compact" :label="p.label" color="primary" />
|
||||
<p class="ml-8 text-subtitle-2 my-0 py-0">
|
||||
{{ p.description }}
|
||||
</p>
|
||||
@@ -47,6 +61,9 @@
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||
const local = reactive({ ...preferences.value });
|
||||
watch(local, (newVal) => { preferences.value = { ...newVal }; });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
@@ -41,19 +41,14 @@
|
||||
>
|
||||
<v-select
|
||||
v-if="index"
|
||||
:model-value="field.logicalOperator"
|
||||
:model-value="field.logicalOperator?.value"
|
||||
:items="[logOps.AND, logOps.OR]"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- left parenthesis -->
|
||||
@@ -67,14 +62,9 @@
|
||||
:model-value="field.leftParenthesis"
|
||||
:items="['', '(', '((', '(((']"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setLeftParenthesisValue(field, index, $event)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- field name -->
|
||||
@@ -84,19 +74,14 @@
|
||||
:class="config.col.class"
|
||||
>
|
||||
<v-select
|
||||
chips
|
||||
:model-value="field.label"
|
||||
:items="fieldDefs"
|
||||
variant="underlined"
|
||||
item-title="label"
|
||||
item-value="label"
|
||||
class="text-center"
|
||||
@update:model-value="setField(index, $event)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- relational operator -->
|
||||
@@ -107,19 +92,14 @@
|
||||
>
|
||||
<v-select
|
||||
v-if="field.type !== 'boolean'"
|
||||
:model-value="field.relationalOperatorValue"
|
||||
:model-value="field.relationalOperatorValue?.value"
|
||||
:items="field.relationalOperatorChoices"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- field value -->
|
||||
@@ -275,23 +255,14 @@
|
||||
:model-value="field.rightParenthesis"
|
||||
:items="['', ')', '))', ')))']"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setRightParenthesisValue(field, index, $event)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<!-- field actions -->
|
||||
<v-col
|
||||
/>
|
||||
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
|
||||
:cols="config.items.fieldActions.cols(index)"
|
||||
:sm="config.items.fieldActions.sm(index)"
|
||||
:class="config.col.class"
|
||||
>
|
||||
>
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
@@ -723,9 +694,6 @@ const config = computed(() => {
|
||||
col: {
|
||||
class: "d-flex justify-center align-end py-0",
|
||||
},
|
||||
select: {
|
||||
textClass: "d-flex justify-center text-center",
|
||||
},
|
||||
items: {
|
||||
icon: {
|
||||
cols: (_index: number) => 2,
|
||||
@@ -36,10 +36,8 @@
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="mb-n3 px-4">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
<v-card-title class="mb-n3 px-4" style="font-size: 1.25rem;">
|
||||
{{ name }}
|
||||
</v-card-title>
|
||||
|
||||
<slot name="actions">
|
||||
@@ -1,24 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-app-bar
|
||||
<v-row
|
||||
v-if="!disableToolbar"
|
||||
color="transparent"
|
||||
:absolute="false"
|
||||
flat
|
||||
class="mt-n1 flex-sm-wrap rounded position-relative w-100 left-0 top-0"
|
||||
class="align-center pb-2"
|
||||
>
|
||||
<slot name="title">
|
||||
<v-icon
|
||||
v-if="title"
|
||||
size="large"
|
||||
start
|
||||
>
|
||||
{{ displayTitleIcon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
</slot>
|
||||
<v-icon
|
||||
v-if="title"
|
||||
size="large"
|
||||
start
|
||||
>
|
||||
{{ displayTitleIcon }}
|
||||
</v-icon>
|
||||
<span class="text-headline-small">{{ title }}</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
:icon="$vuetify.display.xs"
|
||||
@@ -111,7 +104,7 @@
|
||||
]"
|
||||
@toggle-dense-view="toggleMobileCards()"
|
||||
/>
|
||||
</v-app-bar>
|
||||
</v-row>
|
||||
<div v-if="recipes && ready">
|
||||
<div class="mt-2">
|
||||
<v-row v-if="!useMobileCards">
|
||||
@@ -136,7 +129,7 @@
|
||||
</v-row>
|
||||
<v-row
|
||||
v-else
|
||||
dense
|
||||
density="comfortable"
|
||||
>
|
||||
<v-col
|
||||
v-for="recipe in recipes"
|
||||
@@ -159,14 +152,15 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<v-card v-intersect="infiniteScroll" />
|
||||
<v-fade-transition>
|
||||
<AppLoader
|
||||
v-if="loading"
|
||||
:loading="loading"
|
||||
/>
|
||||
</v-fade-transition>
|
||||
<v-card v-intersect="infiniteScroll" variant="flat" />
|
||||
</div>
|
||||
<v-fade-transition>
|
||||
<AppLoader
|
||||
v-if="loading"
|
||||
:loading="loading"
|
||||
/>
|
||||
</v-fade-transition>
|
||||
<AppScrollToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -243,6 +237,7 @@ const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { savePosition, getSavedPage, restorePosition } = useScrollPosition();
|
||||
const router = useRouter();
|
||||
|
||||
const queryFilter = computed(() => {
|
||||
@@ -283,8 +278,29 @@ async function fetchRecipes(pageCount = 1) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
loading.value = true;
|
||||
const savedPage = getSavedPage(route.path);
|
||||
|
||||
if (savedPage && savedPage > 2) {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
const newRecipes = await fetchRecipes(savedPage);
|
||||
if (newRecipes.length < perPage * savedPage) {
|
||||
hasMore.value = false;
|
||||
}
|
||||
page.value = savedPage;
|
||||
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
ready.value = true;
|
||||
restorePosition(route.path);
|
||||
}
|
||||
else {
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
if (savedPage) {
|
||||
restorePosition(route.path);
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||
@@ -337,6 +353,8 @@ const infiniteScroll = useThrottleFn(async () => {
|
||||
emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
savePosition(route.path, page.value);
|
||||
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
|
||||
@@ -1,91 +1,60 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog
|
||||
<BaseButton @click="dialog = true">
|
||||
{{ $t("new-recipe.bulk-add") }}
|
||||
</BaseButton>
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
width="800"
|
||||
:title="$t('new-recipe.bulk-add')"
|
||||
:icon="$globals.icons.createAlt"
|
||||
:submit-text="$t('general.add')"
|
||||
:disable-submit-on-enter="true"
|
||||
can-submit
|
||||
@submit="save"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<BaseButton
|
||||
v-bind="activatorProps"
|
||||
@click="inputText = inputTextProp"
|
||||
>
|
||||
{{ $t("new-recipe.bulk-add") }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="inputText"
|
||||
variant="outlined"
|
||||
rows="12"
|
||||
hide-details
|
||||
autofocus
|
||||
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
|
||||
/>
|
||||
|
||||
<v-card>
|
||||
<v-app-bar
|
||||
density="compact"
|
||||
dark
|
||||
color="primary"
|
||||
class="mb-2 position-relative left-0 top-0 w-100"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
start
|
||||
>
|
||||
{{ $globals.icons.createAlt }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("new-recipe.bulk-add") }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="inputText"
|
||||
variant="outlined"
|
||||
rows="12"
|
||||
hide-details
|
||||
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
|
||||
/>
|
||||
|
||||
<v-divider />
|
||||
<v-divider />
|
||||
<v-list lines="two">
|
||||
<template
|
||||
v-for="(util) in utilities"
|
||||
:key="util.id"
|
||||
>
|
||||
<v-list-item
|
||||
density="compact"
|
||||
class="py-1"
|
||||
class="px-0"
|
||||
>
|
||||
<v-list-item-title>
|
||||
<v-list-item-subtitle class="wrap-word">
|
||||
{{ util.description }}
|
||||
</v-list-item-subtitle>
|
||||
<template #prepend>
|
||||
<v-avatar>
|
||||
<v-btn
|
||||
icon
|
||||
variant="tonal"
|
||||
base-color="info"
|
||||
:title="$t('general.run')"
|
||||
@click="util.action"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.play }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-pre-wrap">
|
||||
{{ util.description }}
|
||||
</v-list-item-title>
|
||||
<BaseButton
|
||||
size="small"
|
||||
color="info"
|
||||
@click="util.action"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
{{ $t("general.run") }}
|
||||
</BaseButton>
|
||||
</v-list-item>
|
||||
<v-divider class="mx-2" />
|
||||
</template>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<BaseButton
|
||||
cancel
|
||||
@click="dialog = false"
|
||||
/>
|
||||
<v-spacer />
|
||||
<BaseButton
|
||||
save
|
||||
color="success"
|
||||
@click="save"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,66 +7,64 @@
|
||||
content-class="top-dialog"
|
||||
:scrollable="false"
|
||||
>
|
||||
<v-app-bar
|
||||
sticky
|
||||
dark
|
||||
color="primary-lighten-1 top-0 position-relative left-0"
|
||||
:rounded="!$vuetify.display.xs"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<v-text-field
|
||||
id="arrow-search"
|
||||
v-model="search.query.value"
|
||||
autofocus
|
||||
variant="solo"
|
||||
flat
|
||||
autocomplete="off"
|
||||
bg-color="primary-lighten-1"
|
||||
color="white"
|
||||
density="compact"
|
||||
class="mx-2 arrow-search"
|
||||
hide-details
|
||||
single-line
|
||||
:placeholder="$t('search.search')"
|
||||
:prepend-inner-icon="$globals.icons.search"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
v-if="$vuetify.display.xs"
|
||||
icon
|
||||
size="x-small"
|
||||
@click="dialog = false"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.close }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<v-card
|
||||
class="position-relative mt-1 pa-1 scroll"
|
||||
max-height="700px"
|
||||
relative
|
||||
:rounded="!$vuetify.display.xs"
|
||||
:loading="loading"
|
||||
>
|
||||
<v-toolbar
|
||||
dark
|
||||
color="primary-lighten-1"
|
||||
>
|
||||
<v-text-field
|
||||
id="arrow-search"
|
||||
v-model="search.query.value"
|
||||
autofocus
|
||||
variant="solo"
|
||||
flat
|
||||
autocomplete="off"
|
||||
bg-color="primary-lighten-1"
|
||||
color="white"
|
||||
density="compact"
|
||||
class="mx-2 arrow-search"
|
||||
hide-details
|
||||
single-line
|
||||
:placeholder="$t('search.search')"
|
||||
:prepend-inner-icon="$globals.icons.search"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
v-if="$vuetify.display.xs"
|
||||
icon
|
||||
size="x-small"
|
||||
@click="dialog = false"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.close }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-actions>
|
||||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
v-for="(recipe, index) in search.data.value"
|
||||
:key="index"
|
||||
:tabindex="index"
|
||||
class="ma-1 arrow-nav"
|
||||
:name="recipe.name ?? ''"
|
||||
:description="recipe.description ?? ''"
|
||||
:slug="recipe.slug ?? ''"
|
||||
:rating="recipe.rating ?? 0"
|
||||
:image="recipe.image"
|
||||
:recipe-id="recipe.id ?? ''"
|
||||
v-bind="$attrs.selected ? { selected: () => handleSelect(recipe) } : {}"
|
||||
/>
|
||||
<div class="scroll pa-1" style="max-height: 700px;">
|
||||
<RecipeCardMobile
|
||||
v-for="(recipe, index) in search.data.value"
|
||||
:key="index"
|
||||
:tabindex="index"
|
||||
class="ma-1 arrow-nav"
|
||||
:name="recipe.name ?? ''"
|
||||
:description="recipe.description ?? ''"
|
||||
:slug="recipe.slug ?? ''"
|
||||
:rating="recipe.rating ?? 0"
|
||||
:image="recipe.image"
|
||||
:recipe-id="recipe.id ?? ''"
|
||||
v-bind="$attrs.selected ? { selected: () => handleSelect(recipe) } : {}"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
@@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<BaseDialog
|
||||
v-model="dialogDeleteImage"
|
||||
:title="$t('recipe.delete-image')"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
color="error"
|
||||
can-delete
|
||||
@delete="deleteImage"
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("recipe.delete-image-confirmation") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<v-menu
|
||||
v-model="menu"
|
||||
offset-y
|
||||
@@ -37,18 +49,6 @@
|
||||
delete
|
||||
@click="dialogDeleteImage = true"
|
||||
/>
|
||||
<BaseDialog
|
||||
v-model="dialogDeleteImage"
|
||||
:title="$t('recipe.delete-image')"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
color="error"
|
||||
can-delete
|
||||
@delete="deleteImage"
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("recipe.delete-image-confirmation") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-text class="mt-n5">
|
||||
@@ -13,7 +13,7 @@
|
||||
/>
|
||||
<v-row
|
||||
:no-gutters="mdAndUp"
|
||||
dense
|
||||
density="comfortable"
|
||||
class="d-flex flex-wrap my-1"
|
||||
>
|
||||
<v-col
|
||||
@@ -1,62 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-dialog
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
width="500"
|
||||
:title="properties.title"
|
||||
:icon="properties.icon"
|
||||
can-submit
|
||||
:submit-disabled="!name"
|
||||
@submit="select"
|
||||
>
|
||||
<v-card>
|
||||
<v-app-bar
|
||||
density="compact"
|
||||
dark
|
||||
color="primary mb-2 position-relative left-0 top-0 w-100 pl-3"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
start
|
||||
class="mt-1"
|
||||
>
|
||||
{{ itemType === Organizer.Tool ? $globals.icons.potSteam
|
||||
: itemType === Organizer.Category ? $globals.icons.categories
|
||||
: $globals.icons.tags }}
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ properties.title }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer />
|
||||
</v-app-bar>
|
||||
<v-card-title />
|
||||
<v-form @submit.prevent="select">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
density="compact"
|
||||
:label="properties.label"
|
||||
:rules="[rules.required]"
|
||||
autofocus
|
||||
/>
|
||||
<v-checkbox
|
||||
v-if="itemType === Organizer.Tool"
|
||||
v-model="onHand"
|
||||
:label="$t('tool.on-hand')"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton
|
||||
cancel
|
||||
@click="dialog = false"
|
||||
/>
|
||||
<v-spacer />
|
||||
<BaseButton
|
||||
type="submit"
|
||||
create
|
||||
:disabled="!name"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-form>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
:label="properties.label"
|
||||
:rules="[rules.required]"
|
||||
autofocus
|
||||
/>
|
||||
<v-checkbox
|
||||
v-if="itemType === Organizer.Tool"
|
||||
v-model="onHand"
|
||||
:label="$t('tool.on-hand')"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-form>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -65,6 +33,8 @@ import { useUserApi } from "~/composables/api";
|
||||
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const CREATED_ITEM_EVENT = "created-item";
|
||||
|
||||
interface Props {
|
||||
@@ -115,18 +85,21 @@ const properties = computed(() => {
|
||||
return {
|
||||
title: i18n.t("tag.create-a-tag"),
|
||||
label: i18n.t("tag.tag-name"),
|
||||
icon: $globals.icons.tags,
|
||||
api: userApi.tags,
|
||||
};
|
||||
case Organizer.Tool:
|
||||
return {
|
||||
title: i18n.t("tool.create-a-tool"),
|
||||
label: i18n.t("tool.tool-name"),
|
||||
icon: $globals.icons.potSteam,
|
||||
api: userApi.tools,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: i18n.t("category.create-a-category"),
|
||||
label: i18n.t("category.category-name"),
|
||||
icon: $globals.icons.categories,
|
||||
api: userApi.categories,
|
||||
};
|
||||
}
|
||||
@@ -139,12 +112,9 @@ const rules = {
|
||||
async function select() {
|
||||
if (store) {
|
||||
// @ts-expect-error the same state is used for different organizer types, which have different requirements
|
||||
await store.actions.createOne({ name: name.value, onHand: onHand.value });
|
||||
const newItem = await store.actions.createOne({ name: name.value, onHand: onHand.value });
|
||||
emit(CREATED_ITEM_EVENT, newItem);
|
||||
}
|
||||
|
||||
const newItem = store.store.value.find(item => item.name === name.value);
|
||||
|
||||
emit(CREATED_ITEM_EVENT, newItem);
|
||||
dialog.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -26,6 +26,7 @@
|
||||
v-if="updateTarget"
|
||||
v-model="dialogs.update"
|
||||
:title="$t('general.update')"
|
||||
:icon="$globals.icons.edit"
|
||||
can-confirm
|
||||
@confirm="updateOne()"
|
||||
>
|
||||
@@ -42,7 +43,7 @@
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<v-row dense>
|
||||
<v-row density="comfortable">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="searchString"
|
||||
@@ -56,7 +57,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-app-bar
|
||||
<v-row
|
||||
color="transparent"
|
||||
flat
|
||||
class="mt-n1 rounded align-center position-relative w-100 left-0 top-0"
|
||||
@@ -75,7 +76,7 @@
|
||||
create
|
||||
@click="dialogs.organizer = true"
|
||||
/>
|
||||
</v-app-bar>
|
||||
</v-row>
|
||||
<section
|
||||
v-for="(itms, key, idx) in itemsSorted"
|
||||
:key="'header' + idx"
|
||||
@@ -27,12 +27,10 @@
|
||||
color="accent"
|
||||
variant="flat"
|
||||
label
|
||||
|
||||
:text="item.name"
|
||||
closable
|
||||
@click:close="removeByIndex(index)"
|
||||
>
|
||||
{{ item.value }}
|
||||
</v-chip>
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-if="showAdd"
|
||||
@@ -21,7 +21,7 @@
|
||||
@save="saveParsedIngredients"
|
||||
/>
|
||||
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown }">
|
||||
<v-card :flat="$vuetify.display.smAndDown" class="d-print-none">
|
||||
<v-card flat class="d-print-none">
|
||||
<RecipePageHeader
|
||||
:recipe="recipe"
|
||||
:recipe-scale="scale"
|
||||
@@ -68,17 +68,21 @@
|
||||
<!--
|
||||
The left column is conditionally rendered based on cook mode.
|
||||
-->
|
||||
<v-col v-if="!isCookMode || isEditForm" cols="12" sm="12" md="4" lg="4">
|
||||
<RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" />
|
||||
<RecipePageOrganizers v-if="$vuetify.display.mdAndUp" v-model="recipe" @item-selected="chipClicked" />
|
||||
<v-col
|
||||
v-if="!isCookMode || isEditForm"
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="4"
|
||||
:class="$vuetify.display.mdAndUp ? 'border-e-thin' : null"
|
||||
>
|
||||
<RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" class="pr-2" />
|
||||
<RecipePageOrganizers v-if="$vuetify.display.mdAndUp" v-model="recipe" class="pr-2" @item-selected="chipClicked" />
|
||||
</v-col>
|
||||
<v-divider v-if="$vuetify.display.mdAndUp && !isCookMode" class="my-divider" :vertical="true" />
|
||||
|
||||
<!--
|
||||
the right column is always rendered, but it's layout width is determined by where the left column is
|
||||
rendered.
|
||||
-->
|
||||
<v-col cols="12" sm="12" :md="8 + (isCookMode ? 1 : 0) * 4" :lg="8 + (isCookMode ? 1 : 0) * 4">
|
||||
<v-col cols="12" sm="12" :md="8 + (isCookMode ? 1 : 0) * 4">
|
||||
<RecipePageInstructions
|
||||
v-model="recipe.recipeInstructions"
|
||||
v-model:assets="recipe.assets"
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
@@ -62,17 +62,18 @@ const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||
const { user } = usePageUser();
|
||||
const { isEditMode } = usePageState(props.recipe.slug);
|
||||
|
||||
const recipeTools = computed(() => {
|
||||
const recipeTools = ref<RecipeToolWithOnHand[]>([]);
|
||||
watch(() => props.recipe.tools, () => {
|
||||
if (!(user.householdSlug && toolStore)) {
|
||||
return props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
|
||||
recipeTools.value = props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
|
||||
}
|
||||
else {
|
||||
return props.recipe.tools.map((tool) => {
|
||||
recipeTools.value = props.recipe.tools.map((tool) => {
|
||||
const onHand = tool.householdsWithTool?.includes(user.householdSlug) || false;
|
||||
return { ...tool, onHand } as RecipeToolWithOnHand;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
function updateTool(index: number) {
|
||||
if (user.id && user.householdSlug && toolStore) {
|
||||
@@ -1,117 +1,101 @@
|
||||
<template>
|
||||
<section @keyup.ctrl.z="undoMerge">
|
||||
<!-- Ingredient Link Editor -->
|
||||
<v-dialog
|
||||
v-if="dialog"
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
width="600"
|
||||
:title="$t('recipe.ingredient-linker')"
|
||||
:icon="$globals.icons.link"
|
||||
width="100%"
|
||||
max-width="600px"
|
||||
max-height="40%"
|
||||
>
|
||||
<v-card :ripple="false">
|
||||
<v-sheet
|
||||
color="primary"
|
||||
class="mt-n1 mb-3 pa-3 d-flex align-center"
|
||||
style="border-radius: 6px; width: 100%;"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
start
|
||||
>
|
||||
{{ $globals.icons.link }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("recipe.ingredient-linker") }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-sheet>
|
||||
|
||||
<v-card-text class="pt-4">
|
||||
<p>
|
||||
{{ activeText }}
|
||||
</p>
|
||||
<v-divider class="mb-4" />
|
||||
<template v-if="Object.keys(groupedUnusedIngredients).length > 0">
|
||||
<h4 class="py-3 ml-1">
|
||||
{{ $t("recipe.unlinked") }}
|
||||
<v-card-text class="pt-4">
|
||||
<p>
|
||||
{{ activeText }}
|
||||
</p>
|
||||
<v-divider class="my-4" />
|
||||
<template v-if="Object.keys(groupedUnusedIngredients).length > 0">
|
||||
<h4 class="ml-1">
|
||||
{{ $t("recipe.unlinked") }}
|
||||
</h4>
|
||||
<template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title">
|
||||
<h4 v-if="title" class="py-3 ml-1 pl-4">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title">
|
||||
<h4 v-if="title" class="py-3 ml-1 pl-4">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<v-checkbox-btn
|
||||
v-for="ing in ingredients"
|
||||
:key="ing.referenceId"
|
||||
v-model="activeRefs"
|
||||
:value="ing.referenceId"
|
||||
class="ml-4"
|
||||
>
|
||||
<template #label>
|
||||
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
<v-checkbox-btn
|
||||
v-for="ing in ingredients"
|
||||
:key="ing.referenceId"
|
||||
v-model="activeRefs"
|
||||
:value="ing.referenceId"
|
||||
class="ml-4"
|
||||
>
|
||||
<template #label>
|
||||
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="Object.keys(groupedUsedIngredients).length > 0">
|
||||
<h4 class="py-3 ml-1">
|
||||
{{ $t("recipe.linked-to-other-step") }}
|
||||
<template v-if="Object.keys(groupedUsedIngredients).length > 0">
|
||||
<h4 class="py-3 ml-1">
|
||||
{{ $t("recipe.linked-to-other-step") }}
|
||||
</h4>
|
||||
<template v-for="(ingredients, title) in groupedUsedIngredients" :key="title">
|
||||
<h4 v-if="title" class="py-3 ml-1 pl-4">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<template v-for="(ingredients, title) in groupedUsedIngredients" :key="title">
|
||||
<h4 v-if="title" class="py-3 ml-1 pl-4">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<v-checkbox-btn
|
||||
v-for="ing in ingredients"
|
||||
:key="ing.referenceId"
|
||||
v-model="activeRefs"
|
||||
:value="ing.referenceId"
|
||||
class="ml-4"
|
||||
>
|
||||
<template #label>
|
||||
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
<v-checkbox-btn
|
||||
v-for="ing in ingredients"
|
||||
:key="ing.referenceId"
|
||||
v-model="activeRefs"
|
||||
:value="ing.referenceId"
|
||||
class="ml-4"
|
||||
>
|
||||
<template #label>
|
||||
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</template>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<template #card-actions>
|
||||
<BaseButton
|
||||
cancel
|
||||
@click="dialog = false"
|
||||
/>
|
||||
<v-spacer />
|
||||
<div class="d-flex flex-wrap justify-end">
|
||||
<BaseButton
|
||||
cancel
|
||||
@click="dialog = false"
|
||||
class="my-1"
|
||||
color="info"
|
||||
@click="autoSetReferences"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
{{ $t("recipe.auto") }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="ml-2 my-1"
|
||||
save
|
||||
@click="setIngredientIds"
|
||||
/>
|
||||
<v-spacer />
|
||||
<div class="d-flex flex-wrap justify-end">
|
||||
<BaseButton
|
||||
class="my-1"
|
||||
color="info"
|
||||
@click="autoSetReferences"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
{{ $t("recipe.auto") }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="ml-2 my-1"
|
||||
save
|
||||
@click="setIngredientIds"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="availableNextStep"
|
||||
class="ml-2 my-1"
|
||||
@click="saveAndOpenNextLinkIngredients"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.forward }}
|
||||
</template>
|
||||
{{ $t("recipe.nextStep") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<BaseButton
|
||||
v-if="availableNextStep"
|
||||
class="ml-2 my-1"
|
||||
@click="saveAndOpenNextLinkIngredients"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.forward }}
|
||||
</template>
|
||||
{{ $t("recipe.nextStep") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
<div class="d-flex justify-space-between justify-start">
|
||||
<h2
|
||||
@@ -851,6 +835,10 @@ function openImageUpload(index: number) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.v-card-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.recipe-step-title {
|
||||
/* Multiline display */
|
||||
white-space: normal;
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
@@ -371,14 +371,18 @@ async function parseIngredients() {
|
||||
}
|
||||
state.loading.parser = true;
|
||||
try {
|
||||
const ingsAsString = props.ingredients
|
||||
.filter(ing => !ing.referencedRecipe)
|
||||
.map(ing => ingredientToParserString(ing));
|
||||
const filteredIngredients = props.ingredients.filter(ing => !ing.referencedRecipe);
|
||||
const ingsAsString = filteredIngredients.map(ing => ingredientToParserString(ing));
|
||||
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
|
||||
if (error || !data) {
|
||||
throw new Error("Failed to parse ingredients");
|
||||
}
|
||||
parsedIngs.value = data;
|
||||
|
||||
// Restore section titles from original ingredients — the parser doesn't return them
|
||||
data.forEach((parsed, index) => {
|
||||
parsed.ingredient.title = filteredIngredients[index]?.title || "";
|
||||
});
|
||||
|
||||
const parsed = data ?? [];
|
||||
const recipeRefs = props.ingredients.filter(ing => ing.referencedRecipe).map(ing => ({
|
||||
input: ing.note || "",
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import RecipeSettingsSwitches from "./RecipeSettingsSwitches.vue";
|
||||
|
||||
const value = defineModel<object>({ required: true });
|
||||
@@ -15,8 +15,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineModel, defineProps } from "vue";
|
||||
<script setup lang="ts">
|
||||
import type { RecipeSettings } from "~/lib/api/types/recipe";
|
||||
import { useI18n } from "#imports";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user