mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-26 17:53:12 -05:00
Compare commits
2 Commits
v3.6.1
...
auto-local
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d4bb320b7 | ||
|
|
e417a2001d |
@@ -8,13 +8,28 @@ FROM mcr.microsoft.com/devcontainers/python:${VARIANT}
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
|
||||
RUN echo "export PROMPT_COMMAND='history -a'" >> /home/vscode/.bashrc \
|
||||
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/vscode/.bashrc \
|
||||
&& chown vscode:vscode -R /home/vscode/
|
||||
|
||||
RUN npm install -g @go-task/cli
|
||||
|
||||
# Install additional OS packages
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=off \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
|
||||
# prepend poetry and venv to path
|
||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
# RUN poetry config virtualenvs.create false
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
curl \
|
||||
@@ -24,9 +39,5 @@ RUN apt-get update \
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1
|
||||
|
||||
# Install uv
|
||||
RUN pip install uv
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Create directory for Docker Secrets
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||
"VARIANT": "3.12-bullseye",
|
||||
// Options
|
||||
"NODE_VERSION": "22"
|
||||
"NODE_VERSION": "20"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
@@ -48,7 +48,7 @@
|
||||
],
|
||||
// Use 'onCreateCommand' to run commands at the end of container creation.
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules /home/vscode/commandhistory && task setup --force",
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules /home/vscode/commandhistory && task setup",
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
@@ -56,8 +56,5 @@
|
||||
"dockerDashComposeVersion": "v2"
|
||||
}
|
||||
},
|
||||
"appPort": [
|
||||
"3000:3000",
|
||||
"9000:9000"
|
||||
]
|
||||
"appPort": 3000
|
||||
}
|
||||
|
||||
11
.github/workflows/build-package.yml
vendored
11
.github/workflows/build-package.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
@@ -70,8 +70,13 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
plugins: |
|
||||
poetry-plugin-export
|
||||
|
||||
- name: Retrieve built frontend
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./tests/e2e/yarn.lock
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
32
.github/workflows/locale-sync.yml
vendored
32
.github/workflows/locale-sync.yml
vendored
@@ -25,21 +25,24 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-python-dependencies
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- name: Check venv cache
|
||||
id: cache-validate
|
||||
if: steps.cached-python-dependencies.outputs.cache-hit == 'true'
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "import fastapi;print('venv good?')" > test.py && uv run python test.py && echo "cache-hit-success=true" >> $GITHUB_OUTPUT
|
||||
echo "import fastapi;print('venv good?')" > test.py && poetry run python test.py && echo "cache-hit-success=true" >> $GITHUB_OUTPUT
|
||||
rm test.py
|
||||
continue-on-error: true
|
||||
|
||||
@@ -47,13 +50,13 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||
uv sync --group dev
|
||||
if: steps.cached-python-dependencies.outputs.cache-hit != 'true'
|
||||
poetry install
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Run locale generation
|
||||
run: |
|
||||
cd dev/code-generation
|
||||
uv run python main.py locales
|
||||
poetry run python main.py locales
|
||||
env:
|
||||
CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }}
|
||||
|
||||
@@ -83,7 +86,7 @@ jobs:
|
||||
|
||||
# Add and commit changes
|
||||
git add .
|
||||
git commit -m "chore: crowdin locale sync"
|
||||
git commit -m "chore: automatic locale sync"
|
||||
|
||||
# Push the branch
|
||||
git push origin "$BRANCH_NAME"
|
||||
@@ -93,17 +96,20 @@ jobs:
|
||||
# Create PR using GitHub CLI with explicit repository
|
||||
gh pr create \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "chore(l10n): Crowdin locale sync" \
|
||||
--title "chore: automatic locale sync" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$BRANCH_NAME" \
|
||||
--label "l10n" \
|
||||
--body "## Summary
|
||||
|
||||
Automatically generated locale updates from the weekly sync job.
|
||||
|
||||
## Changes
|
||||
- Updated frontend locale files
|
||||
- Generated from latest translation sources" \
|
||||
- Generated from latest translation sources
|
||||
|
||||
## Test plan
|
||||
- [ ] Verify locale files are properly formatted
|
||||
- [ ] Test that translations load correctly in the frontend" \
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
1
.github/workflows/pull-request-lint.yml
vendored
1
.github/workflows/pull-request-lint.yml
vendored
@@ -31,7 +31,6 @@ jobs:
|
||||
deps
|
||||
auto
|
||||
l10n
|
||||
config
|
||||
# Configure that a scope must always be provided.
|
||||
requireScope: false
|
||||
# If the PR contains one of these newline-delimited labels, the
|
||||
|
||||
134
.github/workflows/release.yml
vendored
134
.github/workflows/release.yml
vendored
@@ -5,75 +5,19 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
commit-version-bump:
|
||||
name: Commit version bump to repository
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
commit-sha: ${{ steps.commit.outputs.commit-sha }}
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Extract Version From Tag Name
|
||||
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "mealie-commit-bot[bot]"
|
||||
git config user.email "mealie-commit-bot[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Update all version strings
|
||||
run: |
|
||||
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
|
||||
sed -i '/^name = "mealie"$/,/^version = / s/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' uv.lock
|
||||
sed -i 's/\("version": "\)[^"]*"/\1${{ env.VERSION_NUM }}"/' frontend/package.json
|
||||
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
|
||||
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
|
||||
- name: Commit and push changes
|
||||
id: commit
|
||||
run: |
|
||||
git add pyproject.toml frontend/package.json uv.lock docs/
|
||||
git commit -m "chore: bump version to ${{ github.event.release.tag_name }}"
|
||||
git push origin HEAD:${{ github.event.repository.default_branch }}
|
||||
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Move release tag to new commit
|
||||
run: |
|
||||
git tag -f ${{ github.event.release.tag_name }}
|
||||
git push -f origin ${{ github.event.release.tag_name }}
|
||||
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/test-backend.yml
|
||||
needs:
|
||||
- commit-version-bump
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend Tests"
|
||||
uses: ./.github/workflows/test-frontend.yml
|
||||
needs:
|
||||
- commit-version-bump
|
||||
|
||||
build-package:
|
||||
name: Build Package
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
needs:
|
||||
- commit-version-bump
|
||||
with:
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
tag: release
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
@@ -99,48 +43,10 @@ jobs:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
rollback-on-failure:
|
||||
name: Rollback version commit if deployment fails
|
||||
needs:
|
||||
- commit-version-bump
|
||||
- publish
|
||||
if: always() && needs.publish.result == 'failure'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "mealie-commit-bot[bot]"
|
||||
git config user.email "mealie-commit-bot[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Delete release tag
|
||||
run: |
|
||||
git push --delete origin ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Revert version bump commit
|
||||
run: |
|
||||
git revert --no-edit ${{ needs.commit-version-bump.outputs.commit-sha }}
|
||||
git push origin HEAD:${{ github.event.repository.default_branch }}
|
||||
|
||||
notify-discord:
|
||||
name: Notify Discord
|
||||
needs:
|
||||
- publish
|
||||
if: success()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord notification
|
||||
@@ -149,3 +55,41 @@ jobs:
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of Mealie has been released. See the release notes https://github.com/mealie-recipes/mealie/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}"
|
||||
|
||||
update-image-tags:
|
||||
name: Update image tag in sample docker-compose files
|
||||
needs:
|
||||
- publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Version From Tag Name
|
||||
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Modify version strings
|
||||
run: |
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
|
||||
sed -i 's/^\s*"version": "[^"]*"/"version": "${{ env.VERSION_NUM }}"/' frontend/package.json
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
# This doesn't currently work for us because it creates the PR but the workflows don't run.
|
||||
# TODO: Provide a personal access token as a parameter here, that solves that problem.
|
||||
# https://github.com/peter-evans/create-pull-request
|
||||
with:
|
||||
commit-message: "Update image tag, for release ${{ github.event.release.tag_name }}"
|
||||
branch: "docs/newrelease-update-version-${{ github.event.release.tag_name }}"
|
||||
labels: |
|
||||
documentation
|
||||
delete-branch: true
|
||||
base: mealie-next
|
||||
title: "docs(auto): Update image tag, for release ${{ github.event.release.tag_name }}"
|
||||
body: "Auto-generated by `.github/workflows/release.yml`, on publish of release ${{ github.event.release.tag_name }}"
|
||||
|
||||
22
.github/workflows/test-backend.yml
vendored
22
.github/workflows/test-backend.yml
vendored
@@ -49,21 +49,24 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-python-dependencies
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- name: Check venv cache
|
||||
id: cache-validate
|
||||
if: steps.cached-python-dependencies.outputs.cache-hit == 'true'
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "import fastapi;print('venv good?')" > test.py && uv run python test.py && echo "cache-hit-success=true" >> $GITHUB_OUTPUT
|
||||
echo "import fastapi;print('venv good?')" > test.py && poetry run python test.py && echo "cache-hit-success=true" >> $GITHUB_OUTPUT
|
||||
rm test.py
|
||||
continue-on-error: true
|
||||
|
||||
@@ -71,12 +74,13 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||
uv sync --group dev --extra pgsql
|
||||
if: steps.cached-python-dependencies.outputs.cache-hit != 'true' || steps.cache-validate.outputs.cache-hit-success != 'true'
|
||||
poetry install
|
||||
poetry add "psycopg2-binary==2.9.9"
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' || steps.cache-validate.outputs.cache-hit-success != 'true'
|
||||
|
||||
- name: Formatting (Ruff)
|
||||
run: |
|
||||
uv run ruff format . --check
|
||||
poetry run ruff format . --check
|
||||
|
||||
- name: Lint (Ruff)
|
||||
run: |
|
||||
|
||||
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run linter 👀
|
||||
run: yarn lint --max-warnings=0
|
||||
run: yarn lint
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run tests 🧪
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
exclude: "mkdocs.yml"
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.7
|
||||
rev: v0.12.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -55,15 +55,12 @@
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig",
|
||||
"pyproject.toml": "uv.lock, alembic.ini, .pylintrc",
|
||||
"pyproject.toml": "poetry.lock, alembic.ini, .pylintrc",
|
||||
"netlify.toml": "runtime.txt",
|
||||
"README.md": "LICENSE, SECURITY.md"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
51
Taskfile.yml
51
Taskfile.yml
@@ -28,7 +28,7 @@ tasks:
|
||||
docs:gen:
|
||||
desc: runs the API documentation generator
|
||||
cmds:
|
||||
- uv run python dev/code-generation/gen_docs_api.py
|
||||
- poetry run python dev/code-generation/gen_docs_api.py
|
||||
|
||||
docs:
|
||||
desc: runs the documentation server
|
||||
@@ -36,7 +36,7 @@ tasks:
|
||||
deps:
|
||||
- docs:gen
|
||||
cmds:
|
||||
- uv run python -m mkdocs serve
|
||||
- poetry run python -m mkdocs serve
|
||||
|
||||
setup:ui:
|
||||
desc: setup frontend dependencies
|
||||
@@ -54,10 +54,10 @@ tasks:
|
||||
desc: setup python dependencies
|
||||
run: once
|
||||
cmds:
|
||||
- uv sync --extra pgsql --group dev
|
||||
- uv run pre-commit install
|
||||
- poetry install --with main,dev,postgres
|
||||
- poetry run pre-commit install
|
||||
sources:
|
||||
- uv.lock
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- .pre-commit-config.yaml
|
||||
|
||||
@@ -70,8 +70,7 @@ tasks:
|
||||
dev:generate:
|
||||
desc: run code generators
|
||||
cmds:
|
||||
- uv run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
||||
- task: docs:gen
|
||||
- poetry run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
||||
- task: py:format
|
||||
|
||||
dev:services:
|
||||
@@ -88,30 +87,28 @@ tasks:
|
||||
- rm -r ./dev/data/recipes/
|
||||
- rm -r ./dev/data/users/
|
||||
- rm -f ./dev/data/mealie*.db
|
||||
- rm -f ./dev/data/mealie*.db-shm
|
||||
- rm -f ./dev/data/mealie*.db-wal
|
||||
- rm -f ./dev/data/mealie.log
|
||||
- rm -f ./dev/data/.secret
|
||||
|
||||
py:mypy:
|
||||
desc: runs python type checking
|
||||
cmds:
|
||||
- uv run mypy mealie
|
||||
- poetry run mypy mealie
|
||||
|
||||
py:test:
|
||||
desc: runs python tests (support args after '--')
|
||||
cmds:
|
||||
- uv run pytest {{ .CLI_ARGS }}
|
||||
- poetry run pytest {{ .CLI_ARGS }}
|
||||
|
||||
py:format:
|
||||
desc: runs python code formatter
|
||||
cmds:
|
||||
- uv run ruff format .
|
||||
- poetry run ruff format .
|
||||
|
||||
py:lint:
|
||||
desc: runs python linter
|
||||
cmds:
|
||||
- uv run ruff check mealie
|
||||
- poetry run ruff check mealie
|
||||
|
||||
py:check:
|
||||
desc: runs all linters, type checkers, and formatters
|
||||
@@ -124,10 +121,10 @@ tasks:
|
||||
py:coverage:
|
||||
desc: runs python coverage and generates html report
|
||||
cmds:
|
||||
- uv run pytest
|
||||
- uv run coverage report -m
|
||||
- uv run coveragepy-lcov
|
||||
- uv run coverage html
|
||||
- poetry run pytest
|
||||
- poetry run coverage report -m
|
||||
- poetry run coveragepy-lcov
|
||||
- poetry run coverage html
|
||||
- open htmlcov/index.html
|
||||
|
||||
py:package:copy-frontend:
|
||||
@@ -147,17 +144,17 @@ tasks:
|
||||
desc: Generate requirements file to pin all packages, effectively a "pip freeze" before installation begins
|
||||
internal: true
|
||||
cmds:
|
||||
- uv export --no-editable --no-emit-project --extra pgsql --format requirements-txt --output-file dist/requirements.txt
|
||||
- poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt
|
||||
# Include mealie in the requirements, hashing the package that was just built to ensure it's the one installed
|
||||
- echo "mealie[pgsql]=={{.MEALIE_VERSION}} \\" >> dist/requirements.txt
|
||||
- pip hash dist/mealie-{{.MEALIE_VERSION}}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
|
||||
- poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
|
||||
- echo " \\" >> dist/requirements.txt
|
||||
- pip hash dist/mealie-{{.MEALIE_VERSION}}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
- poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
vars:
|
||||
MEALIE_VERSION:
|
||||
sh: python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])"
|
||||
sh: poetry version --short
|
||||
sources:
|
||||
- uv.lock
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- dist/mealie-*.whl
|
||||
- dist/mealie-*.tar.gz
|
||||
@@ -184,13 +181,13 @@ tasks:
|
||||
deps:
|
||||
- py:package:deps
|
||||
cmds:
|
||||
- uv build --out-dir dist
|
||||
- poetry build -n --output=dist
|
||||
- task: py:package:generate-requirements
|
||||
|
||||
py:
|
||||
desc: runs the backend server
|
||||
cmds:
|
||||
- uv run python mealie/app.py
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
py:postgres:
|
||||
desc: runs the backend server configured for containerized postgres
|
||||
@@ -202,12 +199,12 @@ tasks:
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: mealie
|
||||
cmds:
|
||||
- uv run python mealie/app.py
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
py:migrate:
|
||||
desc: generates a new database migration file e.g. task py:migrate -- "add new column"
|
||||
cmds:
|
||||
- uv run alembic --config mealie/alembic/alembic.ini revision --autogenerate -m "{{ .CLI_ARGS }}"
|
||||
- poetry run alembic --config mealie/alembic/alembic.ini revision --autogenerate -m "{{ .CLI_ARGS }}"
|
||||
- task: py:format
|
||||
|
||||
ui:build:
|
||||
@@ -228,7 +225,7 @@ tasks:
|
||||
desc: runs the frontend linter
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn lint --max-warnings=0
|
||||
- yarn lint
|
||||
|
||||
ui:test:
|
||||
desc: runs the frontend tests
|
||||
|
||||
@@ -35,7 +35,7 @@ conventional_commits = true
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/mealie-recipes/mealie/issues/${2}))"},
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/hay-kot/mealie/issues/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from freezegun import freeze_time
|
||||
|
||||
from mealie.app import app
|
||||
from mealie.core.config import determine_data_dir
|
||||
@@ -38,43 +37,14 @@ HTML_TEMPLATE = """<!-- Custom HTML site displayed as the Home chapter -->
|
||||
"""
|
||||
|
||||
HTML_PATH = DATA_DIR.parent.parent.joinpath("docs/docs/overrides/api.html")
|
||||
CONSTANT_DT = datetime(2025, 10, 24, 15, 53, 0, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def normalize_timestamps(s: dict[str, Any]) -> dict[str, Any]:
|
||||
field_format = s.get("format")
|
||||
is_timestamp = field_format in ["date-time", "date", "time"]
|
||||
has_default = s.get("default")
|
||||
|
||||
if not is_timestamp:
|
||||
for k, v in s.items():
|
||||
if isinstance(v, dict):
|
||||
s[k] = normalize_timestamps(v)
|
||||
elif isinstance(v, list):
|
||||
s[k] = [normalize_timestamps(i) if isinstance(i, dict) else i for i in v]
|
||||
|
||||
return s
|
||||
elif not has_default:
|
||||
return s
|
||||
|
||||
if field_format == "date-time":
|
||||
s["default"] = CONSTANT_DT.isoformat()
|
||||
elif field_format == "date":
|
||||
s["default"] = CONSTANT_DT.date().isoformat()
|
||||
elif field_format == "time":
|
||||
s["default"] = CONSTANT_DT.time().isoformat()
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def generate_api_docs(my_app: FastAPI):
|
||||
openapi_schema = my_app.openapi()
|
||||
openapi_schema = normalize_timestamps(openapi_schema)
|
||||
|
||||
with open(HTML_PATH, "w") as fd:
|
||||
text = HTML_TEMPLATE.replace("MY_SPECIFIC_TEXT", json.dumps(openapi_schema))
|
||||
text = HTML_TEMPLATE.replace("MY_SPECIFIC_TEXT", json.dumps(my_app.openapi()))
|
||||
fd.write(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with freeze_time("2024-01-20T17:00:55Z"):
|
||||
generate_api_docs(app)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -106,16 +105,12 @@ def main():
|
||||
# Flatten list of lists
|
||||
all_children = [item for sublist in all_children for item in sublist]
|
||||
|
||||
out_path = GENERATED / "__init__.py"
|
||||
render_python_template(
|
||||
TEMPLATE,
|
||||
out_path,
|
||||
GENERATED / "__init__.py",
|
||||
{"children": all_children},
|
||||
)
|
||||
|
||||
subprocess.run(["uv", "run", "ruff", "check", str(out_path), "--fix"])
|
||||
subprocess.run(["uv", "run", "ruff", "format", str(out_path)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from utils import PROJECT_DIR, log, render_python_template
|
||||
@@ -85,23 +84,16 @@ def find_modules(root: pathlib.Path) -> list[Modules]:
|
||||
return modules
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def main():
|
||||
modules = find_modules(SCHEMA_PATH)
|
||||
|
||||
template_paths: list[pathlib.Path] = []
|
||||
for module in modules:
|
||||
log.debug(f"Module: {module.directory.name}")
|
||||
for file in module.files:
|
||||
log.debug(f" File: {file.import_path}")
|
||||
log.debug(f" Classes: [{', '.join(file.classes)}]")
|
||||
|
||||
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(["uv", "run", "ruff", "check", *path_args, "--fix"])
|
||||
subprocess.run(["uv", "run", "ruff", "format", *path_args])
|
||||
render_python_template(template, module.directory / "__init__.py", {"module": module})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -76,7 +76,7 @@ export const LOCALES = [{% for locale in locales %}
|
||||
progress: {{ locale.progress }},
|
||||
dir: "{{ locale.dir }}",
|
||||
},{% endfor %}
|
||||
];
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
@@ -173,36 +173,15 @@ the code generation ID is hardcoded into the script and required in the nuxt con
|
||||
|
||||
|
||||
def inject_nuxt_values():
|
||||
datetime_files = list(datetime_dir.glob("*.json"))
|
||||
datetime_files.sort()
|
||||
|
||||
datetime_imports = []
|
||||
datetime_object_entries = []
|
||||
|
||||
for match in datetime_files:
|
||||
# Convert locale name to camelCase variable name (e.g., "en-US" -> "enUS")
|
||||
var_name = match.stem.replace("-", "")
|
||||
|
||||
# Generate import statement
|
||||
import_line = f'import * as {var_name} from "./lang/dateTimeFormats/{match.name}";'
|
||||
datetime_imports.append(import_line)
|
||||
|
||||
# Generate object entry
|
||||
object_entry = f' "{match.stem}": {var_name},'
|
||||
datetime_object_entries.append(object_entry)
|
||||
|
||||
all_date_locales = datetime_imports + ["", "const datetimeFormats = {"] + datetime_object_entries + ["};"]
|
||||
all_date_locales = [
|
||||
f'"{match.stem}": require("./lang/dateTimeFormats/{match.name}"),' for match in datetime_dir.glob("*.json")
|
||||
]
|
||||
|
||||
all_langs = []
|
||||
for match in locales_dir.glob("*.json"):
|
||||
match_data = LOCALE_DATA.get(match.stem)
|
||||
match_dir = match_data.dir if match_data else "ltr"
|
||||
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}" }},'
|
||||
all_langs.append(lang_string)
|
||||
|
||||
all_langs.sort()
|
||||
|
||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import Template
|
||||
@@ -9,8 +8,8 @@ from utils import log
|
||||
# ============================================================
|
||||
|
||||
template = """// This Code is auto generated by gen_ts_types.py
|
||||
{% for name in global %}import type {{ name }} from "@/components/global/{{ name }}.vue";
|
||||
{% endfor %}{% for name in layout %}import type {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||
{% for name in global %}import {{ name }} from "@/components/global/{{ name }}.vue";
|
||||
{% endfor %}{% for name in layout %}import {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||
{% endfor %}
|
||||
declare module "vue" {
|
||||
export interface GlobalComponents {
|
||||
@@ -190,7 +189,6 @@ def generate_typescript_types() -> None: # noqa: C901
|
||||
skipped_dirs: list[Path] = []
|
||||
failed_modules: list[Path] = []
|
||||
|
||||
out_paths: list[Path] = []
|
||||
for module in schema_path.iterdir():
|
||||
if module.is_dir() and module.stem in ignore_dirs:
|
||||
skipped_dirs.append(module)
|
||||
@@ -207,18 +205,10 @@ def generate_typescript_types() -> None: # noqa: C901
|
||||
path_as_module = path_to_module(module)
|
||||
generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore
|
||||
clean_output_file(out_path)
|
||||
out_paths.append(out_path)
|
||||
except Exception:
|
||||
failed_modules.append(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:")
|
||||
for skipped_dir in skipped_dirs:
|
||||
log.debug(f" 📁 {skipped_dir.name}")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,6 +23,11 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict):
|
||||
|
||||
dest.write_text(text)
|
||||
|
||||
# lint/format file with Ruff
|
||||
log.info(f"Formatting {dest}")
|
||||
subprocess.run(["poetry", "run", "ruff", "check", str(dest), "--fix"])
|
||||
subprocess.run(["poetry", "run", "ruff", "format", str(dest)])
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeSlicer:
|
||||
@@ -31,7 +37,7 @@ class CodeSlicer:
|
||||
indentation: str | None
|
||||
text: list[str]
|
||||
|
||||
_next_line: int | None = None
|
||||
_next_line = None
|
||||
|
||||
def purge_lines(self) -> None:
|
||||
start = self.start + 1
|
||||
|
||||
24
dev/data/templates/recipes.md
Normal file
24
dev/data/templates/recipes.md
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||

|
||||
|
||||
# {{ recipe.name }}
|
||||
{{ recipe.description }}
|
||||
|
||||
## Ingredients
|
||||
{% for ingredient in recipe.recipeIngredient %}
|
||||
- [ ] {{ ingredient }} {% endfor %}
|
||||
|
||||
## Instructions
|
||||
{% for step in recipe.recipeInstructions %}
|
||||
- [ ] {{ step.text }} {% endfor %}
|
||||
|
||||
{% for note in recipe.notes %}
|
||||
**{{ note.title }}:** {{ note.text }}
|
||||
{% endfor %}
|
||||
|
||||
---
|
||||
|
||||
Tags: {{ recipe.tags }}
|
||||
Categories: {{ recipe.categories }}
|
||||
Original URL: {{ recipe.orgURL }}
|
||||
@@ -44,6 +44,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 cup unsalted butter, cut into cubes",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26",
|
||||
@@ -53,6 +54,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 cup light brown sugar",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82",
|
||||
@@ -62,6 +64,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1/2 cup granulated white sugar",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "034f481b-c426-4a17-b983-5aea9be4974b",
|
||||
@@ -71,6 +74,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 large eggs",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4",
|
||||
@@ -80,6 +84,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 tsp vanilla extract",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "85561ace-f249-401d-834c-e600a2f6280e",
|
||||
@@ -89,6 +94,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1/2 cup creamy peanut butter",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd",
|
||||
@@ -98,6 +104,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 tsp cornstarch",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0",
|
||||
@@ -107,6 +114,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 tsp baking soda",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "64627441-39f9-4ee3-8494-bafe36451d12",
|
||||
@@ -116,6 +124,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1/2 tsp salt",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384",
|
||||
@@ -125,6 +134,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 cup cake flour",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "06967994-8548-4952-a8cc-16e8db228ebd",
|
||||
@@ -134,6 +144,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 cups all-purpose flour",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691",
|
||||
@@ -143,6 +154,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 cups peanut butter chips",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef",
|
||||
@@ -152,6 +164,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1½ cups Reese's Pieces candies",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2",
|
||||
@@ -208,6 +221,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"showAssets": False,
|
||||
"landscapeView": False,
|
||||
"disableComments": False,
|
||||
"disableAmount": True,
|
||||
"locked": False,
|
||||
},
|
||||
"assets": [],
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
###############################################
|
||||
# Frontend Build
|
||||
###############################################
|
||||
FROM node:24@sha256:aa648b387728c25f81ff811799bbf8de39df66d7e2d9b3ab55cc6300cb9175d9 \
|
||||
AS frontend-builder
|
||||
FROM node:20 AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -21,8 +20,7 @@ RUN yarn generate
|
||||
###############################################
|
||||
# Base Image - Python
|
||||
###############################################
|
||||
FROM python:3.12-slim@sha256:2267adc248a477c1f1a852a07a5a224d42abe54c28aafa572efa157dfb001bba \
|
||||
AS python-base
|
||||
FROM python:3.12-slim as python-base
|
||||
|
||||
ENV MEALIE_HOME="/app"
|
||||
|
||||
@@ -50,29 +48,40 @@ RUN apt-get update \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install uv
|
||||
ENV POETRY_HOME="/opt/poetry" \
|
||||
POETRY_NO_INTERACTION=1
|
||||
|
||||
# prepend poetry to path
|
||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
ENV POETRY_VERSION=2.0.1
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# install poetry plugins needed to build the package
|
||||
RUN poetry self add "poetry-plugin-export>=1.9"
|
||||
|
||||
WORKDIR /mealie
|
||||
|
||||
# copy project files here to ensure they will be cached.
|
||||
COPY uv.lock pyproject.toml ./
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
COPY mealie ./mealie
|
||||
|
||||
# Copy frontend to package it into the wheel
|
||||
COPY --from=frontend-builder /frontend/dist ./mealie/frontend
|
||||
|
||||
# Build the source and binary package
|
||||
RUN uv build --out-dir dist
|
||||
RUN poetry build --output=dist
|
||||
|
||||
# Create the requirements file, which is used to install the built package and
|
||||
# its pinned dependencies later. mealie is included to ensure the built one is
|
||||
# what's installed.
|
||||
RUN uv export --no-editable --no-emit-project --extra pgsql --format requirements-txt --output-file dist/requirements.txt \
|
||||
&& MEALIE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") \
|
||||
&& echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt \
|
||||
&& pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \
|
||||
RUN export MEALIE_VERSION=$(poetry version --short) \
|
||||
&& poetry export --only=main --extras=pgsql --output=dist/requirements.txt \
|
||||
&& echo "mealie[pgsql]==$MEALIE_VERSION \\" >> dist/requirements.txt \
|
||||
&& poetry run pip hash dist/mealie-$MEALIE_VERSION-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \
|
||||
&& echo " \\" >> dist/requirements.txt \
|
||||
&& pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
&& poetry run pip hash dist/mealie-$MEALIE_VERSION.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
|
||||
###############################################
|
||||
# Package Container
|
||||
@@ -110,7 +119,7 @@ RUN . $VENV_PATH/bin/activate \
|
||||
###############################################
|
||||
# Production Image
|
||||
###############################################
|
||||
FROM python-base AS production
|
||||
FROM python-base as production
|
||||
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
||||
ENV PRODUCTION=true
|
||||
ENV TESTING=false
|
||||
@@ -123,7 +132,7 @@ RUN apt-get update \
|
||||
gosu \
|
||||
iproute2 \
|
||||
libldap-common \
|
||||
libldap2 \
|
||||
libldap-2.5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# create directory used for Docker Secrets
|
||||
|
||||
@@ -12,13 +12,13 @@ yarnpkg generate
|
||||
popd
|
||||
rm -r mealie/frontend
|
||||
cp -a frontend/dist mealie/frontend
|
||||
uv build --out-dir dist
|
||||
uv export --no-editable --no-emit-project --extra pgsql --format requirements-txt --output-file dist/requirements.txt
|
||||
MEALIE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
||||
poetry build
|
||||
poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt
|
||||
MEALIE_VERSION=$(poetry version --short)
|
||||
echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt
|
||||
pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
|
||||
poetry run pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
|
||||
echo " \\" >> dist/requirements.txt
|
||||
pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
poetry run pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
```
|
||||
|
||||
The Python package can be installed with all of its dependencies pinned to the versions tested by the developers with:
|
||||
|
||||
@@ -33,8 +33,8 @@ Make sure the VSCode Dev Containers extension is installed, then select "Dev Con
|
||||
### Prerequisites
|
||||
|
||||
- [Python 3.12](https://www.python.org/downloads/)
|
||||
- [uv](https://docs.astral.sh/uv/)
|
||||
- [Node](https://nodejs.org/en/)
|
||||
- [Poetry](https://python-poetry.org/docs/#installation)
|
||||
- [Node v16.x](https://nodejs.org/en/)
|
||||
- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable)
|
||||
- [task](https://taskfile.dev/#/installation)
|
||||
|
||||
@@ -45,7 +45,7 @@ Once the prerequisites are installed you can cd into the project base directory
|
||||
=== "Linux / macOS"
|
||||
|
||||
```bash
|
||||
# Navigate To The Root Directory
|
||||
# Naviate To The Root Directory
|
||||
cd /path/to/project
|
||||
|
||||
# Utilize the Taskfile to Install Dependencies
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
Mealie supports adding the ingredients of a recipe to your [Bring](https://www.getbring.com/) shopping list, as you can
|
||||
see [here](https://docs.mealie.io/documentation/getting-started/features/#recipe-actions).
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
|
||||
In a lot of ways, Home Assistant is why this project exists! Since Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard.
|
||||
|
||||
## Display Today's Meal in Lovelace
|
||||
### Display Today's Meal in Lovelace
|
||||
|
||||
You can use the Mealie API to get access to meal plans in Home Assistant like in the image below.
|
||||
|
||||

|
||||
|
||||
## Steps:
|
||||
Steps:
|
||||
|
||||
### 1. Get your API Token
|
||||
#### 1. Get your API Token
|
||||
|
||||
Create an API token from Mealie's User Settings page (see [this page](https://docs.mealie.io/documentation/getting-started/api-usage/#getting-a-token) to learn how).
|
||||
Create an API token from Mealie's User Settings page (https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation)
|
||||
|
||||
### 2. Create Home Assistant Sensors
|
||||
#### 2. Create Home Assistant Sensors
|
||||
|
||||
Create REST sensors in home assistant to get the details of today's meal.
|
||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
|
||||
|
||||
Make sure the url and port (`http://mealie:9000`) matches your installation's address and _API_ port.
|
||||
Make sure the url and port (`http://mealie:9000` ) matches your installation's address and _API_ port.
|
||||
|
||||
```yaml
|
||||
rest:
|
||||
@@ -40,7 +40,7 @@ rest:
|
||||
unique_id: mealie_todays_meal_id
|
||||
```
|
||||
|
||||
### 3. Create a Camera Entity
|
||||
#### 3. Create a Camera Entity
|
||||
|
||||
We will create a camera entity to display the image of today's meal in Lovelace.
|
||||
|
||||
@@ -52,7 +52,7 @@ In the still image url field put in:
|
||||
Under the entity page for the new camera, rename it.
|
||||
e.g. `camera.mealie_todays_meal_image`
|
||||
|
||||
### 4. Create a Lovelace Card
|
||||
#### 4. Create a Lovelace Card
|
||||
|
||||
Create a picture entity card and set the entity to `mealie_todays_meal` and the camera entity to `camera.mealie_todays_meal_image` or set in the yaml directly.
|
||||
|
||||
@@ -76,4 +76,4 @@ card_mod:
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This requires an [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS.
|
||||
Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This requires an [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS.
|
||||
|
||||
@@ -12,10 +12,12 @@ var url = document.URL.endsWith('/') ?
|
||||
document.URL;
|
||||
var mealie = "http://localhost:8080";
|
||||
var group_slug = "home" // Change this to your group slug. You can obtain this from your URL after logging-in to Mealie
|
||||
var use_keywords= "&use_keywords=1" // Optional - use keywords from recipe - update to "" if you don't want that
|
||||
var edity = "&edit=1" // Optional - keep in edit mode - update to "" if you don't want that
|
||||
|
||||
if (mealie.slice(-1) === "/") {
|
||||
mealie = mealie.slice(0, -1)
|
||||
}
|
||||
var dest = mealie + "/g/" + group_slug + "/r/create/url?recipe_import_url=" + url;
|
||||
var dest = mealie + "/g/" + group_slug + "/r/create/url?recipe_import_url=" + url + use_keywords + edity;
|
||||
window.open(dest, "_blank");
|
||||
```
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
An easy way to add recipes to Mealie from an Apple device is via an Apple Shortcut. This is a short guide to install an configure a shortcut able to add recipes via a link or image(s).
|
||||
|
||||
!!! note
|
||||
If adding via images make sure to enable [Mealie's OpenAI Integration](https://docs.mealie.io/documentation/getting-started/installation/open-ai/)
|
||||
*Note: if adding via images make sure to enable [Mealie's openai integration](https://docs.mealie.io/documentation/getting-started/installation/open-ai/)*
|
||||
|
||||
## Javascript can only be run via Shortcuts on the Safari browser on MacOS and iOS. If you do not use Safari you may skip this section
|
||||
Some sites have begun blocking AI scraping bots, inadvertently blocking the recipe scraping library Mealie uses as well. To circumvent this, the shortcut uses javascript to capture the raw html loaded in the browser and sends that to mealie when possible.
|
||||
@@ -17,13 +16,12 @@ Settings app -> apps -> Shortcuts -> Advanced -> Allow Running Scripts
|
||||
|
||||
Shortcuts app -> Settings (CMD ,) -> Advanced -> Allow Running Scripts
|
||||
|
||||
## Initial Setup
|
||||
## Initial setup
|
||||
An API key is needed to authenticate with mealie. To create an api key for a user, navigate to http://YOUR_MEALIE_URL/user/profile/api-tokens. Alternatively you can create a key via the mealie home page by clicking the user's profile pic in the top left -> Api Tokens
|
||||
|
||||
The shortcut can be installed via **[This link](https://www.icloud.com/shortcuts/52834724050b42aebe0f2efd8d067360)**. Upon install, replace "MEALIE_API_KEY" with the API key generated previously and "MEALIE_URI" with the full URL used to access your mealie instance e.g. "http://10.0.0.5:9000" or "https://mealie.domain.com".
|
||||
|
||||
## Using the Shortcut
|
||||
## Using the shortcut
|
||||
Once installed, the shortcut will automatically appear as an option when sharing an image or webpage. It can also be useful to add the shortcut to the home screen of your device. If selected from the home screen or shortcuts app, a menu will appear with prompts to import via **taking photo(s)**, **selecting photo(s)**, **scanning a URL**, or **pasting a URL**.
|
||||
|
||||
!!! note
|
||||
Despite the Mealie API being able to accept multiple recipe images for import it is currently impossible to send multiple files in 1 web request via Shortcuts. Instead, the shortcut combines the images into a singular, vertically-concatenated image to send to mealie. This can result in slightly less-accurate text recognition.
|
||||
*Note: despite the mealie API being able to accept multiple recipe images for import it is currently impossible to send multiple files in 1 web request via Shortcuts. Instead, the shortcut combines the images into a singular, vertically-concatenated image to send to mealie. This can result in slightly less-accurate text recognition.*
|
||||
@@ -1,77 +1,71 @@
|
||||
# Automating Backups with n8n
|
||||
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
[n8n](https://github.com/n8n-io/n8n) is a free and source-available fair-code licensed workflow automation tool. It's an alternative to tools like Zapier or Make, allowing you to use a UI to create automated workflows.
|
||||
> [n8n](https://github.com/n8n-io/n8n) is a free and source-available fair-code licensed workflow automation tool. Alternative to Zapier or Make, allowing you to use a UI to create automated workflows.
|
||||
|
||||
This example workflow:
|
||||
|
||||
1. Creates a Mealie backup every morning via an API call
|
||||
2. Keeps the last 7 backups, deleting older ones
|
||||
1. Backups Mealie every morning via an API call
|
||||
2. Deletes all but the last 7 backups
|
||||
|
||||
!!! warning "Important"
|
||||
This only automates the backup function, this does not backup your data to anywhere except your local instance. Please make sure you are backing up your data to an external source.
|
||||
> [!CAUTION]
|
||||
> This only automates the backup function, this does not backup your data to anywhere except your local instance. Please make sure you are backing up your data to an external source.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
# Setup
|
||||
|
||||
### Deploying n8n
|
||||
## Deploying n8n
|
||||
|
||||
Follow the relevant guide in the [n8n Documentation](https://docs.n8n.io/)
|
||||
|
||||
### Importing n8n workflow
|
||||
## Importing n8n workflow
|
||||
|
||||
1. In n8n, add a new workflow
|
||||
2. In the top right hit the 3 dot menu and select 'Import from URL...'
|
||||
|
||||

|
||||

|
||||
|
||||
3. Paste `https://github.com/mealie-recipes/mealie/blob/mealie-next/docs/docs/assets/other/n8n/n8n-mealie-backup.json` and click 'Import'
|
||||
3. Paste `https://github.com/mealie-recipes/mealie/blob/mealie-next/docs/docs/assets/other/n8n/n8n-mealie-backup.json` and click Import
|
||||
4. Click through the nodes and update the URLs for your environment
|
||||
|
||||
### API Credentials
|
||||
## API Credentials
|
||||
|
||||
#### Generate Mealie API Token
|
||||
|
||||
1. Head to `<YOUR MEALIE INSTANCE>/user/profile/api-tokens`
|
||||
|
||||
!!! tip
|
||||
If you dont see this screen make sure that "Show advanced features" is checked under `<YOUR MEALIE INSTANCE>/user/profile/edit`
|
||||
|
||||
2. Under token name, enter the name of the token (for example, 'n8n') and hit 'Generate'
|
||||
|
||||
1. Head to https://mealie.example.com/user/profile/api-tokens
|
||||
> If you dont see this screen make sure that "Show advanced features" is checked under https://mealie.example.com/user/profile/edit
|
||||
2. Under token name, enter the name of the token i.e. 'n8n' and hit Generate
|
||||
3. Copy and keep this API Token somewhere safe, this is like your password!
|
||||
|
||||
!!! tip
|
||||
You can use your normal user for this, but assuming you're an admin you could also choose to create a user named n8n and generate the API key against that user.
|
||||
> You can use your normal user for this, but assuming you're an admin you could also choose to create a user named n8n and generate the API key against that user.
|
||||
|
||||
#### Setup Credentials in n8n
|
||||
|
||||
See also [n8n Docs](https://docs.n8n.io/credentials/add-edit-credentials/).
|
||||
> [n8n Docs](https://docs.n8n.io/credentials/add-edit-credentials/)
|
||||
|
||||
1. Create a new "Header Auth" Credential
|
||||
|
||||

|
||||

|
||||
|
||||
2. In the connection screen set - Name as `Authorization` - Value as `Bearer {INSERT MEALIE API KEY}`
|
||||
|
||||

|
||||

|
||||
|
||||
3. In the workflow you created, for the "Run Backup", "Get All backups", and "Delete Oldies" nodes, update:
|
||||
|
||||
- Authentication to `Generic Credential Type`
|
||||
- Generic Auth Type to `Header Auth`
|
||||
- Header Auth to `Mealie API` or whatever you named your credentials
|
||||
|
||||

|
||||

|
||||
|
||||
### Notification Node
|
||||
## Notification Node
|
||||
|
||||
!!! warning "Important"
|
||||
Please use error notifications of some kind. It's very easy to set and forget an automation, then have the worst happen and lose data.
|
||||
> Please use error notifications of some kind. It's very easy to set and forget an automation, then have the worst happen and lose data.
|
||||
|
||||
[ntfy](https://github.com/binwiederhier/ntfy) is a great open source, self-hostable tool for sending notifications.
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag).
|
||||
|
||||
|
||||
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag)
|
||||
SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt™) sets up an Nginx web server and reverse proxy with PHP support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.
|
||||
|
||||
## Step 1: Get a domain
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
|
||||
You can change the theme by settings the environment variables.
|
||||
|
||||
- [Backend Config - Theming](./installation/backend-config.md#theming)
|
||||
- [Backend Config - Themeing](./installation/backend-config.md#themeing)
|
||||
|
||||
|
||||
??? question "How can I change the login session timeout?"
|
||||
|
||||
@@ -22,7 +22,6 @@ Mealie supports importing recipes from a few other sources besides websites. Cur
|
||||
- Recipe Keeper
|
||||
- Copy Me That
|
||||
- My Recipe Box
|
||||
- DVO Cook'n X3
|
||||
|
||||
You can access these options on your installation at the `/group/migrations` page on your installation. If you'd like to see another source added, feel free to request so on Github.
|
||||
|
||||
@@ -88,7 +87,6 @@ The shopping lists feature is a great way to keep track of what you need to buy
|
||||
Managing shopping lists can be done from the Sidebar > Shopping Lists.
|
||||
|
||||
Here you will be able to:
|
||||
|
||||
- See items already on the Shopping List
|
||||
- See linked recipes with ingredients
|
||||
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
|
||||
@@ -119,7 +117,6 @@ Mealie is designed to integrate with many different external services. There are
|
||||
### Notifiers
|
||||
|
||||
Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include:
|
||||
|
||||
- Creating / Updating a recipe
|
||||
- Adding items to a shopping list
|
||||
- Creating a new mealplan
|
||||
@@ -201,7 +198,6 @@ Mealie lets you fully customize how you organize your users. You can use Groups
|
||||
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
|
||||
|
||||
Common use cases for groups include:
|
||||
|
||||
- Hosting multiple instances of Mealie for others who want to keep their data private and secure
|
||||
- Creating completely isolated recipe pools
|
||||
|
||||
@@ -210,7 +206,6 @@ Common use cases for groups include:
|
||||
Households are subdivisions within a single Group. Households maintain their own users and settings, while sharing their recipes with other households. Households also share organizers (tags, categories, etc.) with the entire group. Meal Plans, Shopping Lists, and Integrations are only accessible within a household.
|
||||
|
||||
Common use cases for households include:
|
||||
|
||||
- Sharing a common recipe pool amongst families
|
||||
- Maintaining separate meal plans and shopping lists from other households
|
||||
- Maintaining separate integrations and customizations from other households
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
### General
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------- | :-------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ----------------------------- | :-------------------: | -------------------------------------------------------------------------------------------------- |
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| DEFAULT_HOUSEHOLD | Family | The default household for users in each group |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid. Must be <= 9600 (400 days, in hours). |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
| API_DOCS | True | Turns on/off access to the API documentation locally |
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
@@ -33,9 +33,8 @@
|
||||
### Database
|
||||
|
||||
| Variables | Default | Description |
|
||||
|---------------------------------------------------------|:--------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| ------------------------------------------------------- | :------: | ----------------------------------------------------------------------- |
|
||||
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
||||
| SQLITE_MIGRATE_JOURNAL_WAL | False | If set to true, switches SQLite's journal mode to WAL, which allows for multiple concurrent accesses. This can be useful when you have a decent amount of concurrency or when using certain remote storage systems such as Ceph. |
|
||||
| POSTGRES_USER<super>[†][secrets]</super> | mealie | Postgres database user |
|
||||
| POSTGRES_PASSWORD<super>[†][secrets]</super> | mealie | Postgres database password |
|
||||
| POSTGRES_SERVER<super>[†][secrets]</super> | postgres | Postgres database server address |
|
||||
@@ -132,19 +131,12 @@ For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
|
||||
### Theming
|
||||
|
||||
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
|
||||
|
||||
!!! info
|
||||
If you're setting these variables but not seeing these changes persist, try removing the `#` character. Also, depending on which syntax you're using, double-check you're using quotes correctly.
|
||||
|
||||
If using YAML mapping syntax, be sure to include quotes around these values, otherwise they will be treated as comments in your YAML file:<br>`THEME_LIGHT_PRIMARY: '#E58325'` or `THEME_LIGHT_PRIMARY: 'E58325'`
|
||||
|
||||
If using YAML sequence syntax, don't include any quotes:<br>`THEME_LIGHT_PRIMARY=#E58325` or `THEME_LIGHT_PRIMARY=E58325`
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------------- | :-----: | --------------------------- |
|
||||
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
||||
|
||||
@@ -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.6.1`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.8.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
|
||||
|
||||
@@ -60,7 +60,7 @@ The following steps were tested on a Ubuntu 20.04 server, but should work for mo
|
||||
|
||||
## Step 3: Customizing The `docker-compose.yaml` files.
|
||||
|
||||
After you've decided how to set up your files, it's important to set a few ENV variables to ensure that you can use all the features of Mealie. Verify that:
|
||||
After you've decided setup the files it's important to set a few ENV variables to ensure that you can use all the features of Mealie. I recommend that you verify and check that:
|
||||
|
||||
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
||||
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc). You can setup a [google app password](https://support.google.com/accounts/answer/185833?hl=en) if you want to send email via gmail.
|
||||
@@ -117,7 +117,7 @@ The latest tag provides the latest released image of Mealie.
|
||||
|
||||
---
|
||||
|
||||
**These tags are no longer updated**
|
||||
**These tags no are long updated**
|
||||
|
||||
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Installing with PostgreSQL
|
||||
|
||||
!!! Warning
|
||||
When upgrading postgresql major versions, manual steps are required [Postgres#37](https://github.com/docker-library/postgres/issues/37).
|
||||
|
||||
PostgreSQL might be considered if you need to support many concurrent users. In addition, some features are only enabled on PostgreSQL, such as fuzzy search.
|
||||
|
||||
**For Environment Variable Configuration, see** [Backend Configuration](./backend-config.md)
|
||||
@@ -10,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.6.1 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -41,7 +38,7 @@ services:
|
||||
|
||||
postgres:
|
||||
container_name: postgres
|
||||
image: postgres:17
|
||||
image: postgres:15
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-pgdata:/var/lib/postgresql/data
|
||||
@@ -49,7 +46,6 @@ services:
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
PGUSER: mealie
|
||||
POSTGRES_DB: mealie
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready"]
|
||||
interval: 30s
|
||||
|
||||
@@ -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.6.1 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -28,7 +28,6 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
- Copy Me That
|
||||
- Paprika
|
||||
- Tandoor Recipes
|
||||
- DVO Cook'n X3
|
||||
- Random Meal Plan generation
|
||||
- Advanced rule configuration to fine tune random recipes
|
||||
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
|
||||
## Feature Requests
|
||||
[Please request new features on Github](https://github.com/mealie-recipes/mealie/discussions/new?category=feature-request)
|
||||
|
||||
## Progress
|
||||
See the [Github Projects page](https://github.com/users/hay-kot/projects/2) to see what is currently being worked on
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
You MUST read the release notes prior to upgrading your container. Mealie has a robust backup and restore system for managing your data. Pre-v1.0.0 versions of Mealie use a different database structure, so if you are upgrading from pre-v1.0.0 to v1.0.0, you MUST backup your data and then re-import it. Even if you are already on v1.0.0, it is strongly recommended to backup all data before updating.
|
||||
|
||||
### Before Upgrading
|
||||
- [Read The Release Notes](https://github.com/mealie-recipes/mealie/releases)
|
||||
- Read The Release Notes
|
||||
- Identify Breaking Changes
|
||||
- Create a Backup and Download from the UI
|
||||
- Upgrade
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Mealie provides an integrated mechanic for doing full installation backups of the database.
|
||||
|
||||
Navigate to Settings > Admin Settings > Backups or manually by adding `/admin/backups` to your instance URL.
|
||||
Navigate to Settings > Backups or manually by adding `/admin/backups` to your instance URL.
|
||||
|
||||
From this page, you will be able to:
|
||||
|
||||
@@ -39,7 +39,7 @@ Restoring the Database when using Postgres requires Mealie to be configured with
|
||||
```sql
|
||||
ALTER USER mealie WITH SUPERUSER;
|
||||
|
||||
-- Run restore from Mealie
|
||||
# Run restore from Mealie
|
||||
|
||||
ALTER USER mealie WITH NOSUPERUSER;
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Permissions and Public Access
|
||||
|
||||
Mealie provides various levels of user access and permissions. This includes:
|
||||
|
||||
- Authentication and registration ([LDAP](../authentication/ldap.md) and [OpenID Connect](../authentication/oidc.md) are both supported)
|
||||
- Customizable user permissions
|
||||
- Fine-tuned public access for non-users
|
||||
@@ -9,12 +8,12 @@ Mealie provides various levels of user access and permissions. This includes:
|
||||
## Customizable User Permissions
|
||||
|
||||
Each user can be configured to have varying levels of access. Some of these permissions include:
|
||||
|
||||
- Access to Administrator tools
|
||||
- Access to inviting other users
|
||||
- Access to manage their group and group data
|
||||
|
||||
Administrators can configure these settings on the User Management page (navigate to Settings > Admin Settings > Users or append `/admin/manage/users` to your instance URL).
|
||||
Administrators can navigate to the Settings page and access the User Management page to configure these settings.
|
||||
|
||||
|
||||
[User Management Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
|
||||
@@ -23,8 +22,8 @@ Administrators can configure these settings on the User Management page (navigat
|
||||
By default, groups and households are set to private, meaning only logged-in users may access the group/household. In order for a recipe to be viewable by public (not logged-in) users, three criteria must be met:
|
||||
|
||||
1. The group must not be private
|
||||
2. The household must not be private, _and_ the household setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Household Management page (navigate to Settings > Admin Settings > Households or append `/admin/manage/households` to your instance URL)
|
||||
3. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page
|
||||
2. The household must not be private, *and* the household setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Household Settings page
|
||||
2. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page
|
||||
|
||||
Additionally, if the group is not private, public users can view all public group data (public recipes, public cookbooks, etc.) from the home page ([e.g. the demo home page](https://demo.mealie.io/g/home)).
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -351,7 +351,7 @@
|
||||
<!-- Custom narrow footer -->
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-footer-social">
|
||||
<a class="md-footer-social__link" href="https://github.com/mealie-recipes/mealie" rel="noopener" target="_blank"
|
||||
<a class="md-footer-social__link" href="https://github.com/hay-kot/mealie" rel="noopener" target="_blank"
|
||||
title="github.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@@ -86,7 +86,7 @@ nav:
|
||||
|
||||
- Community Guides:
|
||||
- Bring API without internet exposure: "documentation/community-guide/bring-api.md"
|
||||
- Automating Backups with n8n: "documentation/community-guide/n8n-backup-automation.md"
|
||||
- Automate Backups with n8n: "documentation/community-guide/n8n-backup-automation.md"
|
||||
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
||||
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
||||
- Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md"
|
||||
|
||||
390
frontend/assets/css/fonts.css
Normal file
390
frontend/assets/css/fonts.css
Normal file
@@ -0,0 +1,390 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-cyrillic-ext1.woff2") format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-cyrillic2.woff2") format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-greek-ext3.woff2") format("woff2");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-greek4.woff2") format("woff2");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-vietnamese5.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-latin-ext6.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-100-latin7.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-cyrillic-ext8.woff2") format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-cyrillic9.woff2") format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-greek-ext10.woff2") format("woff2");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-greek11.woff2") format("woff2");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-vietnamese12.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-latin-ext13.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-300-latin14.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-cyrillic-ext15.woff2") format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-cyrillic16.woff2") format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-greek-ext17.woff2") format("woff2");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-greek18.woff2") format("woff2");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-vietnamese19.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-latin-ext20.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-400-latin21.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-cyrillic-ext22.woff2") format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-cyrillic23.woff2") format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-greek-ext24.woff2") format("woff2");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-greek25.woff2") format("woff2");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-vietnamese26.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-latin-ext27.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-500-latin28.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-cyrillic-ext29.woff2") format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-cyrillic30.woff2") format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-greek-ext31.woff2") format("woff2");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-greek32.woff2") format("woff2");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-vietnamese33.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-latin-ext34.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-700-latin35.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-cyrillic-ext36.woff2") format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-cyrillic37.woff2") format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-greek-ext38.woff2") format("woff2");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-greek39.woff2") format("woff2");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-vietnamese40.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-latin-ext41.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("~assets/fonts/Roboto-900-latin42.woff2") format("woff2");
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab !important;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
BIN
frontend/assets/fonts/Roboto-100-cyrillic-ext1.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-cyrillic-ext1.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-cyrillic2.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-cyrillic2.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-greek-ext3.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-greek-ext3.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-greek4.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-greek4.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-latin-ext6.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-latin-ext6.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-latin7.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-latin7.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-100-vietnamese5.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-100-vietnamese5.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-cyrillic-ext8.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-cyrillic-ext8.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-cyrillic9.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-cyrillic9.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-greek-ext10.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-greek-ext10.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-greek11.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-greek11.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-latin-ext13.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-latin-ext13.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-latin14.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-latin14.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-300-vietnamese12.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-300-vietnamese12.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-cyrillic-ext15.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-cyrillic-ext15.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-cyrillic16.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-cyrillic16.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-greek-ext17.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-greek-ext17.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-greek18.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-greek18.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-latin-ext20.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-latin-ext20.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-latin21.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-latin21.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-400-vietnamese19.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-400-vietnamese19.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-cyrillic-ext22.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-cyrillic-ext22.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-cyrillic23.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-cyrillic23.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-greek-ext24.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-greek-ext24.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-greek25.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-greek25.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-latin-ext27.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-latin-ext27.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-latin28.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-latin28.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-500-vietnamese26.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-500-vietnamese26.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-cyrillic-ext29.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-cyrillic-ext29.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-cyrillic30.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-cyrillic30.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-greek-ext31.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-greek-ext31.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-greek32.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-greek32.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-latin-ext34.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-latin-ext34.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-latin35.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-latin35.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-700-vietnamese33.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-700-vietnamese33.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-cyrillic-ext36.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-cyrillic-ext36.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-cyrillic37.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-cyrillic37.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-greek-ext38.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-greek-ext38.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-greek39.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-greek39.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-latin-ext41.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-latin-ext41.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-latin42.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-latin42.woff2
Normal file
Binary file not shown.
BIN
frontend/assets/fonts/Roboto-900-vietnamese40.woff2
Normal file
BIN
frontend/assets/fonts/Roboto-900-vietnamese40.woff2
Normal file
Binary file not shown.
@@ -44,20 +44,36 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
const modelValue = defineModel<ReadCookBook>({ required: true });
|
||||
const i18n = useI18n();
|
||||
const cookbook = toRef(modelValue);
|
||||
function handleInput(value: string | undefined) {
|
||||
export default defineNuxtComponent({
|
||||
components: { QueryFilterBuilder },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => ReadCookBook,
|
||||
required: true,
|
||||
},
|
||||
actions: {
|
||||
type: Object as () => any,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const cookbook = toRef(() => props.modelValue);
|
||||
|
||||
function handleInput(value: string | undefined) {
|
||||
cookbook.value.queryFilterString = value || "";
|
||||
}
|
||||
emit("update:modelValue", cookbook.value);
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.t("category.categories"),
|
||||
@@ -93,5 +109,13 @@ const fieldDefs: FieldDefinition[] = [
|
||||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
return {
|
||||
cookbook,
|
||||
handleInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<v-card-text>
|
||||
<CookbookEditor
|
||||
v-model="editTarget"
|
||||
:actions="actions"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
@@ -64,49 +65,53 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardSection, CookbookEditor },
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
|
||||
const book = getOne(slug);
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
|
||||
const isOwnHousehold = computed(() => {
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user.value && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth.user.value.householdId === book.value.householdId;
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
|
||||
const dialogStates = reactive({
|
||||
const dialogStates = reactive({
|
||||
edit: false,
|
||||
});
|
||||
});
|
||||
|
||||
const editTarget = ref<ReadCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
const editTarget = ref<RecipeCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
dialogStates.edit = true;
|
||||
editTarget.value = book.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function editCookbook() {
|
||||
async function editCookbook() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
@@ -122,9 +127,28 @@ async function editCookbook() {
|
||||
}
|
||||
dialogStates.edit = false;
|
||||
editTarget.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
useSeoMeta({
|
||||
title: book?.value?.name || "Cookbook",
|
||||
});
|
||||
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
canEdit,
|
||||
dialogStates,
|
||||
editTarget,
|
||||
handleEditCookbook,
|
||||
editCookbook,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,33 +20,45 @@
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||
import type { GroupDataExport } from "~/lib/api/types/group";
|
||||
|
||||
defineProps<{
|
||||
exports: GroupDataExport[];
|
||||
}>();
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
exports: {
|
||||
type: Array as () => GroupDataExport[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const headers = [
|
||||
const headers = [
|
||||
{ title: i18n.t("export.export"), value: "name" },
|
||||
{ title: i18n.t("export.file-name"), value: "filename" },
|
||||
{ title: i18n.t("export.size"), value: "size" },
|
||||
{ title: i18n.t("export.link-expires"), value: "expires" },
|
||||
{ title: "", value: "actions" },
|
||||
];
|
||||
];
|
||||
|
||||
function getTimeToExpire(timeString: string) {
|
||||
function getTimeToExpire(timeString: string) {
|
||||
const expiresAt = parseISO(timeString);
|
||||
|
||||
return formatDistanceToNow(expiresAt, {
|
||||
addSuffix: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function downloadData(_: any) {
|
||||
function downloadData(_: any) {
|
||||
console.log("Downloading data...");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
downloadData,
|
||||
headers,
|
||||
getTimeToExpire,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,10 +9,30 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||
return {
|
||||
preferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-select
|
||||
v-model="selected"
|
||||
:items="households"
|
||||
:label="label"
|
||||
:hint="description"
|
||||
:persistent-hint="!!description"
|
||||
item-title="name"
|
||||
:multiple="multiselect"
|
||||
:prepend-inner-icon="$globals.icons.household"
|
||||
return-object
|
||||
>
|
||||
<template #chip="data">
|
||||
<v-chip
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.item"
|
||||
size="small"
|
||||
closable
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
@click:close="removeByIndex(data.index)"
|
||||
>
|
||||
{{ data.item.raw.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useHouseholdStore } from "~/composables/store/use-household-store";
|
||||
|
||||
interface HouseholdLike {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as () => HouseholdLike[],
|
||||
required: true,
|
||||
},
|
||||
multiselect: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
selected.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const label = computed(
|
||||
() => props.multiselect ? i18n.t("household.households") : i18n.t("household.household"),
|
||||
);
|
||||
|
||||
const { store: households } = useHouseholdStore();
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
label,
|
||||
households,
|
||||
removeByIndex,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
:open-on-hover="mdAndUp"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
:class="{ 'rounded-circle': fab }"
|
||||
:size="fab ? 'small' : undefined"
|
||||
@@ -26,7 +26,7 @@
|
||||
:icon="!fab"
|
||||
variant="text"
|
||||
dark
|
||||
v-bind="activatorProps"
|
||||
v-bind="props"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||
import type { ShoppingListSummary } from "~/lib/api/types/household";
|
||||
@@ -64,32 +64,40 @@ export interface ContextMenuItem {
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
recipes?: Recipe[];
|
||||
menuTop?: boolean;
|
||||
fab?: boolean;
|
||||
color?: string;
|
||||
menuIcon?: string | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipes: () => [],
|
||||
menuTop: true,
|
||||
fab: false,
|
||||
color: "primary",
|
||||
menuIcon: null,
|
||||
});
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
},
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
menuIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
const emit = defineEmits<{
|
||||
[key: string]: [];
|
||||
}>();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const api = useUserApi();
|
||||
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
shoppingListDialog: false,
|
||||
menuItems: [
|
||||
@@ -101,38 +109,36 @@ const state = reactive({
|
||||
isPublic: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const { shoppingListDialog, menuItems } = toRefs(state);
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipesWithScales = computed(() => {
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipesWithScales = computed(() => {
|
||||
return props.recipes.map((recipe) => {
|
||||
return {
|
||||
scale: 1,
|
||||
...recipe,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function getShoppingLists() {
|
||||
async function getShoppingLists() {
|
||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||
if (data) {
|
||||
shoppingLists.value = data.items as ShoppingListSummary[] ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
state.shoppingListDialog = true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
const handler = eventHandlers[eventKey];
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
@@ -141,7 +147,18 @@ function contextMenuEventHandler(eventKey: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(eventKey);
|
||||
context.emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
contextMenuEventHandler,
|
||||
icon,
|
||||
recipesWithScales,
|
||||
shoppingLists,
|
||||
mdAndUp,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
style="gap: 10px"
|
||||
>
|
||||
<v-select
|
||||
v-model="day"
|
||||
v-model="inputDay"
|
||||
:items="MEAL_DAY_OPTIONS"
|
||||
:label="$t('meal-plan.rule-day')"
|
||||
/>
|
||||
<v-select
|
||||
v-model="entryType"
|
||||
v-model="inputEntryType"
|
||||
:items="MEAL_TYPE_OPTIONS"
|
||||
:label="$t('meal-plan.meal-type')"
|
||||
/>
|
||||
@@ -19,49 +19,64 @@
|
||||
<div class="mb-5">
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="props.queryFilter"
|
||||
:initial-query-filter="queryFilter"
|
||||
@input="handleQueryFilterInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: proper pluralization of inputDay -->
|
||||
{{ $t('meal-plan.this-rule-will-apply', {
|
||||
dayCriteria: day === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [day]),
|
||||
mealTypeCriteria: entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [entryType]),
|
||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType]),
|
||||
}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
||||
|
||||
interface Props {
|
||||
queryFilter?: QueryFilterJSON | null;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
queryFilter: null,
|
||||
showHelp: false,
|
||||
});
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
QueryFilterBuilder,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
entryType: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
queryFilterString: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
queryFilter: {
|
||||
type: Object as () => QueryFilterJSON,
|
||||
default: null,
|
||||
},
|
||||
showHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:day", "update:entry-type", "update:query-filter-string"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const day = defineModel<string>("day", { default: "unset" });
|
||||
const entryType = defineModel<string>("entryType", { default: "unset" });
|
||||
const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||
];
|
||||
];
|
||||
|
||||
const MEAL_DAY_OPTIONS = [
|
||||
const MEAL_DAY_OPTIONS = [
|
||||
{ title: i18n.t("general.monday"), value: "monday" },
|
||||
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
||||
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
||||
@@ -70,14 +85,40 @@ const MEAL_DAY_OPTIONS = [
|
||||
{ title: i18n.t("general.saturday"), value: "saturday" },
|
||||
{ title: i18n.t("general.sunday"), value: "sunday" },
|
||||
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||
];
|
||||
];
|
||||
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
console.warn("handleQueryFilterInput called with value:", value);
|
||||
queryFilterString.value = value || "";
|
||||
}
|
||||
const inputDay = computed({
|
||||
get: () => {
|
||||
return props.day;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:day", val);
|
||||
},
|
||||
});
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
const inputEntryType = computed({
|
||||
get: () => {
|
||||
return props.entryType;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:entry-type", val);
|
||||
},
|
||||
});
|
||||
|
||||
const inputQueryFilterString = computed({
|
||||
get: () => {
|
||||
return props.queryFilterString;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:query-filter-string", val);
|
||||
},
|
||||
});
|
||||
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
inputQueryFilterString.value = value || "";
|
||||
};
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.t("category.categories"),
|
||||
@@ -118,5 +159,17 @@ const fieldDefs: FieldDefinition[] = [
|
||||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
return {
|
||||
MEAL_TYPE_OPTIONS,
|
||||
MEAL_DAY_OPTIONS,
|
||||
inputDay,
|
||||
inputEntryType,
|
||||
inputQueryFilterString,
|
||||
handleQueryFilterInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
:label="$t('settings.webhooks.webhook-url')"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-text-field
|
||||
<v-time-picker
|
||||
v-model="scheduledTime"
|
||||
type="time"
|
||||
clearable
|
||||
variant="underlined"
|
||||
class="elevation-2"
|
||||
ampm-in-title
|
||||
format="ampm"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="py-0 justify-end">
|
||||
@@ -50,25 +50,24 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import type { ReadWebhook } from "~/lib/api/types/household";
|
||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||
|
||||
const props = defineProps<{
|
||||
webhook: ReadWebhook;
|
||||
}>();
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
webhook: {
|
||||
type: Object as () => ReadWebhook,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["delete", "save", "test"],
|
||||
setup(props, { emit }) {
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
save: [webhook: ReadWebhook];
|
||||
test: [id: string];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
||||
const scheduledTime = computed({
|
||||
const scheduledTime = computed({
|
||||
get() {
|
||||
return itemLocal.value;
|
||||
},
|
||||
@@ -76,17 +75,27 @@ const scheduledTime = computed({
|
||||
itemUTC.value = timeLocalToUTC(v);
|
||||
itemLocal.value = v;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const webhookCopy = ref({ ...props.webhook });
|
||||
const webhookCopy = ref({ ...props.webhook });
|
||||
|
||||
function handleSave() {
|
||||
function handleSave() {
|
||||
webhookCopy.value.scheduledTime = itemLocal.value;
|
||||
emit("save", webhookCopy.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Set page title using useSeoMeta
|
||||
useSeoMeta({
|
||||
// Set page title using useSeoMeta
|
||||
useSeoMeta({
|
||||
title: i18n.t("settings.webhooks.webhooks"),
|
||||
});
|
||||
|
||||
return {
|
||||
webhookCopy,
|
||||
scheduledTime,
|
||||
handleSave,
|
||||
itemUTC,
|
||||
itemLocal,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -41,19 +41,27 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||
const i18n = useI18n();
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
type Preference = {
|
||||
key: keyof ReadHouseholdPreferences;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
const recipePreferences: Preference[] = [
|
||||
const recipePreferences: Preference[] = [
|
||||
{
|
||||
key: "recipePublic",
|
||||
label: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
@@ -79,9 +87,14 @@ const recipePreferences: Preference[] = [
|
||||
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
|
||||
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
|
||||
},
|
||||
];
|
||||
{
|
||||
key: "recipeDisableAmount",
|
||||
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
|
||||
},
|
||||
];
|
||||
|
||||
const allDays = [
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
@@ -110,7 +123,24 @@ const allDays = [
|
||||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
:model-value="field.value"
|
||||
type="number"
|
||||
variant="underlined"
|
||||
@update:model-value="setFieldValue(field, index, $event)"
|
||||
@:model-value="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-else-if="field.type === 'boolean'"
|
||||
@@ -163,14 +163,14 @@
|
||||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<template #activator="{ props }">
|
||||
<v-text-field
|
||||
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
|
||||
v-model="field.value"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
v-bind="activatorProps"
|
||||
v-bind="props"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
@@ -184,53 +184,53 @@
|
||||
</v-menu>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Category"
|
||||
v-model="field.organizers"
|
||||
:model-value="field.organizers"
|
||||
:selector-type="Organizer.Category"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tag"
|
||||
v-model="field.organizers"
|
||||
:model-value="field.organizers"
|
||||
:selector-type="Organizer.Tag"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tool"
|
||||
v-model="field.organizers"
|
||||
:model-value="field.organizers"
|
||||
:selector-type="Organizer.Tool"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Food"
|
||||
v-model="field.organizers"
|
||||
:model-value="field.organizers"
|
||||
:selector-type="Organizer.Food"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Household"
|
||||
v-model="field.organizers"
|
||||
:model-value="field.organizers"
|
||||
:selector-type="Organizer.Household"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<!-- right parenthesis -->
|
||||
@@ -297,7 +297,7 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
@@ -307,7 +307,12 @@ import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalK
|
||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||
|
||||
const props = defineProps({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
VueDraggable,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
fieldDefs: {
|
||||
type: Array as () => FieldDefinition[],
|
||||
required: true,
|
||||
@@ -316,62 +321,57 @@ const props = defineProps({
|
||||
type: Object as () => QueryFilterJSON | null,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
},
|
||||
emits: ["input", "inputJSON"],
|
||||
setup(props, context) {
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "input", value: string | undefined): void;
|
||||
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
|
||||
}>();
|
||||
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
const state = reactive({
|
||||
showAdvanced: false,
|
||||
qfValid: false,
|
||||
datePickers: [] as boolean[],
|
||||
drag: false,
|
||||
});
|
||||
const { showAdvanced, datePickers, drag } = toRefs(state);
|
||||
});
|
||||
|
||||
const storeMap = {
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
[Organizer.Tag]: useTagStore(),
|
||||
[Organizer.Tool]: useToolStore(),
|
||||
[Organizer.Food]: useFoodStore(),
|
||||
[Organizer.Household]: useHouseholdStore(),
|
||||
};
|
||||
};
|
||||
|
||||
function onDragEnd(event: any) {
|
||||
function onDragEnd(event: any) {
|
||||
state.drag = false;
|
||||
|
||||
const oldIndex: number = event.oldIndex;
|
||||
const newIndex: number = event.newIndex;
|
||||
state.datePickers[oldIndex] = false;
|
||||
state.datePickers[newIndex] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// add id to fields to prevent reactivity issues
|
||||
type FieldWithId = Field & { id: number };
|
||||
const fields = ref<FieldWithId[]>([]);
|
||||
// add id to fields to prevent reactivity issues
|
||||
type FieldWithId = Field & { id: number };
|
||||
const fields = ref<FieldWithId[]>([]);
|
||||
|
||||
const uid = ref(1); // init uid to pass to fields
|
||||
function useUid() {
|
||||
const uid = ref(1); // init uid to pass to fields
|
||||
function useUid() {
|
||||
return uid.value++;
|
||||
}
|
||||
function addField(field: FieldDefinition) {
|
||||
}
|
||||
function addField(field: FieldDefinition) {
|
||||
fields.value.push({
|
||||
...getFieldFromFieldDef(field),
|
||||
id: useUid(),
|
||||
});
|
||||
state.datePickers.push(false);
|
||||
}
|
||||
};
|
||||
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
state.datePickers[index] = false;
|
||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.label === fieldLabel);
|
||||
if (!fieldDef) {
|
||||
@@ -388,49 +388,48 @@ function setField(index: number, fieldLabel: string) {
|
||||
...getFieldFromFieldDef(updatedField, resetValue),
|
||||
id: fields.value[index].id, // keep the id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
fields.value[index].leftParenthesis = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
fields.value[index].rightParenthesis = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||
if (!value) {
|
||||
value = logOps.value.AND.value;
|
||||
}
|
||||
|
||||
fields.value[index].logicalOperator = value ? logOps.value[value] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||
state.datePickers[index] = false;
|
||||
fields.value[index].value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||
fields.value[index].values = values;
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldOrganizers(field: FieldWithId, index: number, organizers: OrganizerBase[]) {
|
||||
fields.value[index].organizers = organizers;
|
||||
// Sync the values array with the organizers array
|
||||
fields.value[index].values = organizers.map(org => org.id?.toString() || "").filter(id => id);
|
||||
}
|
||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
||||
setFieldValues(field, index, values.map(value => value.id.toString()));
|
||||
fields.value[index].organizers = values;
|
||||
}
|
||||
|
||||
function removeField(index: number) {
|
||||
function removeField(index: number) {
|
||||
fields.value.splice(index, 1);
|
||||
state.datePickers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
/* newFields.forEach((field, index) => {
|
||||
const updatedField = getFieldFromFieldDef(field);
|
||||
fields.value[index] = updatedField; // recursive!!!
|
||||
@@ -442,17 +441,19 @@ const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
}
|
||||
state.qfValid = !!qf;
|
||||
|
||||
emit("input", qf || undefined);
|
||||
emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 500);
|
||||
context.emit("input", qf || undefined);
|
||||
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 500);
|
||||
|
||||
watch(fields, fieldsUpdater, { deep: true });
|
||||
watch(fields, fieldsUpdater, { deep: true });
|
||||
|
||||
async function hydrateOrganizers(field: FieldWithId, _index: number) {
|
||||
async function hydrateOrganizers(field: FieldWithId, index: number) {
|
||||
if (!field.values?.length || !isOrganizerType(field.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.organizers = [];
|
||||
|
||||
const { store, actions } = storeMap[field.type];
|
||||
if (!store.value.length) {
|
||||
await actions.refresh();
|
||||
@@ -466,12 +467,11 @@ async function hydrateOrganizers(field: FieldWithId, _index: number) {
|
||||
}
|
||||
return organizer;
|
||||
});
|
||||
|
||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||
return field;
|
||||
}
|
||||
setOrganizerValues(field, index, field.organizers);
|
||||
}
|
||||
|
||||
function initFieldsError(error = "") {
|
||||
function initFieldsError(error = "") {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -480,17 +480,16 @@ function initFieldsError(error = "") {
|
||||
if (props.fieldDefs.length) {
|
||||
addField(props.fieldDefs[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeFields() {
|
||||
function initializeFields() {
|
||||
if (!props.initialQueryFilter?.parts?.length) {
|
||||
return initFieldsError();
|
||||
}
|
||||
};
|
||||
|
||||
const initFields: FieldWithId[] = [];
|
||||
let error = false;
|
||||
|
||||
for (const [index, part] of props.initialQueryFilter.parts.entries()) {
|
||||
props.initialQueryFilter.parts.forEach((part: QueryFilterJSONPart, index: number) => {
|
||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.name === part.attributeName);
|
||||
if (!fieldDef) {
|
||||
error = true;
|
||||
@@ -523,7 +522,7 @@ async function initializeFields() {
|
||||
}
|
||||
|
||||
if (isOrganizerType(field.type)) {
|
||||
await hydrateOrganizers(field, index);
|
||||
hydrateOrganizers(field, index);
|
||||
}
|
||||
}
|
||||
else if (field.type === "boolean") {
|
||||
@@ -554,7 +553,7 @@ async function initializeFields() {
|
||||
}
|
||||
|
||||
initFields.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
if (initFields.length && !error) {
|
||||
fields.value = initFields;
|
||||
@@ -562,18 +561,16 @@ async function initializeFields() {
|
||||
else {
|
||||
initFieldsError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await initializeFields();
|
||||
initializeFields();
|
||||
}
|
||||
catch (error) {
|
||||
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||
}
|
||||
});
|
||||
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
const parts = fields.value.map((field) => {
|
||||
const part: QueryFilterJSONPart = {
|
||||
attributeName: field.name,
|
||||
@@ -599,9 +596,9 @@ function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
const qfJSON = { parts } as QueryFilterJSON;
|
||||
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
||||
return qfJSON;
|
||||
}
|
||||
}
|
||||
|
||||
const config = computed(() => {
|
||||
const config = computed(() => {
|
||||
const baseColMaxWidth = 55;
|
||||
return {
|
||||
col: {
|
||||
@@ -645,6 +642,30 @@ const config = computed(() => {
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
logOps,
|
||||
relOps,
|
||||
config,
|
||||
firstDayOfWeek,
|
||||
onDragEnd,
|
||||
// Fields
|
||||
fields,
|
||||
addField,
|
||||
setField,
|
||||
setLeftParenthesisValue,
|
||||
setRightParenthesisValue,
|
||||
setLogicalOperatorValue,
|
||||
setRelationalOperatorValue,
|
||||
setFieldValue,
|
||||
setFieldValues,
|
||||
setOrganizerValues,
|
||||
removeField,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,14 +5,8 @@
|
||||
density="compact"
|
||||
elevation="0"
|
||||
>
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
:title="$t('recipe.delete-recipe')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
can-confirm
|
||||
@confirm="emitDelete()"
|
||||
>
|
||||
<BaseDialog v-model="deleteDialog" :title="$t('recipe.delete-recipe')" color="error"
|
||||
:icon="$globals.icons.alertCircle" can-confirm @confirm="emitDelete()">
|
||||
<v-card-text>
|
||||
{{ $t("recipe.delete-confirmation") }}
|
||||
</v-card-text>
|
||||
@@ -21,17 +15,10 @@
|
||||
<v-spacer />
|
||||
<div v-if="!open" class="custom-btn-group ma-1">
|
||||
<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">
|
||||
<v-tooltip v-if="canEdit" location="bottom" color="info">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="flat"
|
||||
@@ -39,7 +26,7 @@
|
||||
size="small"
|
||||
color="info"
|
||||
class="ml-1"
|
||||
v-bind="tooltipProps"
|
||||
v-bind="props"
|
||||
@click="$emit('edit', true)"
|
||||
>
|
||||
<v-icon size="x-large">
|
||||
@@ -99,8 +86,8 @@
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RecipeContextMenu from "./RecipeContextMenu/RecipeContextMenu.vue";
|
||||
<script lang="ts">
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
@@ -110,30 +97,49 @@ const DELETE_EVENT = "delete";
|
||||
const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
|
||||
interface Props {
|
||||
recipe: Recipe;
|
||||
slug: string;
|
||||
recipeScale?: number;
|
||||
open: boolean;
|
||||
name: string;
|
||||
loggedIn?: boolean;
|
||||
recipeId: string;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
loggedIn: false,
|
||||
canEdit: false,
|
||||
});
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge },
|
||||
props: {
|
||||
recipe: {
|
||||
required: true,
|
||||
type: Object as () => Recipe,
|
||||
},
|
||||
slug: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
open: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
loggedIn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["print", "input", "delete", "close", "edit"],
|
||||
setup(_, context) {
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
|
||||
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const editorButtons = [
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const editorButtons = [
|
||||
{
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
@@ -158,27 +164,36 @@ const editorButtons = [
|
||||
event: SAVE_EVENT,
|
||||
color: "success",
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
function emitHandler(event: string) {
|
||||
function emitHandler(event: string) {
|
||||
switch (event) {
|
||||
case CLOSE_EVENT:
|
||||
emit("close");
|
||||
emit("input", false);
|
||||
context.emit(CLOSE_EVENT);
|
||||
context.emit("input", false);
|
||||
break;
|
||||
case DELETE_EVENT:
|
||||
deleteDialog.value = true;
|
||||
break;
|
||||
default:
|
||||
emit(event as any);
|
||||
context.emit(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitDelete() {
|
||||
emit("delete");
|
||||
emit("input", false);
|
||||
}
|
||||
function emitDelete() {
|
||||
context.emit(DELETE_EVENT);
|
||||
context.emit("input", false);
|
||||
}
|
||||
|
||||
return {
|
||||
deleteDialog,
|
||||
editorButtons,
|
||||
emitHandler,
|
||||
emitDelete,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div v-if="model.length > 0 || edit">
|
||||
<div v-if="value.length > 0 || edit">
|
||||
<v-card class="mt-4">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("asset.assets") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2" />
|
||||
<v-list
|
||||
v-if="model.length > 0"
|
||||
v-if="value.length > 0"
|
||||
:flat="!edit"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, i) in model"
|
||||
v-for="(item, i) in value"
|
||||
:key="i"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="ma-auto">
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps">
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ props }">
|
||||
<v-icon v-bind="props">
|
||||
{{ getIconDefinition(item.icon).icon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
@@ -28,12 +28,11 @@
|
||||
<v-list-item-title class="pl-2">
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
<template #append>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
v-if="!edit"
|
||||
color="primary"
|
||||
icon
|
||||
size="small"
|
||||
:href="assetURL(item.fileName ?? '')"
|
||||
target="_blank"
|
||||
top
|
||||
@@ -44,9 +43,8 @@
|
||||
<v-btn
|
||||
color="error"
|
||||
icon
|
||||
size="small"
|
||||
top
|
||||
@click="model.splice(i, 1)"
|
||||
@click="value.splice(i, 1)"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
@@ -55,7 +53,7 @@
|
||||
:copy-text="assetEmbed(item.fileName ?? '')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
@@ -92,12 +90,13 @@
|
||||
item-value="name"
|
||||
class="mr-2"
|
||||
>
|
||||
<template #item="{ item, props: itemProps }">
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<v-icon>{{ item.raw.icon }}</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<template #item="{ item }">
|
||||
<v-avatar>
|
||||
<v-icon class="mr-auto">
|
||||
{{ item.raw.icon }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
</v-select>
|
||||
<AppButtonUpload
|
||||
@@ -114,12 +113,13 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import type { RecipeAsset } from "~/lib/api/types/recipe";
|
||||
|
||||
const props = defineProps({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -128,29 +128,32 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array as () => RecipeAsset[],
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const api = useUserApi();
|
||||
|
||||
const model = defineModel<RecipeAsset[]>({ required: true });
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
const state = reactive({
|
||||
newAssetDialog: false,
|
||||
fileObject: {} as File,
|
||||
newAsset: {
|
||||
name: "",
|
||||
icon: "mdi-file",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const iconOptions = [
|
||||
const iconOptions = [
|
||||
{
|
||||
name: "mdi-file",
|
||||
title: i18n.t("asset.file"),
|
||||
@@ -176,32 +179,32 @@ const iconOptions = [
|
||||
title: i18n.t("asset.recipe"),
|
||||
icon: $globals.icons.primary,
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
const serverBase = useRequestURL().origin;
|
||||
const serverBase = useRequestURL().origin;
|
||||
|
||||
function getIconDefinition(icon: string) {
|
||||
function getIconDefinition(icon: string) {
|
||||
return iconOptions.find(item => item.name === icon) || iconOptions[0];
|
||||
}
|
||||
}
|
||||
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
function assetURL(assetName: string) {
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
function assetURL(assetName: string) {
|
||||
return recipeAssetPath(props.recipeId, assetName);
|
||||
}
|
||||
}
|
||||
|
||||
function assetEmbed(name: string) {
|
||||
function assetEmbed(name: string) {
|
||||
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%"> </img>`;
|
||||
}
|
||||
}
|
||||
|
||||
function setFileObject(fileObject: File) {
|
||||
function setFileObject(fileObject: File) {
|
||||
state.fileObject = fileObject;
|
||||
}
|
||||
}
|
||||
|
||||
function validFields() {
|
||||
function validFields() {
|
||||
return state.newAsset.name.length > 0 && state.fileObject.name.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function addAsset() {
|
||||
async function addAsset() {
|
||||
if (!validFields()) {
|
||||
alert.error(i18n.t("asset.error-submitting-form") as string);
|
||||
return;
|
||||
@@ -213,10 +216,21 @@ async function addAsset() {
|
||||
file: state.fileObject,
|
||||
extension: state.fileObject.name.split(".").pop() || "",
|
||||
});
|
||||
if (data) {
|
||||
model.value = [...model.value, data];
|
||||
}
|
||||
|
||||
context.emit("update:modelValue", [...props.modelValue, data]);
|
||||
state.newAsset = { name: "", icon: "mdi-file" };
|
||||
state.fileObject = {} as File;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
addAsset,
|
||||
assetURL,
|
||||
assetEmbed,
|
||||
getIconDefinition,
|
||||
iconOptions,
|
||||
setFileObject,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user