mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-25 01:03:13 -05:00
Compare commits
113 Commits
auto-local
...
renovate/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b116c39b5e | ||
|
|
3ae455539c | ||
|
|
8fd7995681 | ||
|
|
282eedfe2b | ||
|
|
03f849f20f | ||
|
|
5db3b6ab72 | ||
|
|
353c24ca4b | ||
|
|
216ae8571c | ||
|
|
02d32c8905 | ||
|
|
7e0d083e77 | ||
|
|
b3cea081fe | ||
|
|
d79252752b | ||
|
|
b3c214d102 | ||
|
|
3a01925e48 | ||
|
|
16e2386f5a | ||
|
|
bbfa105e99 | ||
|
|
c94c9940b2 | ||
|
|
29c6176d89 | ||
|
|
0c0d7d11a5 | ||
|
|
e75fc6d391 | ||
|
|
f308869154 | ||
|
|
af30b8bdfa | ||
|
|
de4f22c3f6 | ||
|
|
4c55b282d6 | ||
|
|
8d2b2eb581 | ||
|
|
e9daac5fc4 | ||
|
|
ee1205cfdc | ||
|
|
a165b707af | ||
|
|
564385eb83 | ||
|
|
c23aa61f17 | ||
|
|
cd39d0c4cb | ||
|
|
20e2d4e1a1 | ||
|
|
c09cc5a323 | ||
|
|
6d7b6bccab | ||
|
|
91fea086e5 | ||
|
|
e2fbe118a7 | ||
|
|
904e6b7d82 | ||
|
|
5aafb56c4f | ||
|
|
b4740d291d | ||
|
|
fc6dc34ace | ||
|
|
73d86f6f6b | ||
|
|
8e225ee796 | ||
|
|
ced233d361 | ||
|
|
b173172e6c | ||
|
|
a66db96eb5 | ||
|
|
dfd5abfb5d | ||
|
|
e2ae5cb5b6 | ||
|
|
634aa5cd25 | ||
|
|
23c7bd7e3d | ||
|
|
9c1ee972c9 | ||
|
|
1b9023c8c0 | ||
|
|
3a37cd6959 | ||
|
|
8da0d010a5 | ||
|
|
37f7f770a8 | ||
|
|
1cebbefd88 | ||
|
|
d55149b904 | ||
|
|
fad7acadfc | ||
|
|
a539c6cd2e | ||
|
|
7b5502d019 | ||
|
|
26d9d8fe24 | ||
|
|
b64f14aaae | ||
|
|
9b686ecd2b | ||
|
|
a956a638f4 | ||
|
|
c9d9e6822e | ||
|
|
4a563b76ad | ||
|
|
73f97c2cca | ||
|
|
75e3c99d72 | ||
|
|
217ddd8814 | ||
|
|
f2cc8dc922 | ||
|
|
b8329def91 | ||
|
|
2ae7dc3b82 | ||
|
|
510a63a71f | ||
|
|
14433819c3 | ||
|
|
96a9dbccb6 | ||
|
|
cfe20214e5 | ||
|
|
eef54879fe | ||
|
|
c789ecf0ba | ||
|
|
008f55e725 | ||
|
|
bcbe32f503 | ||
|
|
4101797c0e | ||
|
|
6110200a04 | ||
|
|
49f1e76776 | ||
|
|
24e9417d02 | ||
|
|
69d6985f3b | ||
|
|
84cdeb2398 | ||
|
|
6d439de144 | ||
|
|
1b586f8c67 | ||
|
|
f82f387146 | ||
|
|
d31c07a6c5 | ||
|
|
84372c2f4f | ||
|
|
168ac79daa | ||
|
|
22296277a8 | ||
|
|
6e006458be | ||
|
|
76a2fea076 | ||
|
|
3de4024619 | ||
|
|
194771653d | ||
|
|
24aa8f3525 | ||
|
|
fb8e318739 | ||
|
|
6255c71609 | ||
|
|
f2d1569488 | ||
|
|
987c7209fc | ||
|
|
f6dbd1f1f1 | ||
|
|
d30118899d | ||
|
|
af241dad57 | ||
|
|
b86de79c6f | ||
|
|
86e86f8c81 | ||
|
|
d795f91938 | ||
|
|
a59511cc81 | ||
|
|
a5d4cae6d0 | ||
|
|
2987cf8ba6 | ||
|
|
46b46978ff | ||
|
|
12857883a9 | ||
|
|
60fff3b5b8 |
47
.github/workflows/auto-merge-l10n.yml
vendored
47
.github/workflows/auto-merge-l10n.yml
vendored
@@ -14,19 +14,16 @@ jobs:
|
|||||||
if: contains(github.event.pull_request.labels.*.name, 'l10n')
|
if: contains(github.event.pull_request.labels.*.name, 'l10n')
|
||||||
|
|
||||||
steps:
|
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: Validate PR author
|
- name: Validate PR author
|
||||||
env:
|
env:
|
||||||
AUTHOR: ${{ github.event.pull_request.user.login }}
|
AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "$AUTHOR" != "hay-kot" && "$AUTHOR" != "github-actions[bot]" ]]; then
|
if [[
|
||||||
echo "::error::PR author must be hay-kot or github-actions[bot] for auto-merge (got: $AUTHOR)"
|
"$AUTHOR" != "hay-kot" &&
|
||||||
|
"$AUTHOR" != "github-actions[bot]" &&
|
||||||
|
"$AUTHOR" != "mealie-actions[bot]"
|
||||||
|
]]; then
|
||||||
|
echo "::error::PR author must be hay-kot, github-actions[bot], or mealie-actions[bot] for auto-merge (got: $AUTHOR)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Author validated: $AUTHOR"
|
echo "Author validated: $AUTHOR"
|
||||||
@@ -43,8 +40,8 @@ jobs:
|
|||||||
|
|
||||||
echo "PR changes: +$ADDITIONS -$DELETIONS (total: $TOTAL lines)"
|
echo "PR changes: +$ADDITIONS -$DELETIONS (total: $TOTAL lines)"
|
||||||
|
|
||||||
if [ "$TOTAL" -gt 200 ]; then
|
if [ "$TOTAL" -gt 400 ]; then
|
||||||
echo "::error::PR exceeds 200 line change limit ($TOTAL lines)"
|
echo "::error::PR exceeds 400 line change limit ($TOTAL lines)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -57,18 +54,27 @@ jobs:
|
|||||||
FILES=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path')
|
FILES=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path')
|
||||||
|
|
||||||
for file in $FILES; do
|
for file in $FILES; do
|
||||||
if [[ ! "$file" =~ ^frontend/lang/ ]] && [[ ! "$file" =~ ^mealie/repos/seed/resources/[^/]+/locales/ ]]; then
|
# Check if file matches any allowed path
|
||||||
echo "::error::Invalid file path: $file"
|
if [[ "$file" == "frontend/composables/use-locales/available-locales.ts" ]] || \
|
||||||
echo "Only files in frontend/lang/ or mealie/repos/seed/resources/*/locales/ are allowed"
|
[[ "$file" =~ ^frontend/lang/ ]] || \
|
||||||
exit 1
|
[[ "$file" =~ ^mealie/repos/seed/resources/[^/]+/locales/ ]]; then
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# File doesn't match allowed paths
|
||||||
|
echo "::error::Invalid file path: $file"
|
||||||
|
echo "Only the following paths are allowed:"
|
||||||
|
echo " - frontend/composables/use-locales/available-locales.ts"
|
||||||
|
echo " - frontend/lang/"
|
||||||
|
echo " - mealie/repos/seed/resources/*/locales/"
|
||||||
|
exit 1
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "All files are in allowed paths"
|
echo "All files are in allowed paths"
|
||||||
|
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
@@ -88,9 +94,16 @@ jobs:
|
|||||||
--approve \
|
--approve \
|
||||||
--body "Auto-approved: l10n PR from trusted author with valid file paths"
|
--body "Auto-approved: l10n PR from trusted author with valid file paths"
|
||||||
|
|
||||||
|
- 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: Enable auto-merge
|
- name: Enable auto-merge
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
11
.github/workflows/locale-sync.yml
vendored
11
.github/workflows/locale-sync.yml
vendored
@@ -15,10 +15,17 @@ jobs:
|
|||||||
sync-locales:
|
sync-locales:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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 repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -105,7 +112,7 @@ jobs:
|
|||||||
- Updated frontend locale files
|
- Updated frontend locale files
|
||||||
- Generated from latest translation sources" \
|
- Generated from latest translation sources" \
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: No changes detected
|
- name: No changes detected
|
||||||
if: steps.changes.outputs.has_changes == 'false'
|
if: steps.changes.outputs.has_changes == 'false'
|
||||||
|
|||||||
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@@ -37,6 +37,17 @@ jobs:
|
|||||||
|
|
||||||
- uses: depot/setup-action@v1
|
- uses: depot/setup-action@v1
|
||||||
|
|
||||||
|
- name: Generate Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
hkotel/mealie
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# Overwrite the image.version label with our tag
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.version=${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Retrieve Python package
|
- name: Retrieve Python package
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -57,5 +68,6 @@ jobs:
|
|||||||
hkotel/mealie:${{ inputs.tag }}
|
hkotel/mealie:${{ inputs.tag }}
|
||||||
ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
||||||
${{ inputs.tags }}
|
${{ inputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT=${{ github.sha }}
|
COMMIT=${{ github.sha }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ repos:
|
|||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.14.14
|
rev: v0.15.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
10
Taskfile.yml
10
Taskfile.yml
@@ -25,16 +25,9 @@ dotenv:
|
|||||||
- .env
|
- .env
|
||||||
- .dev.env
|
- .dev.env
|
||||||
tasks:
|
tasks:
|
||||||
docs:gen:
|
|
||||||
desc: runs the API documentation generator
|
|
||||||
cmds:
|
|
||||||
- uv run python dev/code-generation/gen_docs_api.py
|
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
desc: runs the documentation server
|
desc: runs the documentation server
|
||||||
dir: docs
|
dir: docs
|
||||||
deps:
|
|
||||||
- docs:gen
|
|
||||||
cmds:
|
cmds:
|
||||||
- uv run python -m mkdocs serve
|
- uv run python -m mkdocs serve
|
||||||
|
|
||||||
@@ -81,7 +74,6 @@ tasks:
|
|||||||
desc: run code generators
|
desc: run code generators
|
||||||
cmds:
|
cmds:
|
||||||
- uv run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
- uv run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
||||||
- task: docs:gen
|
|
||||||
- task: py:format
|
- task: py:format
|
||||||
|
|
||||||
dev:services:
|
dev:services:
|
||||||
@@ -350,4 +342,4 @@ tasks:
|
|||||||
vars: { WAIT_UNTIL_HEALTHY: true }
|
vars: { WAIT_UNTIL_HEALTHY: true }
|
||||||
- defer: { task: e2e:stop-server }
|
- defer: { task: e2e:stop-server }
|
||||||
- task: e2e:test
|
- task: e2e:test
|
||||||
vars: { PREVENT_REPORT_OPEN: true }
|
vars: { PREVENT_REPORT_OPEN: true }
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import json
|
|
||||||
from datetime import UTC, datetime
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
from mealie.app import app
|
|
||||||
from mealie.core.config import determine_data_dir
|
|
||||||
|
|
||||||
DATA_DIR = determine_data_dir()
|
|
||||||
|
|
||||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
|
||||||
|
|
||||||
HTML_TEMPLATE = """<!-- Custom HTML site displayed as the Home chapter -->
|
|
||||||
{% extends "main.html" %}
|
|
||||||
{% block tabs %}
|
|
||||||
{{ super() }}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="redoc-container"></div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
|
|
||||||
<script>
|
|
||||||
var spec = MY_SPECIFIC_TEXT;
|
|
||||||
Redoc.init(spec, {}, document.getElementById("redoc-container"));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
{% block footer %}{% endblock %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
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))
|
|
||||||
fd.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
generate_api_docs(app)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from dataclasses import dataclass
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import dotenv
|
import dotenv
|
||||||
@@ -10,6 +11,7 @@ from pydantic import ConfigDict
|
|||||||
from requests import Response
|
from requests import Response
|
||||||
from utils import CodeDest, CodeKeys, inject_inline, log
|
from utils import CodeDest, CodeKeys, inject_inline, log
|
||||||
|
|
||||||
|
from mealie.lang.locale_config import LOCALE_CONFIG, LocalePluralFoodHandling, LocaleTextDirection
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
|
||||||
BASE = pathlib.Path(__file__).parent.parent.parent
|
BASE = pathlib.Path(__file__).parent.parent.parent
|
||||||
@@ -17,57 +19,6 @@ BASE = pathlib.Path(__file__).parent.parent.parent
|
|||||||
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "")
|
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LocaleData:
|
|
||||||
name: str
|
|
||||||
dir: str = "ltr"
|
|
||||||
|
|
||||||
|
|
||||||
LOCALE_DATA: dict[str, LocaleData] = {
|
|
||||||
"af-ZA": LocaleData(name="Afrikaans (Afrikaans)"),
|
|
||||||
"ar-SA": LocaleData(name="العربية (Arabic)", dir="rtl"),
|
|
||||||
"bg-BG": LocaleData(name="Български (Bulgarian)"),
|
|
||||||
"ca-ES": LocaleData(name="Català (Catalan)"),
|
|
||||||
"cs-CZ": LocaleData(name="Čeština (Czech)"),
|
|
||||||
"da-DK": LocaleData(name="Dansk (Danish)"),
|
|
||||||
"de-DE": LocaleData(name="Deutsch (German)"),
|
|
||||||
"el-GR": LocaleData(name="Ελληνικά (Greek)"),
|
|
||||||
"en-GB": LocaleData(name="British English"),
|
|
||||||
"en-US": LocaleData(name="American English"),
|
|
||||||
"es-ES": LocaleData(name="Español (Spanish)"),
|
|
||||||
"et-EE": LocaleData(name="Eesti (Estonian)"),
|
|
||||||
"fi-FI": LocaleData(name="Suomi (Finnish)"),
|
|
||||||
"fr-BE": LocaleData(name="Belge (Belgian)"),
|
|
||||||
"fr-CA": LocaleData(name="Français canadien (Canadian French)"),
|
|
||||||
"fr-FR": LocaleData(name="Français (French)"),
|
|
||||||
"gl-ES": LocaleData(name="Galego (Galician)"),
|
|
||||||
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
|
|
||||||
"hr-HR": LocaleData(name="Hrvatski (Croatian)"),
|
|
||||||
"hu-HU": LocaleData(name="Magyar (Hungarian)"),
|
|
||||||
"is-IS": LocaleData(name="Íslenska (Icelandic)"),
|
|
||||||
"it-IT": LocaleData(name="Italiano (Italian)"),
|
|
||||||
"ja-JP": LocaleData(name="日本語 (Japanese)"),
|
|
||||||
"ko-KR": LocaleData(name="한국어 (Korean)"),
|
|
||||||
"lt-LT": LocaleData(name="Lietuvių (Lithuanian)"),
|
|
||||||
"lv-LV": LocaleData(name="Latviešu (Latvian)"),
|
|
||||||
"nl-NL": LocaleData(name="Nederlands (Dutch)"),
|
|
||||||
"no-NO": LocaleData(name="Norsk (Norwegian)"),
|
|
||||||
"pl-PL": LocaleData(name="Polski (Polish)"),
|
|
||||||
"pt-BR": LocaleData(name="Português do Brasil (Brazilian Portuguese)"),
|
|
||||||
"pt-PT": LocaleData(name="Português (Portuguese)"),
|
|
||||||
"ro-RO": LocaleData(name="Română (Romanian)"),
|
|
||||||
"ru-RU": LocaleData(name="Pусский (Russian)"),
|
|
||||||
"sk-SK": LocaleData(name="Slovenčina (Slovak)"),
|
|
||||||
"sl-SI": LocaleData(name="Slovenščina (Slovenian)"),
|
|
||||||
"sr-SP": LocaleData(name="српски (Serbian)"),
|
|
||||||
"sv-SE": LocaleData(name="Svenska (Swedish)"),
|
|
||||||
"tr-TR": LocaleData(name="Türkçe (Turkish)"),
|
|
||||||
"uk-UA": LocaleData(name="Українська (Ukrainian)"),
|
|
||||||
"vi-VN": LocaleData(name="Tiếng Việt (Vietnamese)"),
|
|
||||||
"zh-CN": LocaleData(name="简体中文 (Chinese simplified)"),
|
|
||||||
"zh-TW": LocaleData(name="繁體中文 (Chinese traditional)"),
|
|
||||||
}
|
|
||||||
|
|
||||||
LOCALE_TEMPLATE = """// This Code is auto generated by gen_ts_locales.py
|
LOCALE_TEMPLATE = """// This Code is auto generated by gen_ts_locales.py
|
||||||
export const LOCALES = [{% for locale in locales %}
|
export const LOCALES = [{% for locale in locales %}
|
||||||
{
|
{
|
||||||
@@ -75,6 +26,7 @@ export const LOCALES = [{% for locale in locales %}
|
|||||||
value: "{{ locale.locale }}",
|
value: "{{ locale.locale }}",
|
||||||
progress: {{ locale.progress }},
|
progress: {{ locale.progress }},
|
||||||
dir: "{{ locale.dir }}",
|
dir: "{{ locale.dir }}",
|
||||||
|
pluralFoodHandling: "{{ locale.plural_food_handling }}",
|
||||||
},{% endfor %}
|
},{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -87,10 +39,11 @@ class TargetLanguage(MealieModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
locale: str
|
locale: str
|
||||||
dir: str = "ltr"
|
dir: LocaleTextDirection = LocaleTextDirection.LTR
|
||||||
|
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
|
||||||
threeLettersCode: str
|
threeLettersCode: str
|
||||||
twoLettersCode: str
|
twoLettersCode: str
|
||||||
progress: float = 0.0
|
progress: int = 0
|
||||||
|
|
||||||
|
|
||||||
class CrowdinApi:
|
class CrowdinApi:
|
||||||
@@ -117,43 +70,15 @@ class CrowdinApi:
|
|||||||
def get_languages(self) -> list[TargetLanguage]:
|
def get_languages(self) -> list[TargetLanguage]:
|
||||||
response = self.get_project()
|
response = self.get_project()
|
||||||
tls = response.json()["data"]["targetLanguages"]
|
tls = response.json()["data"]["targetLanguages"]
|
||||||
|
return [TargetLanguage(**t) for t in tls]
|
||||||
|
|
||||||
models = [TargetLanguage(**t) for t in tls]
|
def get_progress(self) -> dict[str, int]:
|
||||||
|
|
||||||
models.insert(
|
|
||||||
0,
|
|
||||||
TargetLanguage(
|
|
||||||
id="en-US",
|
|
||||||
name="English",
|
|
||||||
locale="en-US",
|
|
||||||
dir="ltr",
|
|
||||||
threeLettersCode="en",
|
|
||||||
twoLettersCode="en",
|
|
||||||
progress=100,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
progress: list[dict] = self.get_progress()["data"]
|
|
||||||
|
|
||||||
for model in models:
|
|
||||||
if model.locale in LOCALE_DATA:
|
|
||||||
locale_data = LOCALE_DATA[model.locale]
|
|
||||||
model.name = locale_data.name
|
|
||||||
model.dir = locale_data.dir
|
|
||||||
|
|
||||||
for p in progress:
|
|
||||||
if p["data"]["languageId"] == model.id:
|
|
||||||
model.progress = p["data"]["translationProgress"]
|
|
||||||
|
|
||||||
models.sort(key=lambda x: x.locale, reverse=True)
|
|
||||||
return models
|
|
||||||
|
|
||||||
def get_progress(self) -> dict:
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"https://api.crowdin.com/api/v2/projects/{self.project_id}/languages/progress?limit=500",
|
f"https://api.crowdin.com/api/v2/projects/{self.project_id}/languages/progress?limit=500",
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
)
|
)
|
||||||
return response.json()
|
data = response.json()["data"]
|
||||||
|
return {p["data"]["languageId"]: p["translationProgress"] for p in data}
|
||||||
|
|
||||||
|
|
||||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||||
@@ -195,8 +120,8 @@ def inject_nuxt_values():
|
|||||||
|
|
||||||
all_langs = []
|
all_langs = []
|
||||||
for match in locales_dir.glob("*.json"):
|
for match in locales_dir.glob("*.json"):
|
||||||
match_data = LOCALE_DATA.get(match.stem)
|
match_data = LOCALE_CONFIG.get(match.stem)
|
||||||
match_dir = match_data.dir if match_data else "ltr"
|
match_dir = match_data.dir if match_data else LocaleTextDirection.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")}", dir: "{match_dir}" }},'
|
||||||
all_langs.append(lang_string)
|
all_langs.append(lang_string)
|
||||||
@@ -221,9 +146,82 @@ def inject_registration_validation_values():
|
|||||||
inject_inline(reg_valid, CodeKeys.nuxt_local_messages, all_langs)
|
inject_inline(reg_valid, CodeKeys.nuxt_local_messages, all_langs)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_models() -> list[TargetLanguage]:
|
||||||
|
return [
|
||||||
|
TargetLanguage(
|
||||||
|
id=locale,
|
||||||
|
name=data.name,
|
||||||
|
locale=locale,
|
||||||
|
threeLettersCode=locale.split("-")[-1],
|
||||||
|
twoLettersCode=locale.split("-")[-1],
|
||||||
|
)
|
||||||
|
for locale, data in LOCALE_CONFIG.items()
|
||||||
|
if locale != "en-US" # Crowdin doesn't include this, so we manually inject it later
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_progress() -> dict[str, int]:
|
||||||
|
with open(CodeDest.use_locales) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Extract the array content between [ and ]
|
||||||
|
match = re.search(r"export const LOCALES = (\[.*?\]);", content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("Could not find LOCALES array in file")
|
||||||
|
|
||||||
|
# Convert JS to JSON
|
||||||
|
array_content = match.group(1)
|
||||||
|
|
||||||
|
# Replace unquoted keys with quoted keys for valid JSON
|
||||||
|
# This converts: { name: "value" } to { "name": "value" }
|
||||||
|
json_str = re.sub(r"([,\{\s])([a-zA-Z_][a-zA-Z0-9_]*)\s*:", r'\1"\2":', array_content)
|
||||||
|
|
||||||
|
# Remove trailing commas before } and ]
|
||||||
|
json_str = re.sub(r",(\s*[}\]])", r"\1", json_str)
|
||||||
|
|
||||||
|
locales = json.loads(json_str)
|
||||||
|
return {locale["value"]: locale["progress"] for locale in locales}
|
||||||
|
|
||||||
|
|
||||||
|
def get_languages() -> list[TargetLanguage]:
|
||||||
|
if API_KEY:
|
||||||
|
api = CrowdinApi(None)
|
||||||
|
models = api.get_languages()
|
||||||
|
progress = api.get_progress()
|
||||||
|
else:
|
||||||
|
log.warning("CROWDIN_API_KEY is not set, using local lanugages instead")
|
||||||
|
log.warning("DOUBLE CHECK the output!!! Do not overwrite with bad local locale data!")
|
||||||
|
models = _get_local_models()
|
||||||
|
progress = _get_local_progress()
|
||||||
|
|
||||||
|
models.insert(
|
||||||
|
0,
|
||||||
|
TargetLanguage(
|
||||||
|
id="en-US",
|
||||||
|
name="English",
|
||||||
|
locale="en-US",
|
||||||
|
dir=LocaleTextDirection.LTR,
|
||||||
|
plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT,
|
||||||
|
threeLettersCode="en",
|
||||||
|
twoLettersCode="en",
|
||||||
|
progress=100,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
if model.locale in LOCALE_CONFIG:
|
||||||
|
locale_data = LOCALE_CONFIG[model.locale]
|
||||||
|
model.name = locale_data.name
|
||||||
|
model.dir = locale_data.dir
|
||||||
|
model.plural_food_handling = locale_data.plural_food_handling
|
||||||
|
model.progress = progress.get(model.id, model.progress)
|
||||||
|
|
||||||
|
models.sort(key=lambda x: x.locale, reverse=True)
|
||||||
|
return models
|
||||||
|
|
||||||
|
|
||||||
def generate_locales_ts_file():
|
def generate_locales_ts_file():
|
||||||
api = CrowdinApi(None)
|
models = get_languages()
|
||||||
models = api.get_languages()
|
|
||||||
tmpl = Template(LOCALE_TEMPLATE)
|
tmpl = Template(LOCALE_TEMPLATE)
|
||||||
rendered = tmpl.render(locales=models)
|
rendered = tmpl.render(locales=models)
|
||||||
|
|
||||||
@@ -233,10 +231,6 @@ def generate_locales_ts_file():
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if API_KEY is None or API_KEY == "":
|
|
||||||
log.error("CROWDIN_API_KEY is not set")
|
|
||||||
return
|
|
||||||
|
|
||||||
generate_locales_ts_file()
|
generate_locales_ts_file()
|
||||||
inject_nuxt_values()
|
inject_nuxt_values()
|
||||||
inject_registration_validation_values()
|
inject_registration_validation_values()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###############################################
|
###############################################
|
||||||
# Frontend Build
|
# Frontend Build
|
||||||
###############################################
|
###############################################
|
||||||
FROM node:24@sha256:b2b2184ba9b78c022e1d6a7924ec6fba577adf28f15c9d9c457730cc4ad3807a \
|
FROM node:24@sha256:00e9195ebd49985a6da8921f419978d85dfe354589755192dc090425ce4da2f7 \
|
||||||
AS frontend-builder
|
AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
@@ -111,7 +111,6 @@ RUN . $VENV_PATH/bin/activate \
|
|||||||
# Production Image
|
# 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 PRODUCTION=true
|
||||||
ENV TESTING=false
|
ENV TESTING=false
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: API
|
|
||||||
template: api.html
|
|
||||||
---
|
|
||||||
@@ -6,7 +6,7 @@ While this guide aims to simplify the migration process for developers, it's not
|
|||||||
|
|
||||||
## V1 → V2
|
## V1 → V2
|
||||||
|
|
||||||
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
|
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](../../documentation/getting-started/features.md#groups-and-households) section in the Features guide.
|
||||||
|
|
||||||
### `updateAt` is now `updatedAt`
|
### `updateAt` is now `updatedAt`
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Recipes extras are a key feature of the Mealie API. They allow you to create cus
|
|||||||
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
|
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
|
||||||
|
|
||||||
#### Shopping List and Food Extras
|
#### Shopping List and Food Extras
|
||||||
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Alexa, ToDoist, Trello, or any other list manager with an API.
|
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Todoist, Trello, or any other list manager with an API.
|
||||||
|
|
||||||
To keep shopping lists in sync, for instance, you can store your Trello list id on your Mealie shopping list: <br />
|
To keep shopping lists in sync, for instance, you can store your Trello list id on your Mealie shopping list: <br />
|
||||||
`{"trello_list_id": "5abbe4b7ddc1b351ef961414"}`
|
`{"trello_list_id": "5abbe4b7ddc1b351ef961414"}`
|
||||||
@@ -52,6 +52,7 @@ Many applications will keep track of the query and adjust the page parameter app
|
|||||||
Notice that the route does not contain the baseurl (e.g. `https://mymealieapplication.com/api`).
|
Notice that the route does not contain the baseurl (e.g. `https://mymealieapplication.com/api`).
|
||||||
|
|
||||||
There are a few shorthands available to reduce the number of calls for certain common requests:
|
There are a few shorthands available to reduce the number of calls for certain common requests:
|
||||||
|
|
||||||
- if you want to return _all_ results, effectively disabling pagination, set `perPage = -1` (and fetch the first page)
|
- if you want to return _all_ results, effectively disabling pagination, set `perPage = -1` (and fetch the first page)
|
||||||
- if you want to fetch the _last_ page, set `page = -1`
|
- if you want to fetch the _last_ page, set `page = -1`
|
||||||
|
|
||||||
@@ -78,8 +79,8 @@ This filter will find all foods that are not named "carrot": <br>
|
|||||||
##### Keyword Filters
|
##### Keyword Filters
|
||||||
The API supports many SQL keywords, such as `IS NULL` and `IN`, as well as their negations (e.g. `IS NOT NULL` and `NOT IN`).
|
The API supports many SQL keywords, such as `IS NULL` and `IN`, as well as their negations (e.g. `IS NOT NULL` and `NOT IN`).
|
||||||
|
|
||||||
Here is an example of a filter that returns all recipes where the "last made" value is not null: <br>
|
Here is an example of a filter that returns all shopping list items without a food: <br>
|
||||||
`lastMade IS NOT NULL`
|
`foodId IS NULL`
|
||||||
|
|
||||||
This filter will find all recipes that don't start with the word "Test": <br>
|
This filter will find all recipes that don't start with the word "Test": <br>
|
||||||
`name NOT LIKE "Test%"`
|
`name NOT LIKE "Test%"`
|
||||||
@@ -89,6 +90,28 @@ This filter will find all recipes that don't start with the word "Test": <br>
|
|||||||
This filter will find all recipes that have particular slugs: <br>
|
This filter will find all recipes that have particular slugs: <br>
|
||||||
`slug IN ["pasta-fagioli", "delicious-ramen"]`
|
`slug IN ["pasta-fagioli", "delicious-ramen"]`
|
||||||
|
|
||||||
|
##### Placeholder Keywords
|
||||||
|
You can use placeholders to insert dynamic values as opposed to static values. Currently the only supported placeholder keyword is `$NOW`, to insert the current date/time.
|
||||||
|
|
||||||
|
`$NOW` can optionally be paired with basic offsets. Here is an example of a filter which gives you recipes not made within the past 30 days: <br>
|
||||||
|
`lastMade <= "$NOW-30d"`
|
||||||
|
|
||||||
|
Supported offset operations include:
|
||||||
|
|
||||||
|
- `-` for subtracting a time (i.e. in the past)
|
||||||
|
- `+` for adding a time (i.e. in the future)
|
||||||
|
|
||||||
|
Supported offset intervals include:
|
||||||
|
|
||||||
|
- `y` for years
|
||||||
|
- `m` for months
|
||||||
|
- `d` for days
|
||||||
|
- `H` for hours
|
||||||
|
- `M` for minutes
|
||||||
|
- `S` for seconds
|
||||||
|
|
||||||
|
Note that intervals are _case sensitive_ (e.g. `s` is an invalid interval).
|
||||||
|
|
||||||
##### Nested Property filters
|
##### Nested Property filters
|
||||||
When querying tables with relationships, you can filter properties on related tables. For instance, if you want to query all recipes owned by a particular user: <br>
|
When querying tables with relationships, you can filter properties on related tables. For instance, if you want to query all recipes owned by a particular user: <br>
|
||||||
`user.username = "SousChef20220320"`
|
`user.username = "SousChef20220320"`
|
||||||
@@ -96,7 +119,7 @@ When querying tables with relationships, you can filter properties on related ta
|
|||||||
This timeline event filter will return all timeline events for recipes that were created after a particular date: <br>
|
This timeline event filter will return all timeline events for recipes that were created after a particular date: <br>
|
||||||
`recipe.createdAt >= "2023-02-25"`
|
`recipe.createdAt >= "2023-02-25"`
|
||||||
|
|
||||||
This recipe filter will return all recipes that contains a particular set of tags: <br>
|
This recipe filter will return all recipes that contain a particular set of tags: <br>
|
||||||
`tags.name CONTAINS ALL ["Easy", "Cajun"]`
|
`tags.name CONTAINS ALL ["Easy", "Cajun"]`
|
||||||
|
|
||||||
##### Compound Filters
|
##### Compound Filters
|
||||||
|
|||||||
@@ -124,16 +124,16 @@ For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values
|
|||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| OPENAI_BASE_URL<super>[†][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
| OPENAI_BASE_URL<super>[†][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||||
| OPENAI_API_KEY<super>[†][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
|
| OPENAI_API_KEY<super>[†][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||||
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
| OPENAI_CUSTOM_HEADERS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||||
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
| OPENAI_CUSTOM_PARAMS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||||
| 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_ENABLE_IMAGE_SERVICES <br/> :octicons-tag-24: v1.12.0 | 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_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_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 | 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_CUSTOM_PROMPT_DIR | None | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
|
| OPENAI_CUSTOM_PROMPT_DIR <br/> :octicons-tag-24: v3.10.0 | None | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
|
||||||
|
|
||||||
### Theming
|
### Theming
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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!
|
1. Take a backup just in case!
|
||||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.9.2`
|
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.11.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.
|
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
|
4. Restart the container
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v3.9.2 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.11.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v3.9.2 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.11.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -93,7 +93,7 @@ nav:
|
|||||||
- iOS Shortcut: "documentation/community-guide/ios-shortcut.md"
|
- iOS Shortcut: "documentation/community-guide/ios-shortcut.md"
|
||||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||||
|
|
||||||
- API Reference: "api/redoc.md"
|
- API Reference: "https://demo.mealie.io/docs"
|
||||||
|
|
||||||
- Contributors Guide:
|
- Contributors Guide:
|
||||||
- Non-Code: "contributors/non-coders.md"
|
- Non-Code: "contributors/non-coders.md"
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
max-width: 950px !important;
|
max-width: 950px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg-container {
|
||||||
|
max-width: 1100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.theme--dark.v-application {
|
.theme--dark.v-application {
|
||||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@
|
|||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
const groupSlug = computed(() => auth.user.value?.groupSlug);
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
const sections = ref([
|
const sections = ref([
|
||||||
|
|||||||
@@ -73,11 +73,11 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
|
|||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
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 { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const slug = route.params.slug as string;
|
const slug = route.params.slug as string;
|
||||||
@@ -88,11 +88,11 @@ const router = useRouter();
|
|||||||
const book = getOne(slug);
|
const book = getOne(slug);
|
||||||
|
|
||||||
const isOwnHousehold = computed(() => {
|
const isOwnHousehold = computed(() => {
|
||||||
if (!($auth.user.value && book.value?.householdId)) {
|
if (!(auth.user.value && book.value?.householdId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $auth.user.value.householdId === book.value.householdId;
|
return auth.user.value.householdId === book.value.householdId;
|
||||||
});
|
});
|
||||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||||
|
|
||||||
|
|||||||
217
frontend/components/Domain/Group/GroupDataPage.vue
Normal file
217
frontend/components/Domain/Group/GroupDataPage.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Create Dialog -->
|
||||||
|
<BaseDialog
|
||||||
|
v-model="createDialog"
|
||||||
|
:title="$t('general.create')"
|
||||||
|
:icon="icon"
|
||||||
|
color="primary"
|
||||||
|
:submit-disabled="!createFormValid"
|
||||||
|
can-confirm
|
||||||
|
@confirm="emit('create-one', createForm.data)"
|
||||||
|
>
|
||||||
|
<div class="mx-2 mt-2">
|
||||||
|
<slot name="create-dialog-top" />
|
||||||
|
<AutoForm
|
||||||
|
v-model="createForm.data"
|
||||||
|
v-model:is-valid="createFormValid"
|
||||||
|
:items="createForm.items"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
|
<!-- Edit Dialog -->
|
||||||
|
<BaseDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
:title="$t('general.edit')"
|
||||||
|
:icon="icon"
|
||||||
|
color="primary"
|
||||||
|
:submit-disabled="!editFormValid"
|
||||||
|
can-confirm
|
||||||
|
@confirm="emit('edit-one', editForm.data)"
|
||||||
|
>
|
||||||
|
<div class="mx-2 mt-2">
|
||||||
|
<AutoForm
|
||||||
|
v-model="editForm.data"
|
||||||
|
v-model:is-valid="editFormValid"
|
||||||
|
:items="editForm.items"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #custom-card-action>
|
||||||
|
<slot name="edit-dialog-custom-action" />
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
|
<!-- Delete Dialog -->
|
||||||
|
<BaseDialog
|
||||||
|
v-model="deleteDialog"
|
||||||
|
:title="$t('general.confirm')"
|
||||||
|
:icon="$globals.icons.alertCircle"
|
||||||
|
color="error"
|
||||||
|
can-confirm
|
||||||
|
@confirm="$emit('deleteOne', deleteTarget.id)"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
{{ $t("general.confirm-delete-generic") }}
|
||||||
|
<p v-if="deleteTarget" class="mt-4 ml-4">
|
||||||
|
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
|
||||||
|
</p>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
|
<!-- Bulk Delete Dialog -->
|
||||||
|
<BaseDialog
|
||||||
|
v-model="bulkDeleteDialog"
|
||||||
|
width="650px"
|
||||||
|
:title="$t('general.confirm')"
|
||||||
|
:icon="$globals.icons.alertCircle"
|
||||||
|
color="error"
|
||||||
|
can-confirm
|
||||||
|
@confirm="$emit('bulk-action', 'delete-selected', bulkDeleteTarget)"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="h4">
|
||||||
|
{{ $t('general.confirm-delete-generic-items') }}
|
||||||
|
</p>
|
||||||
|
<v-card variant="outlined">
|
||||||
|
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
|
||||||
|
<template #default="{ item }">
|
||||||
|
<v-list-item class="pb-2">
|
||||||
|
<v-list-item-title>{{ item.name || item.title || item.id }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-virtual-scroll>
|
||||||
|
</v-card>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
|
<BaseCardSectionTitle
|
||||||
|
:icon="icon"
|
||||||
|
section
|
||||||
|
:title="title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CrudTable
|
||||||
|
:headers="tableHeaders"
|
||||||
|
:table-config="tableConfig"
|
||||||
|
:data="data || []"
|
||||||
|
:bulk-actions="bulkActions"
|
||||||
|
:initial-sort="initialSort"
|
||||||
|
@edit-one="editEventHandler"
|
||||||
|
@delete-one="deleteEventHandler"
|
||||||
|
@bulk-action="handleBulkAction"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="slotName in itemSlotNames"
|
||||||
|
#[slotName]="slotProps"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
:name="slotName"
|
||||||
|
v-bind="slotProps"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #button-row>
|
||||||
|
<BaseButton
|
||||||
|
create
|
||||||
|
@click="createDialog = true"
|
||||||
|
>
|
||||||
|
{{ $t("general.create") }}
|
||||||
|
</BaseButton>
|
||||||
|
<slot name="table-button-row" />
|
||||||
|
</template>
|
||||||
|
<template #button-bottom>
|
||||||
|
<slot name="table-button-bottom" />
|
||||||
|
</template>
|
||||||
|
</CrudTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TableHeaders, TableConfig, BulkAction } from "~/components/global/CrudTable.vue";
|
||||||
|
import type { AutoFormItems } from "~/types/auto-forms";
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "deleteOne", id: string): void;
|
||||||
|
(e: "deleteMany", ids: string[]): void;
|
||||||
|
(e: "create-one" | "edit-one", data: any): void;
|
||||||
|
(e: "bulk-action", event: string, items: any[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tableHeaders = defineModel<TableHeaders[]>("tableHeaders", { required: true });
|
||||||
|
const createForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("createForm", { required: true });
|
||||||
|
const createDialog = defineModel("createDialog", { type: Boolean, default: false });
|
||||||
|
|
||||||
|
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
|
||||||
|
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
tableConfig: {
|
||||||
|
type: Object as PropType<TableConfig>,
|
||||||
|
default: () => ({
|
||||||
|
hideColumns: false,
|
||||||
|
canExport: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array as PropType<Array<any>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
bulkActions: {
|
||||||
|
type: Array as PropType<BulkAction[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
initialSort: {
|
||||||
|
type: String,
|
||||||
|
default: "name",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Bulk Action Handler
|
||||||
|
function handleBulkAction(event: string, items: any[]) {
|
||||||
|
if (event === "delete-selected") {
|
||||||
|
bulkDeleteEventHandler(items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("bulk-action", event, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Create & Edit
|
||||||
|
const createFormValid = ref(false);
|
||||||
|
const editFormValid = ref(false);
|
||||||
|
const itemSlotNames = computed(() => Object.keys(slots).filter(slotName => slotName.startsWith("item.")));
|
||||||
|
const editEventHandler = (item: any) => {
|
||||||
|
editForm.value.data = { ...item };
|
||||||
|
editDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Delete Logic
|
||||||
|
const deleteTarget = ref<any>(null);
|
||||||
|
const deleteDialog = ref(false);
|
||||||
|
|
||||||
|
function deleteEventHandler(item: any) {
|
||||||
|
deleteTarget.value = item;
|
||||||
|
deleteDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Bulk Delete Logic
|
||||||
|
const bulkDeleteTarget = ref<Array<any>>([]);
|
||||||
|
const bulkDeleteDialog = ref(false);
|
||||||
|
|
||||||
|
function bulkDeleteEventHandler(items: Array<any>) {
|
||||||
|
bulkDeleteTarget.value = items;
|
||||||
|
bulkDeleteDialog.value = true;
|
||||||
|
console.log("Bulk Delete Event Handler", items);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -91,7 +91,7 @@ const state = reactive({
|
|||||||
shoppingListDialog: false,
|
shoppingListDialog: false,
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
title: i18n.t("recipe.add-to-list"),
|
title: i18n.t("meal-plan.add-day-to-list"),
|
||||||
icon: $globals.icons.cartCheck,
|
icon: $globals.icons.cartCheck,
|
||||||
color: undefined,
|
color: undefined,
|
||||||
event: "shoppingList",
|
event: "shoppingList",
|
||||||
@@ -123,8 +123,8 @@ async function getShoppingLists() {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||||
shoppingList: () => {
|
shoppingList: async () => {
|
||||||
getShoppingLists();
|
await getShoppingLists();
|
||||||
state.shoppingListDialog = true;
|
state.shoppingListDialog = true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||||
import { Organizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
import type { QueryFilterJSON } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
queryFilter?: QueryFilterJSON | null;
|
queryFilter?: QueryFilterJSON | null;
|
||||||
@@ -76,7 +76,6 @@ const MEAL_DAY_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function handleQueryFilterInput(value: string | undefined) {
|
function handleQueryFilterInput(value: string | undefined) {
|
||||||
console.warn("handleQueryFilterInput called with value:", value);
|
|
||||||
queryFilterString.value = value || "";
|
queryFilterString.value = value || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ const fieldDefs: FieldDefinition[] = [
|
|||||||
{
|
{
|
||||||
name: "last_made",
|
name: "last_made",
|
||||||
label: i18n.t("general.last-made"),
|
label: i18n.t("general.last-made"),
|
||||||
type: "date",
|
type: "relativeDate",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "created_at",
|
name: "created_at",
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
<v-select
|
<v-select
|
||||||
v-if="field.type !== 'boolean'"
|
v-if="field.type !== 'boolean'"
|
||||||
:model-value="field.relationalOperatorValue"
|
:model-value="field.relationalOperatorValue"
|
||||||
:items="field.relationalOperatorOptions"
|
:items="field.relationalOperatorChoices"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@@ -129,9 +129,9 @@
|
|||||||
:class="config.col.class"
|
:class="config.col.class"
|
||||||
>
|
>
|
||||||
<v-select
|
<v-select
|
||||||
v-if="field.fieldOptions"
|
v-if="field.fieldChoices"
|
||||||
:model-value="field.values"
|
:model-value="field.values"
|
||||||
:items="field.fieldOptions"
|
:items="field.fieldChoices"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
multiple
|
multiple
|
||||||
@@ -169,23 +169,39 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ props: activatorProps }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
|
:model-value="$d(safeNewDate(field.value + 'T00:00:00'))"
|
||||||
persistent-hint
|
|
||||||
:prepend-icon="$globals.icons.calendar"
|
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
class="date-input"
|
||||||
v-bind="activatorProps"
|
v-bind="activatorProps"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<v-date-picker
|
<v-date-picker
|
||||||
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
|
:model-value="safeNewDate(field.value + 'T00:00:00')"
|
||||||
hide-header
|
hide-header
|
||||||
:first-day-of-week="firstDayOfWeek"
|
:first-day-of-week="firstDayOfWeek"
|
||||||
:local="$i18n.locale"
|
:local="$i18n.locale"
|
||||||
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
|
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
|
||||||
/>
|
/>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
<!--
|
||||||
|
Relative dates are assumed to be negative intervals with a unit of days.
|
||||||
|
The input is a *positive*, interpreted internally as a *negative* offset.
|
||||||
|
-->
|
||||||
|
<v-number-input
|
||||||
|
v-else-if="field.type === 'relativeDate'"
|
||||||
|
:model-value="parseRelativeDateOffset(field.value)"
|
||||||
|
:suffix="$t('query-filter.dates.days-ago', parseRelativeDateOffset(field.value))"
|
||||||
|
variant="underlined"
|
||||||
|
control-variant="stacked"
|
||||||
|
density="compact"
|
||||||
|
inset
|
||||||
|
:min="0"
|
||||||
|
:precision="0"
|
||||||
|
class="date-input"
|
||||||
|
@update:model-value="setFieldValue(field, index, $event)"
|
||||||
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Category"
|
v-else-if="field.type === Organizer.Category"
|
||||||
v-model="field.organizers"
|
v-model="field.organizers"
|
||||||
@@ -319,7 +335,13 @@ import { useDebounceFn } from "@vueuse/core";
|
|||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import { Organizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
|
import type {
|
||||||
|
LogicalOperator,
|
||||||
|
QueryFilterJSON,
|
||||||
|
QueryFilterJSONPart,
|
||||||
|
RelationalKeyword,
|
||||||
|
RelationalOperator,
|
||||||
|
} from "~/lib/api/types/non-generated";
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
import { useUserStore } from "~/composables/store/use-user-store";
|
import { useUserStore } from "~/composables/store/use-user-store";
|
||||||
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||||
@@ -341,7 +363,14 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
const {
|
||||||
|
logOps,
|
||||||
|
placeholderKeywords,
|
||||||
|
getRelOps,
|
||||||
|
buildQueryFilterString,
|
||||||
|
getFieldFromFieldDef,
|
||||||
|
isOrganizerType,
|
||||||
|
} = useQueryFilterBuilder();
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
@@ -396,16 +425,29 @@ function setField(index: number, fieldLabel: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldOptions !== fields.value[index].fieldOptions);
|
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldChoices !== fields.value[index].fieldChoices);
|
||||||
const updatedField = { ...fields.value[index], ...fieldDef };
|
const updatedField = { ...fields.value[index], ...fieldDef };
|
||||||
|
|
||||||
// we have to set this explicitly since it might be undefined
|
// we have to set this explicitly since it might be undefined
|
||||||
updatedField.fieldOptions = fieldDef.fieldOptions;
|
updatedField.fieldChoices = fieldDef.fieldChoices;
|
||||||
|
|
||||||
fields.value[index] = {
|
fields.value[index] = {
|
||||||
...getFieldFromFieldDef(updatedField, resetValue),
|
...getFieldFromFieldDef(updatedField, resetValue),
|
||||||
id: fields.value[index].id, // keep the id
|
id: fields.value[index].id, // keep the id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
switch (fields.value[index].type) {
|
||||||
|
case "date":
|
||||||
|
fields.value[index].value = safeNewDate("");
|
||||||
|
break;
|
||||||
|
case "relativeDate":
|
||||||
|
fields.value[index].value = "$NOW-30d";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||||
@@ -425,12 +467,21 @@ function setLogicalOperatorValue(field: FieldWithId, index: number, value: Logic
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||||
|
const relOps = getRelOps(field.type);
|
||||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
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;
|
state.datePickers[index] = false;
|
||||||
fields.value[index].value = value;
|
|
||||||
|
if (field.type === "relativeDate") {
|
||||||
|
// Value is set to an int representing the offset from $NOW
|
||||||
|
// Values are assumed to be negative offsets ('-') with a unit of days ('d')
|
||||||
|
fields.value[index].value = `$NOW-${Math.abs(value)}d`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fields.value[index].value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||||
@@ -448,12 +499,7 @@ function removeField(index: number) {
|
|||||||
state.datePickers.splice(index, 1);
|
state.datePickers.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
const fieldsUpdater = useDebounceFn(() => {
|
||||||
/* newFields.forEach((field, index) => {
|
|
||||||
const updatedField = getFieldFromFieldDef(field);
|
|
||||||
fields.value[index] = updatedField; // recursive!!!
|
|
||||||
}); */
|
|
||||||
|
|
||||||
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
|
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
|
||||||
if (qf) {
|
if (qf) {
|
||||||
console.debug(`Set query filter: ${qf}`);
|
console.debug(`Set query filter: ${qf}`);
|
||||||
@@ -519,6 +565,9 @@ async function initializeFields() {
|
|||||||
...getFieldFromFieldDef(fieldDef),
|
...getFieldFromFieldDef(fieldDef),
|
||||||
id: useUid(),
|
id: useUid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const relOps = getRelOps(field.type);
|
||||||
|
|
||||||
field.leftParenthesis = part.leftParenthesis || field.leftParenthesis;
|
field.leftParenthesis = part.leftParenthesis || field.leftParenthesis;
|
||||||
field.rightParenthesis = part.rightParenthesis || field.rightParenthesis;
|
field.rightParenthesis = part.rightParenthesis || field.rightParenthesis;
|
||||||
field.logicalOperator = part.logicalOperator
|
field.logicalOperator = part.logicalOperator
|
||||||
@@ -527,12 +576,15 @@ async function initializeFields() {
|
|||||||
field.relationalOperatorValue = part.relationalOperator
|
field.relationalOperatorValue = part.relationalOperator
|
||||||
? relOps.value[part.relationalOperator]
|
? relOps.value[part.relationalOperator]
|
||||||
: field.relationalOperatorValue;
|
: field.relationalOperatorValue;
|
||||||
|
field.relationalOperatorValue = part.relationalOperator
|
||||||
|
? relOps.value[part.relationalOperator]
|
||||||
|
: field.relationalOperatorValue;
|
||||||
|
|
||||||
if (field.leftParenthesis || field.rightParenthesis) {
|
if (field.leftParenthesis || field.rightParenthesis) {
|
||||||
state.showAdvanced = true;
|
state.showAdvanced = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
|
||||||
if (typeof part.value === "string") {
|
if (typeof part.value === "string") {
|
||||||
field.values = part.value ? [part.value] : [];
|
field.values = part.value ? [part.value] : [];
|
||||||
}
|
}
|
||||||
@@ -601,7 +653,7 @@ function buildQueryFilterJSON(): QueryFilterJSON {
|
|||||||
relationalOperator: field.relationalOperatorValue?.value,
|
relationalOperator: field.relationalOperatorValue?.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
|
||||||
part.value = field.values.map(value => value.toString());
|
part.value = field.values.map(value => value.toString());
|
||||||
}
|
}
|
||||||
else if (field.type === "boolean") {
|
else if (field.type === "boolean") {
|
||||||
@@ -619,6 +671,50 @@ function buildQueryFilterJSON(): QueryFilterJSON {
|
|||||||
return qfJSON;
|
return qfJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeNewDate(input: string): Date {
|
||||||
|
const date = new Date(input);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return today;
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a relative date string offset (e.g. $NOW-30d --> 30)
|
||||||
|
*
|
||||||
|
* Currently only values with a negative offset ('-') and a unit of days ('d') are supported
|
||||||
|
*/
|
||||||
|
function parseRelativeDateOffset(value: string): number {
|
||||||
|
const defaultVal = 30;
|
||||||
|
if (!value) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!value.startsWith(placeholderKeywords.value["$NOW"].value)) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainder = value.slice(placeholderKeywords.value["$NOW"].value.length);
|
||||||
|
if (!remainder.startsWith("-")) {
|
||||||
|
throw new Error("Invalid operator (not '-')");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainder.slice(-1) !== "d") {
|
||||||
|
throw new Error("Invalid unit (not 'd')");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice off sign and unit
|
||||||
|
return parseInt(remainder.slice(1, -1));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.warn(`Unable to parse relative date offset from '${value}': ${error}`);
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const config = computed(() => {
|
const config = computed(() => {
|
||||||
const multiple = fields.value.length > 1;
|
const multiple = fields.value.length > 1;
|
||||||
const adv = state.showAdvanced;
|
const adv = state.showAdvanced;
|
||||||
@@ -689,4 +785,13 @@ const config = computed(() => {
|
|||||||
.bg-light {
|
.bg-light {
|
||||||
background-color: rgba(255, 255, 255, var(--bg-opactity));
|
background-color: rgba(255, 255, 255, var(--bg-opactity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.date-input input) {
|
||||||
|
text-align: end;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.date-input .v-field__field) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -130,11 +130,11 @@ defineEmits<{
|
|||||||
delete: [slug: string];
|
delete: [slug: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||||
const recipeRoute = computed<string>(() => {
|
const recipeRoute = computed<string>(() => {
|
||||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||||
|
|||||||
@@ -160,11 +160,11 @@ defineEmits<{
|
|||||||
delete: [slug: string];
|
delete: [slug: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||||
const recipeRoute = computed<string>(() => {
|
const recipeRoute = computed<string>(() => {
|
||||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ const EVENTS = {
|
|||||||
shuffle: "shuffle",
|
shuffle: "shuffle",
|
||||||
};
|
};
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const useMobileCards = computed(() => {
|
const useMobileCards = computed(() => {
|
||||||
@@ -234,7 +234,7 @@ const sortLoading = ref(false);
|
|||||||
const randomSeed = ref(Date.now().toString());
|
const randomSeed = ref(Date.now().toString());
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const perPage = 32;
|
const perPage = 32;
|
||||||
|
|||||||
@@ -202,13 +202,13 @@ const newMealdateString = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
@@ -296,12 +296,12 @@ const recipeRefWithScale = computed(() =>
|
|||||||
);
|
);
|
||||||
const isAdminAndNotOwner = computed(() => {
|
const isAdminAndNotOwner = computed(() => {
|
||||||
return (
|
return (
|
||||||
$auth.user.value?.admin
|
auth.user.value?.admin
|
||||||
&& $auth.user.value?.id !== recipeRef.value?.userId
|
&& auth.user.value?.id !== recipeRef.value?.userId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const canDelete = computed(() => {
|
const canDelete = computed(() => {
|
||||||
const user = $auth.user.value;
|
const user = auth.user.value;
|
||||||
const recipe = recipeRef.value;
|
const recipe = recipeRef.value;
|
||||||
return user && recipe && (user.admin || user.id === recipe.userId);
|
return user && recipe && (user.admin || user.id === recipe.userId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ defineEmits<{
|
|||||||
const selected = defineModel<Recipe[]>({ default: () => [] });
|
const selected = defineModel<Recipe[]>({ default: () => [] });
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const groupSlug = $auth.user.value?.groupSlug;
|
const groupSlug = auth.user.value?.groupSlug;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Initialize sort state with default sorting by dateAdded descending
|
// Initialize sort state with default sorting by dateAdded descending
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const dialog = defineModel<boolean>({ default: false });
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const preferences = useShoppingListPreferences();
|
const preferences = useShoppingListPreferences();
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
@@ -227,7 +227,7 @@ const currentHouseholdSlug = ref("");
|
|||||||
const filteredShoppingLists = ref<ShoppingListSummary[]>([]);
|
const filteredShoppingLists = ref<ShoppingListSummary[]>([]);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
shoppingListDialog: true,
|
shoppingListDialog: false,
|
||||||
shoppingListIngredientDialog: false,
|
shoppingListIngredientDialog: false,
|
||||||
shoppingListShowAllToggled: false,
|
shoppingListShowAllToggled: false,
|
||||||
});
|
});
|
||||||
@@ -239,9 +239,9 @@ const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
|||||||
|
|
||||||
watch([dialog, () => preferences.value.viewAllLists], () => {
|
watch([dialog, () => preferences.value.viewAllLists], () => {
|
||||||
if (dialog.value) {
|
if (dialog.value) {
|
||||||
currentHouseholdSlug.value = $auth.user.value?.householdSlug || "";
|
currentHouseholdSlug.value = auth.user.value?.householdSlug || "";
|
||||||
filteredShoppingLists.value = props.shoppingLists.filter(
|
filteredShoppingLists.value = props.shoppingLists.filter(
|
||||||
list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id,
|
list => preferences.value.viewAllLists || list.userId === auth.user.value?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filteredShoppingLists.value.length === 1 && !state.shoppingListShowAllToggled) {
|
if (filteredShoppingLists.value.length === 1 && !state.shoppingListShowAllToggled) {
|
||||||
@@ -249,6 +249,7 @@ watch([dialog, () => preferences.value.viewAllLists], () => {
|
|||||||
openShoppingListIngredientDialog(selectedShoppingList.value);
|
openShoppingListIngredientDialog(selectedShoppingList.value);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
state.shoppingListDialog = true;
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,7 +372,7 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initState() {
|
function initState() {
|
||||||
state.shoppingListDialog = true;
|
state.shoppingListDialog = false;
|
||||||
state.shoppingListIngredientDialog = false;
|
state.shoppingListIngredientDialog = false;
|
||||||
state.shoppingListShowAllToggled = false;
|
state.shoppingListShowAllToggled = false;
|
||||||
recipeIngredientSections.value = [];
|
recipeIngredientSections.value = [];
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
dark
|
dark
|
||||||
color="primary-lighten-1 top-0 position-relative left-0"
|
color="primary-lighten-1 top-0 position-relative left-0"
|
||||||
:rounded="!$vuetify.display.xs"
|
:rounded="!$vuetify.display.xs"
|
||||||
|
style="width: 100%;"
|
||||||
>
|
>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="arrow-search"
|
id="arrow-search"
|
||||||
@@ -32,9 +33,8 @@
|
|||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="$vuetify.display.xs"
|
v-if="$vuetify.display.xs"
|
||||||
|
icon
|
||||||
size="x-small"
|
size="x-small"
|
||||||
class="rounded-circle"
|
|
||||||
light
|
|
||||||
@click="dialog = false"
|
@click="dialog = false"
|
||||||
>
|
>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
@@ -87,7 +87,7 @@ const emit = defineEmits<{
|
|||||||
selected: [recipe: RecipeSummary];
|
selected: [recipe: RecipeSummary];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const selectedIndex = ref(-1);
|
const selectedIndex = ref(-1);
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ watch(dialog, (val) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
watch(route, close);
|
watch(route, close);
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ whenever(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ import { useLazyRecipes } from "~/composables/recipes";
|
|||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: { RecipeCardSection, RecipeExplorerPageSearch },
|
components: { RecipeCardSection, RecipeExplorerPageSearch },
|
||||||
setup() {
|
setup() {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||||
|
|
||||||
|
|||||||
@@ -141,13 +141,13 @@ const emit = defineEmits<{
|
|||||||
ready: [];
|
ready: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const showRandomLoading = ref(false);
|
const showRandomLoading = ref(false);
|
||||||
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -81,11 +81,11 @@ import {
|
|||||||
usePublicToolStore,
|
usePublicToolStore,
|
||||||
} from "~/composables/store";
|
} from "~/composables/store";
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ const isFavorite = computed(() => {
|
|||||||
|
|
||||||
async function toggleFavorite() {
|
async function toggleFavorite() {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
|
|
||||||
if (!$auth.user.value) return;
|
if (!auth.user.value) return;
|
||||||
if (!isFavorite.value) {
|
if (!isFavorite.value) {
|
||||||
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
await api.users.addFavorite(auth.user.value?.id, props.recipeId);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await api.users.removeFavorite($auth.user.value?.id, props.recipeId);
|
await api.users.removeFavorite(auth.user.value?.id, props.recipeId);
|
||||||
}
|
}
|
||||||
await refreshUserRatings();
|
await refreshUserRatings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
density="compact"
|
density="compact"
|
||||||
variant="solo"
|
variant="solo"
|
||||||
return-object
|
return-object
|
||||||
:items="units || []"
|
:items="filteredUnits"
|
||||||
:custom-filter="normalizeFilter"
|
:custom-filter="() => true"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
class="mx-1"
|
class="mx-1"
|
||||||
:placeholder="$t('recipe.choose-unit')"
|
:placeholder="$t('recipe.choose-unit')"
|
||||||
@@ -117,8 +117,8 @@
|
|||||||
density="compact"
|
density="compact"
|
||||||
variant="solo"
|
variant="solo"
|
||||||
return-object
|
return-object
|
||||||
:items="foods || []"
|
:items="filteredFoods"
|
||||||
:custom-filter="normalizeFilter"
|
:custom-filter="() => true"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
class="mx-1 py-0"
|
class="mx-1 py-0"
|
||||||
:placeholder="$t('recipe.choose-food')"
|
:placeholder="$t('recipe.choose-food')"
|
||||||
@@ -176,7 +176,6 @@
|
|||||||
variant="solo"
|
variant="solo"
|
||||||
return-object
|
return-object
|
||||||
:items="search.data.value || []"
|
:items="search.data.value || []"
|
||||||
:custom-filter="normalizeFilter"
|
|
||||||
item-title="name"
|
item-title="name"
|
||||||
class="mx-1 py-0"
|
class="mx-1 py-0"
|
||||||
:placeholder="$t('search.type-to-search')"
|
:placeholder="$t('search.type-to-search')"
|
||||||
@@ -227,11 +226,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive, toRefs } from "vue";
|
import { ref, computed, reactive, toRefs, watch } from "vue";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||||
import { normalizeFilter } from "~/composables/use-utils";
|
import { useSearch } from "~/composables/use-search";
|
||||||
import { useNuxtApp } from "#app";
|
import { useNuxtApp } from "#app";
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
@@ -343,8 +342,8 @@ const btns = computed(() => {
|
|||||||
// Foods
|
// Foods
|
||||||
const foodStore = useFoodStore();
|
const foodStore = useFoodStore();
|
||||||
const foodData = useFoodData();
|
const foodData = useFoodData();
|
||||||
const foodSearch = ref("");
|
|
||||||
const foodAutocomplete = ref<HTMLInputElement>();
|
const foodAutocomplete = ref<HTMLInputElement>();
|
||||||
|
const { search: foodSearch, filtered: filteredFoods } = useSearch(foodStore.store);
|
||||||
|
|
||||||
async function createAssignFood() {
|
async function createAssignFood() {
|
||||||
foodData.data.name = foodSearch.value;
|
foodData.data.name = foodSearch.value;
|
||||||
@@ -355,8 +354,8 @@ async function createAssignFood() {
|
|||||||
|
|
||||||
// Recipes
|
// Recipes
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||||
@@ -375,8 +374,8 @@ watch(loading, (val) => {
|
|||||||
// Units
|
// Units
|
||||||
const unitStore = useUnitStore();
|
const unitStore = useUnitStore();
|
||||||
const unitsData = useUnitData();
|
const unitsData = useUnitData();
|
||||||
const unitSearch = ref("");
|
|
||||||
const unitAutocomplete = ref<HTMLInputElement>();
|
const unitAutocomplete = ref<HTMLInputElement>();
|
||||||
|
const { search: unitSearch, filtered: filteredUnits } = useSearch(unitStore.store);
|
||||||
|
|
||||||
async function createAssignUnit() {
|
async function createAssignUnit() {
|
||||||
unitsData.data.name = unitSearch.value;
|
unitsData.data.name = unitSearch.value;
|
||||||
@@ -430,9 +429,6 @@ function quantityFilter(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { showTitle } = toRefs(state);
|
const { showTitle } = toRefs(state);
|
||||||
|
|
||||||
const foods = foodStore.store;
|
|
||||||
const units = unitStore.store;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
import { useParsedIngredientText } from "~/composables/recipes";
|
import { useIngredientTextParser } from "~/composables/recipes";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ingredient?: RecipeIngredient;
|
ingredient?: RecipeIngredient;
|
||||||
@@ -20,6 +20,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { ingredient, scale = 1 } = defineProps<Props>();
|
const { ingredient, scale = 1 } = defineProps<Props>();
|
||||||
|
const { useParsedIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
const baseText = computed(() => {
|
const baseText = computed(() => {
|
||||||
if (!ingredient) return "";
|
if (!ingredient) return "";
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/household";
|
import type { RecipeIngredient } from "~/lib/api/types/household";
|
||||||
import { useParsedIngredientText } from "~/composables/recipes";
|
import { useIngredientTextParser } from "~/composables/recipes";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ingredient: RecipeIngredient;
|
ingredient: RecipeIngredient;
|
||||||
@@ -44,8 +44,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
});
|
});
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || auth.user?.value?.groupSlug || "");
|
||||||
|
const { useParsedIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
const parsedIng = computed(() => {
|
const parsedIng = computed(() => {
|
||||||
return useParsedIngredientText(props.ingredient, props.scale, true, groupSlug.value.toString());
|
return useParsedIngredientText(props.ingredient, props.scale, true, groupSlug.value.toString());
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
import { useIngredientTextParser } from "~/composables/recipes";
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -66,6 +66,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
isCookMode: false,
|
isCookMode: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
function validateTitle(title?: string | null) {
|
function validateTitle(title?: string | null) {
|
||||||
return !(title === undefined || title === "" || title === null);
|
return !(title === undefined || title === "" || title === null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const madeThisDialog = ref(false);
|
|||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const domMadeThisForm = ref<VForm>();
|
const domMadeThisForm = ref<VForm>();
|
||||||
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
||||||
subject: "",
|
subject: "",
|
||||||
@@ -179,7 +179,7 @@ const newTimelineEventTimestampString = computed(() => {
|
|||||||
const lastMade = ref(props.recipe.lastMade);
|
const lastMade = ref(props.recipe.lastMade);
|
||||||
const lastMadeReady = ref(false);
|
const lastMadeReady = ref(false);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!$auth.user?.value?.householdSlug) {
|
if (!auth.user?.value?.householdSlug) {
|
||||||
lastMade.value = props.recipe.lastMade;
|
lastMade.value = props.recipe.lastMade;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -255,8 +255,8 @@ async function createTimelineEvent() {
|
|||||||
madeThisFormLoading.value = true;
|
madeThisFormLoading.value = true;
|
||||||
|
|
||||||
newTimelineEvent.value.recipeId = props.recipe.id;
|
newTimelineEvent.value.recipeId = props.recipe.id;
|
||||||
// Note: $auth.user is now a ref
|
// Note: auth.user is now a ref
|
||||||
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName });
|
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: auth.user.value?.fullName });
|
||||||
|
|
||||||
// the user only selects the date, so we set the time to end of day local time
|
// the user only selects the date, so we set the time to end of day local time
|
||||||
// we choose the end of day so it always comes after "new recipe" events
|
// we choose the end of day so it always comes after "new recipe" events
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { frac } = useFraction();
|
const { frac } = useFraction();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || auth.user?.value?.groupSlug || "");
|
||||||
|
|
||||||
const attrs = computed(() => {
|
const attrs = computed(() => {
|
||||||
return props.small
|
return props.small
|
||||||
|
|||||||
@@ -162,9 +162,9 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user?.value?.groupSlug || "");
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Context Menu
|
// Context Menu
|
||||||
|
|||||||
@@ -220,11 +220,11 @@ import { useNavigationWarning } from "~/composables/use-navigation-warning";
|
|||||||
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
||||||
|
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
|
const groupSlug = computed(() => (route.params.groupSlug as string) || auth.user?.value?.groupSlug || "");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|||||||
@@ -431,6 +431,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
||||||
|
|
||||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||||
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
|
||||||
const dialog = ref(false);
|
const dialog = ref(false);
|
||||||
const disabledSteps = ref<number[]>([]);
|
const disabledSteps = ref<number[]>([]);
|
||||||
@@ -581,7 +582,7 @@ function setUsedIngredients() {
|
|||||||
watch(activeRefs, () => setUsedIngredients());
|
watch(activeRefs, () => setUsedIngredients());
|
||||||
|
|
||||||
function autoSetReferences() {
|
function autoSetReferences() {
|
||||||
useExtractIngredientReferences(
|
extractIngredientReferences(
|
||||||
props.recipe.recipeIngredient,
|
props.recipe.recipeIngredient,
|
||||||
activeRefs.value,
|
activeRefs.value,
|
||||||
activeText.value,
|
activeText.value,
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ import type { IngredientFood, IngredientUnit, ParsedIngredient, RecipeIngredient
|
|||||||
import type { Parser } from "~/lib/api/user/recipes/recipe";
|
import type { Parser } from "~/lib/api/user/recipes/recipe";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
import { useIngredientTextParser } from "~/composables/recipes";
|
||||||
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
|
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
|
||||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
@@ -208,6 +208,8 @@ const props = defineProps<{
|
|||||||
ingredients: NoUndefinedField<RecipeIngredient[]>;
|
ingredients: NoUndefinedField<RecipeIngredient[]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:modelValue", value: boolean): void;
|
(e: "update:modelValue", value: boolean): void;
|
||||||
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
|
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ import { useStaticRoutes } from "~/composables/api";
|
|||||||
import type { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
|
import type { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||||
import { parseIngredientText, useNutritionLabels } from "~/composables/recipes";
|
import { useIngredientTextParser, useNutritionLabels } from "~/composables/recipes";
|
||||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||||
|
|
||||||
@@ -362,6 +362,8 @@ const hasNotes = computed(() => {
|
|||||||
return props.recipe.notes && props.recipe.notes.length > 0;
|
return props.recipe.notes && props.recipe.notes.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
function parseText(ingredient: RecipeIngredient) {
|
function parseText(ingredient: RecipeIngredient) {
|
||||||
return parseIngredientText(ingredient, props.scale);
|
return parseIngredientText(ingredient, props.scale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<v-card width="400">
|
<v-card width="400">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="state.search"
|
v-model="searchInput"
|
||||||
v-memo="[state.search]"
|
v-memo="[searchInput]"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
hide-details
|
hide-details
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
<div />
|
<div />
|
||||||
<div class="d-flex py-4 px-1 align-center">
|
<div class="d-flex flex-wrap py-4 px-1 align-center">
|
||||||
<v-btn-toggle
|
<v-btn-toggle
|
||||||
v-if="requireAll != undefined"
|
v-if="requireAll != undefined"
|
||||||
v-model="combinator"
|
v-model="combinator"
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
density="compact"
|
density="compact"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
class="my-1"
|
||||||
>
|
>
|
||||||
<v-btn value="hasAll">
|
<v-btn value="hasAll">
|
||||||
{{ $t('search.has-all') }}
|
{{ $t('search.has-all') }}
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
size="small"
|
size="small"
|
||||||
color="accent"
|
color="accent"
|
||||||
|
class="my-1"
|
||||||
@click="clearSelection"
|
@click="clearSelection"
|
||||||
>
|
>
|
||||||
{{ $t("search.clear-selection") }}
|
{{ $t("search.clear-selection") }}
|
||||||
@@ -144,17 +146,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { watchDebounced } from "@vueuse/core";
|
import type { ISearchableItem } from "~/composables/use-search";
|
||||||
|
import { useSearch } from "~/composables/use-search";
|
||||||
export interface SelectableItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array as () => SelectableItem[],
|
type: Array as () => ISearchableItem[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -173,12 +171,11 @@ export default defineNuxtComponent({
|
|||||||
emits: ["update:requireAll", "update:modelValue"],
|
emits: ["update:requireAll", "update:modelValue"],
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
search: "",
|
|
||||||
menu: false,
|
menu: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use shallowRef for better performance with arrays
|
// Use the search composable
|
||||||
const debouncedSearch = shallowRef("");
|
const { search: searchInput, filtered } = useSearch(computed(() => props.items));
|
||||||
|
|
||||||
const combinator = computed({
|
const combinator = computed({
|
||||||
get: () => (props.requireAll ? "hasAll" : "hasAny"),
|
get: () => (props.requireAll ? "hasAll" : "hasAny"),
|
||||||
@@ -189,7 +186,7 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
// Use shallowRef to prevent deep reactivity on large arrays
|
// Use shallowRef to prevent deep reactivity on large arrays
|
||||||
const selected = computed({
|
const selected = computed({
|
||||||
get: () => props.modelValue as SelectableItem[],
|
get: () => props.modelValue as ISearchableItem[],
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
context.emit("update:modelValue", value);
|
context.emit("update:modelValue", value);
|
||||||
},
|
},
|
||||||
@@ -202,44 +199,12 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
watchDebounced(
|
|
||||||
() => state.search,
|
|
||||||
(newSearch) => {
|
|
||||||
debouncedSearch.value = newSearch;
|
|
||||||
},
|
|
||||||
{ debounce: 500, maxWait: 1500, immediate: false }, // Increased debounce time
|
|
||||||
);
|
|
||||||
|
|
||||||
const filtered = computed(() => {
|
|
||||||
const items = props.items;
|
|
||||||
const search = debouncedSearch.value;
|
|
||||||
|
|
||||||
if (!search || search.length < 2) { // Only filter after 2 characters
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchLower = search.toLowerCase();
|
|
||||||
return items.filter(item => item.name.toLowerCase().includes(searchLower));
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedCount = computed(() => selected.value.length);
|
const selectedCount = computed(() => selected.value.length);
|
||||||
const selectedIds = computed(() => {
|
const selectedIds = computed(() => {
|
||||||
return new Set(selected.value.map(item => item.id));
|
return new Set(selected.value.map(item => item.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCheckboxClick = (item: SelectableItem) => {
|
const handleRadioClick = (item: ISearchableItem) => {
|
||||||
const currentSelection = selected.value;
|
|
||||||
const isSelected = selectedIds.value.has(item.id);
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
selected.value = currentSelection.filter(i => i.id !== item.id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selected.value = [...currentSelection, item];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRadioClick = (item: SelectableItem) => {
|
|
||||||
if (selectedRadio.value === item) {
|
if (selectedRadio.value === item) {
|
||||||
selectedRadio.value = null;
|
selectedRadio.value = null;
|
||||||
}
|
}
|
||||||
@@ -248,18 +213,18 @@ export default defineNuxtComponent({
|
|||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
selectedRadio.value = null;
|
selectedRadio.value = null;
|
||||||
state.search = "";
|
searchInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
combinator,
|
combinator,
|
||||||
state,
|
state,
|
||||||
|
searchInput,
|
||||||
selected,
|
selected,
|
||||||
selectedRadio,
|
selectedRadio,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
filtered,
|
filtered,
|
||||||
handleCheckboxClick,
|
|
||||||
handleRadioClick,
|
handleRadioClick,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
:items="foods"
|
:items="foods"
|
||||||
:label="$t('shopping-list.food')"
|
:label="$t('shopping-list.food')"
|
||||||
:icon="$globals.icons.foods"
|
:icon="$globals.icons.foods"
|
||||||
|
:autofocus="autoFocus === 'food'"
|
||||||
create
|
create
|
||||||
@create="createAssignFood"
|
@create="createAssignFood"
|
||||||
/>
|
/>
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
:label="$t('shopping-list.note')"
|
:label="$t('shopping-list.note')"
|
||||||
rows="1"
|
rows="1"
|
||||||
auto-grow
|
auto-grow
|
||||||
autofocus
|
:autofocus="autoFocus === 'note'"
|
||||||
@keypress="handleNoteKeyPress"
|
@keypress="handleNoteKeyPress"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,6 +166,8 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const autoFocus = !listItem.value.food && listItem.value.note ? "note" : "food";
|
||||||
|
|
||||||
async function createAssignFood(val: string) {
|
async function createAssignFood(val: string) {
|
||||||
// keep UI reactive
|
// keep UI reactive
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
@@ -204,6 +207,7 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
listItem,
|
listItem,
|
||||||
|
autoFocus,
|
||||||
createAssignFood,
|
createAssignFood,
|
||||||
createAssignUnit,
|
createAssignUnit,
|
||||||
assignLabelToFood,
|
assignLabelToFood,
|
||||||
|
|||||||
@@ -62,15 +62,15 @@ export default defineNuxtComponent({
|
|||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { store: users } = useUserStore();
|
const { store: users } = useUserStore();
|
||||||
const user = computed(() => {
|
const user = computed(() => {
|
||||||
return users.value.find(user => user.id === props.userId);
|
return users.value.find(user => user.id === props.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageURL = computed(() => {
|
const imageURL = computed(() => {
|
||||||
// Note: $auth.user is a ref now
|
// Note: auth.user is a ref now
|
||||||
const authUser = $auth.user.value;
|
const authUser = auth.user.value;
|
||||||
const key = authUser?.cacheKey ?? "";
|
const key = authUser?.cacheKey ?? "";
|
||||||
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
|
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export default defineNuxtComponent({
|
|||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
|
|
||||||
const isAdmin = computed(() => $auth.user.value?.admin);
|
const isAdmin = computed(() => auth.user.value?.admin);
|
||||||
const token = ref("");
|
const token = ref("");
|
||||||
const selectedGroup = ref<string | null>(null);
|
const selectedGroup = ref<string | null>(null);
|
||||||
const selectedHousehold = ref<string | null>(null);
|
const selectedHousehold = ref<string | null>(null);
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ export default defineNuxtComponent({
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { $appInfo, $globals } = useNuxtApp();
|
const { $appInfo, $globals } = useNuxtApp();
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const cookbookPreferences = useCookbookPreferences();
|
const cookbookPreferences = useCookbookPreferences();
|
||||||
const ownCookbookStore = useCookbookStore(i18n);
|
const ownCookbookStore = useCookbookStore(i18n);
|
||||||
@@ -152,7 +152,7 @@ export default defineNuxtComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserHouseholdId = computed(() => $auth.user.value?.householdId);
|
const currentUserHouseholdId = computed(() => auth.user.value?.householdId);
|
||||||
const cookbookLinks = computed<SideBarLink[]>(() => {
|
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||||
if (!cookbooks.value?.length) {
|
if (!cookbooks.value?.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -187,7 +187,7 @@ export default defineNuxtComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
links.sort((a, b) => a.title.localeCompare(b.title));
|
links.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
if ($auth.user.value && cookbookPreferences.value.hideOtherHouseholds) {
|
if (auth.user.value && cookbookPreferences.value.hideOtherHouseholds) {
|
||||||
return ownLinks;
|
return ownLinks;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { loggedIn } = useLoggedInState();
|
const { loggedIn } = useLoggedInState();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||||
const { xs, smAndUp } = useDisplay();
|
const { xs, smAndUp } = useDisplay();
|
||||||
|
|
||||||
const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
|
const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
|
||||||
@@ -128,7 +128,7 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
await $auth.signOut("/login?direct=1");
|
await auth.signOut("/login?direct=1");
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -168,13 +168,13 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { loggedIn, isOwnGroup } = useLoggedInState();
|
const { loggedIn, isOwnGroup } = useLoggedInState();
|
||||||
const isAdmin = computed(() => $auth.user.value?.admin);
|
const isAdmin = computed(() => auth.user.value?.admin);
|
||||||
const canManage = computed(() => $auth.user.value?.canManage);
|
const canManage = computed(() => auth.user.value?.canManage);
|
||||||
|
|
||||||
const userFavoritesLink = computed(() => $auth.user.value ? `/user/${$auth.user.value.id}/favorites` : undefined);
|
const userFavoritesLink = computed(() => auth.user.value ? `/user/${auth.user.value.id}/favorites` : undefined);
|
||||||
const userProfileLink = computed(() => $auth.user.value ? "/user/profile" : undefined);
|
const userProfileLink = computed(() => auth.user.value ? "/user/profile" : undefined);
|
||||||
|
|
||||||
const toggleDark = useToggleDarkMode();
|
const toggleDark = useToggleDarkMode();
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ export default defineNuxtComponent({
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
canManage,
|
canManage,
|
||||||
isOwnGroup,
|
isOwnGroup,
|
||||||
sessionUser: $auth.user,
|
sessionUser: auth.user,
|
||||||
toggleDark,
|
toggleDark,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
*/
|
*/
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup(_, ctx) {
|
setup(_, ctx) {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
|
|
||||||
const r = $auth.user.value?.advanced || false;
|
const r = auth.user.value?.advanced || false;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return r ? ctx.slots.default?.() : null;
|
return r ? ctx.slots.default?.() : null;
|
||||||
|
|||||||
@@ -1,211 +1,136 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card
|
<v-form v-model="isValid" validate-on="input">
|
||||||
:color="color"
|
<v-card
|
||||||
:dark="dark"
|
:color="color"
|
||||||
flat
|
:dark="dark"
|
||||||
:width="width"
|
flat
|
||||||
class="my-2"
|
:width="width"
|
||||||
>
|
class="my-2"
|
||||||
<v-row>
|
>
|
||||||
<v-col
|
<v-row>
|
||||||
v-for="(inputField, index) in items"
|
<v-col
|
||||||
:key="index"
|
v-for="(inputField, index) in items"
|
||||||
cols="12"
|
:key="index"
|
||||||
sm="12"
|
cols="12"
|
||||||
>
|
sm="12"
|
||||||
<v-divider
|
|
||||||
v-if="inputField.section"
|
|
||||||
class="my-2"
|
|
||||||
/>
|
|
||||||
<v-card-title
|
|
||||||
v-if="inputField.section"
|
|
||||||
class="pl-0"
|
|
||||||
>
|
>
|
||||||
{{ inputField.section }}
|
<v-divider
|
||||||
</v-card-title>
|
v-if="inputField.section"
|
||||||
<v-card-text
|
class="my-2"
|
||||||
v-if="inputField.sectionDetails"
|
|
||||||
class="pl-0 mt-0 pt-0"
|
|
||||||
>
|
|
||||||
{{ inputField.sectionDetails }}
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<!-- Check Box -->
|
|
||||||
<v-checkbox
|
|
||||||
v-if="inputField.type === fieldTypes.BOOLEAN"
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
:name="inputField.varName"
|
|
||||||
:readonly="fieldState[inputField.varName]?.readonly"
|
|
||||||
:disabled="fieldState[inputField.varName]?.disabled"
|
|
||||||
:hint="inputField.hint"
|
|
||||||
:hide-details="!inputField.hint"
|
|
||||||
:persistent-hint="!!inputField.hint"
|
|
||||||
density="comfortable"
|
|
||||||
@change="emitBlur"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span class="ml-4">
|
|
||||||
{{ inputField.label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</v-checkbox>
|
|
||||||
|
|
||||||
<!-- Text Field -->
|
|
||||||
<v-text-field
|
|
||||||
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
:readonly="fieldState[inputField.varName]?.readonly"
|
|
||||||
:disabled="fieldState[inputField.varName]?.disabled"
|
|
||||||
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
|
|
||||||
variant="solo-filled"
|
|
||||||
flat
|
|
||||||
:autofocus="index === 0"
|
|
||||||
density="comfortable"
|
|
||||||
:label="inputField.label"
|
|
||||||
:name="inputField.varName"
|
|
||||||
:hint="inputField.hint || ''"
|
|
||||||
:rules="!(inputField.disableUpdate && updateMode) ? [...rulesByKey(inputField.rules as any), ...defaultRules] : []"
|
|
||||||
lazy-validation
|
|
||||||
@blur="emitBlur"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Text Area -->
|
|
||||||
<v-textarea
|
|
||||||
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
:readonly="fieldState[inputField.varName]?.readonly"
|
|
||||||
:disabled="fieldState[inputField.varName]?.disabled"
|
|
||||||
variant="solo-filled"
|
|
||||||
flat
|
|
||||||
rows="3"
|
|
||||||
auto-grow
|
|
||||||
density="comfortable"
|
|
||||||
:label="inputField.label"
|
|
||||||
:name="inputField.varName"
|
|
||||||
:hint="inputField.hint || ''"
|
|
||||||
:rules="[...rulesByKey(inputField.rules as any), ...defaultRules]"
|
|
||||||
lazy-validation
|
|
||||||
@blur="emitBlur"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Option Select -->
|
|
||||||
<v-select
|
|
||||||
v-else-if="inputField.type === fieldTypes.SELECT"
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
:readonly="fieldState[inputField.varName]?.readonly"
|
|
||||||
:disabled="fieldState[inputField.varName]?.disabled"
|
|
||||||
variant="solo-filled"
|
|
||||||
flat
|
|
||||||
:label="inputField.label"
|
|
||||||
:name="inputField.varName"
|
|
||||||
:items="inputField.options"
|
|
||||||
item-title="text"
|
|
||||||
item-value="text"
|
|
||||||
:return-object="false"
|
|
||||||
:hint="inputField.hint"
|
|
||||||
density="comfortable"
|
|
||||||
persistent-hint
|
|
||||||
lazy-validation
|
|
||||||
@blur="emitBlur"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Color Picker -->
|
|
||||||
<div
|
|
||||||
v-else-if="inputField.type === fieldTypes.COLOR"
|
|
||||||
class="d-flex"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<v-menu offset-y>
|
|
||||||
<template #activator="{ props: templateProps }">
|
|
||||||
<v-btn
|
|
||||||
class="my-2 ml-auto"
|
|
||||||
style="min-width: 200px"
|
|
||||||
:color="model[inputField.varName]"
|
|
||||||
dark
|
|
||||||
v-bind="templateProps"
|
|
||||||
>
|
|
||||||
{{ inputField.label }}
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-color-picker
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
value="#7417BE"
|
|
||||||
hide-canvas
|
|
||||||
hide-inputs
|
|
||||||
show-swatches
|
|
||||||
class="mx-auto"
|
|
||||||
@input="emitBlur"
|
|
||||||
/>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Object Type -->
|
|
||||||
<div v-else-if="inputField.type === fieldTypes.OBJECT">
|
|
||||||
<auto-form
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
:color="color"
|
|
||||||
:items="(inputField as any).items"
|
|
||||||
@blur="emitBlur"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<v-card-title
|
||||||
|
v-if="inputField.section"
|
||||||
<!-- List Type -->
|
class="pl-0"
|
||||||
<div v-else-if="inputField.type === fieldTypes.LIST">
|
|
||||||
<div
|
|
||||||
v-for="(item, idx) in model[inputField.varName]"
|
|
||||||
:key="idx"
|
|
||||||
>
|
>
|
||||||
<p>
|
{{ inputField.section }}
|
||||||
{{ inputField.label }} {{ idx + 1 }}
|
</v-card-title>
|
||||||
<span>
|
<v-card-text
|
||||||
<BaseButton
|
v-if="inputField.sectionDetails"
|
||||||
class="ml-5"
|
class="pl-0 mt-0 pt-0"
|
||||||
x-small
|
>
|
||||||
delete
|
{{ inputField.sectionDetails }}
|
||||||
@click="removeByIndex(model[inputField.varName], idx)"
|
</v-card-text>
|
||||||
/>
|
|
||||||
|
<!-- Check Box -->
|
||||||
|
<v-checkbox
|
||||||
|
v-if="inputField.type === fieldTypes.BOOLEAN"
|
||||||
|
v-model="model[inputField.varName]"
|
||||||
|
:name="inputField.varName"
|
||||||
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
|
:hint="inputField.hint"
|
||||||
|
:hide-details="!inputField.hint"
|
||||||
|
:persistent-hint="!!inputField.hint"
|
||||||
|
density="comfortable"
|
||||||
|
validate-on="input"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="ml-4">
|
||||||
|
{{ inputField.label }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</template>
|
||||||
<v-divider class="mb-5 mx-2" />
|
</v-checkbox>
|
||||||
<auto-form
|
|
||||||
v-model="model[inputField.varName][idx]"
|
<!-- Text Field -->
|
||||||
:color="color"
|
<v-text-field
|
||||||
:items="(inputField as any).items"
|
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
|
||||||
@blur="emitBlur"
|
v-model="model[inputField.varName]"
|
||||||
/>
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
|
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
|
||||||
|
variant="solo-filled"
|
||||||
|
flat
|
||||||
|
density="comfortable"
|
||||||
|
:label="inputField.label"
|
||||||
|
:name="inputField.varName"
|
||||||
|
:hint="inputField.hint || ''"
|
||||||
|
:rules="!(inputField.disableUpdate && updateMode) ? inputField.rules || [] : []"
|
||||||
|
validate-on="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Text Area -->
|
||||||
|
<v-textarea
|
||||||
|
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
||||||
|
v-model="model[inputField.varName]"
|
||||||
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
|
variant="solo-filled"
|
||||||
|
flat
|
||||||
|
rows="3"
|
||||||
|
auto-grow
|
||||||
|
density="comfortable"
|
||||||
|
:label="inputField.label"
|
||||||
|
:name="inputField.varName"
|
||||||
|
:hint="inputField.hint || ''"
|
||||||
|
:rules="!(inputField.disableUpdate && updateMode) ? inputField.rules || [] : []"
|
||||||
|
validate-on="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Option Select -->
|
||||||
|
<v-select
|
||||||
|
v-else-if="inputField.type === fieldTypes.SELECT"
|
||||||
|
v-model="model[inputField.varName]"
|
||||||
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
|
variant="solo-filled"
|
||||||
|
flat
|
||||||
|
:label="inputField.label"
|
||||||
|
:name="inputField.varName"
|
||||||
|
:items="inputField.options"
|
||||||
|
item-title="text"
|
||||||
|
:item-value="inputField.selectReturnValue || 'text'"
|
||||||
|
:return-object="false"
|
||||||
|
:hint="inputField.hint"
|
||||||
|
density="comfortable"
|
||||||
|
persistent-hint
|
||||||
|
:rules="!(inputField.disableUpdate && updateMode) ? inputField.rules || [] : []"
|
||||||
|
validate-on="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Color Picker -->
|
||||||
|
<div
|
||||||
|
v-else-if="inputField.type === fieldTypes.COLOR"
|
||||||
|
class="d-flex"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<InputColor v-model="model[inputField.varName]" />
|
||||||
</div>
|
</div>
|
||||||
<v-card-actions>
|
</v-col>
|
||||||
<v-spacer />
|
</v-row>
|
||||||
<BaseButton
|
</v-card>
|
||||||
small
|
</v-form>
|
||||||
@click="model[inputField.varName].push(getTemplate((inputField as any).items))"
|
|
||||||
>
|
|
||||||
{{ $t("general.new") }}
|
|
||||||
</BaseButton>
|
|
||||||
</v-card-actions>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { validators } from "@/composables/use-validators";
|
|
||||||
import { fieldTypes } from "@/composables/forms";
|
import { fieldTypes } from "@/composables/forms";
|
||||||
import type { AutoFormItems } from "~/types/auto-forms";
|
import type { AutoFormItems } from "~/types/auto-forms";
|
||||||
|
|
||||||
const BLUR_EVENT = "blur";
|
|
||||||
|
|
||||||
type ValidatorKey = keyof typeof validators;
|
|
||||||
|
|
||||||
// Use defineModel for v-model
|
// Use defineModel for v-model
|
||||||
const modelValue = defineModel<Record<string, any> | any[]>({
|
const model = defineModel<Record<string, any> | any[]>({
|
||||||
type: [Object, Array],
|
type: [Object, Array],
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
const isValid = defineModel("isValid", { type: Boolean, default: false });
|
||||||
// alias to avoid template TS complaining about possible undefined
|
|
||||||
const model = modelValue as any;
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
updateMode: {
|
updateMode: {
|
||||||
@@ -220,10 +145,6 @@ const props = defineProps({
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: "max",
|
default: "max",
|
||||||
},
|
},
|
||||||
globalRules: {
|
|
||||||
default: null,
|
|
||||||
type: Array as () => string[],
|
|
||||||
},
|
|
||||||
color: {
|
color: {
|
||||||
default: null,
|
default: null,
|
||||||
type: String,
|
type: String,
|
||||||
@@ -242,31 +163,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["blur", "update:modelValue"]);
|
|
||||||
|
|
||||||
function rulesByKey(keys?: ValidatorKey[] | null) {
|
|
||||||
if (keys === undefined || keys === null) {
|
|
||||||
return [] as any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const list: any[] = [];
|
|
||||||
keys.forEach((key) => {
|
|
||||||
const split = key.split(":");
|
|
||||||
const validatorKey = split[0] as ValidatorKey;
|
|
||||||
if (validatorKey in validators) {
|
|
||||||
if (split.length === 1) {
|
|
||||||
list.push((validators as any)[validatorKey]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
list.push((validators as any)[validatorKey](split[1] as any));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultRules = computed<any[]>(() => rulesByKey(props.globalRules as any));
|
|
||||||
|
|
||||||
// Combined state map for readonly and disabled fields
|
// Combined state map for readonly and disabled fields
|
||||||
const fieldState = computed<Record<string, { readonly: boolean; disabled: boolean }>>(() => {
|
const fieldState = computed<Record<string, { readonly: boolean; disabled: boolean }>>(() => {
|
||||||
const map: Record<string, { readonly: boolean; disabled: boolean }> = {};
|
const map: Record<string, { readonly: boolean; disabled: boolean }> = {};
|
||||||
@@ -279,25 +175,6 @@ const fieldState = computed<Record<string, { readonly: boolean; disabled: boolea
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
function removeByIndex(list: never[], index: number) {
|
|
||||||
// Removes the item at the index
|
|
||||||
list.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTemplate(item: AutoFormItems) {
|
|
||||||
const obj = {} as { [key: string]: string };
|
|
||||||
|
|
||||||
item.forEach((field) => {
|
|
||||||
obj[field.varName] = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitBlur() {
|
|
||||||
emit(BLUR_EVENT, modelValue.value);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
nudge-bottom="6"
|
nudge-bottom="6"
|
||||||
:close-on-content-click="false"
|
:close-on-content-click="false"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
>
|
>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
{{ $globals.icons.cog }}
|
{{ $globals.icons.cog }}
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { downloadAsJson } from "~/composables/use-utils";
|
import { downloadAsJson } from "~/composables/use-utils";
|
||||||
|
|
||||||
export interface TableConfig {
|
export interface TableConfig {
|
||||||
@@ -120,7 +120,7 @@ export interface TableHeaders {
|
|||||||
text: string;
|
text: string;
|
||||||
value: string;
|
value: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
align?: string;
|
align?: "start" | "center" | "end";
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
sort?: (a: any, b: any) => number;
|
sort?: (a: any, b: any) => number;
|
||||||
}
|
}
|
||||||
@@ -131,106 +131,95 @@ export interface BulkAction {
|
|||||||
event: string;
|
event: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps({
|
||||||
props: {
|
tableConfig: {
|
||||||
tableConfig: {
|
type: Object as () => TableConfig,
|
||||||
type: Object as () => TableConfig,
|
default: () => ({
|
||||||
default: () => ({
|
hideColumns: false,
|
||||||
hideColumns: false,
|
canExport: false,
|
||||||
canExport: false,
|
}),
|
||||||
}),
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: Array as () => TableHeaders[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Array as () => any[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
bulkActions: {
|
|
||||||
type: Array as () => BulkAction[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
initialSort: {
|
|
||||||
type: String,
|
|
||||||
default: "id",
|
|
||||||
},
|
|
||||||
initialSortDesc: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ["delete-one", "edit-one"],
|
headers: {
|
||||||
setup(props, context) {
|
type: Array as () => TableHeaders[],
|
||||||
const i18n = useI18n();
|
required: true,
|
||||||
const sortBy = computed(() => [{
|
},
|
||||||
key: props.initialSort,
|
data: {
|
||||||
order: props.initialSortDesc ? "desc" : "asc",
|
type: Array as () => any[],
|
||||||
}]);
|
required: true,
|
||||||
|
},
|
||||||
// ===========================================================
|
bulkActions: {
|
||||||
// Reactive Headers
|
type: Array as () => BulkAction[],
|
||||||
// Create a local reactive copy of headers that we can modify
|
default: () => [],
|
||||||
const localHeaders = ref([...props.headers]);
|
},
|
||||||
|
initialSort: {
|
||||||
// Watch for changes in props.headers and update local copy
|
type: String,
|
||||||
watch(() => props.headers, (newHeaders) => {
|
default: "id",
|
||||||
localHeaders.value = [...newHeaders];
|
},
|
||||||
}, { deep: true });
|
initialSortDesc: {
|
||||||
|
type: Boolean,
|
||||||
const filteredHeaders = computed<string[]>(() => {
|
default: false,
|
||||||
return localHeaders.value.filter(header => header.show).map(header => header.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const headersWithoutActions = computed(() =>
|
|
||||||
localHeaders.value
|
|
||||||
.filter(header => filteredHeaders.value.includes(header.value))
|
|
||||||
.map(header => ({
|
|
||||||
...header,
|
|
||||||
title: i18n.t(header.text),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeHeaders = computed(() => [
|
|
||||||
...headersWithoutActions.value,
|
|
||||||
{ title: "", value: "actions", show: true, align: "end" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const selected = ref<any[]>([]);
|
|
||||||
|
|
||||||
// ===========================================================
|
|
||||||
// Bulk Action Event Handler
|
|
||||||
|
|
||||||
const bulkActionListener = computed(() => {
|
|
||||||
const handlers: { [key: string]: () => void } = {};
|
|
||||||
|
|
||||||
props.bulkActions.forEach((action) => {
|
|
||||||
handlers[action.event] = () => {
|
|
||||||
context.emit(action.event, selected.value);
|
|
||||||
// clear selection
|
|
||||||
selected.value = [];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return handlers;
|
|
||||||
});
|
|
||||||
|
|
||||||
const search = ref("");
|
|
||||||
|
|
||||||
return {
|
|
||||||
sortBy,
|
|
||||||
selected,
|
|
||||||
localHeaders,
|
|
||||||
filteredHeaders,
|
|
||||||
headersWithoutActions,
|
|
||||||
activeHeaders,
|
|
||||||
bulkActionListener,
|
|
||||||
search,
|
|
||||||
downloadAsJson,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "delete-one" | "edit-one", item: any): void;
|
||||||
|
(e: "bulk-action", event: string, items: any[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const sortBy = computed<{ key: string; order: "asc" | "desc" }[]>(() => [{
|
||||||
|
key: props.initialSort,
|
||||||
|
order: props.initialSortDesc ? "desc" : "asc",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// Reactive Headers
|
||||||
|
// Create a local reactive copy of headers that we can modify
|
||||||
|
const localHeaders = ref([...props.headers]);
|
||||||
|
|
||||||
|
// Watch for changes in props.headers and update local copy
|
||||||
|
watch(() => props.headers, (newHeaders) => {
|
||||||
|
localHeaders.value = [...newHeaders];
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
const filteredHeaders = computed<string[]>(() => {
|
||||||
|
return localHeaders.value.filter(header => header.show).map(header => header.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const headersWithoutActions = computed(() =>
|
||||||
|
localHeaders.value
|
||||||
|
.filter(header => filteredHeaders.value.includes(header.value))
|
||||||
|
.map(header => ({
|
||||||
|
...header,
|
||||||
|
title: i18n.t(header.text),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeHeaders = computed(() => [
|
||||||
|
...headersWithoutActions.value,
|
||||||
|
{ title: "", value: "actions", show: true, align: "end" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selected = ref<any[]>([]);
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// Bulk Action Event Handler
|
||||||
|
|
||||||
|
const bulkActionListener = computed(() => {
|
||||||
|
const handlers: { [key: string]: () => void } = {};
|
||||||
|
|
||||||
|
props.bulkActions.forEach((action) => {
|
||||||
|
handlers[action.event] = () => {
|
||||||
|
emit("bulk-action", action.event, selected.value);
|
||||||
|
// clear selection
|
||||||
|
selected.value = [];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = ref("");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
v-model:search="searchInput"
|
v-model:search="searchInput"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
return-object
|
return-object
|
||||||
:items="items"
|
:items="filteredItems"
|
||||||
:custom-filter="normalizeFilter"
|
|
||||||
:prepend-icon="icon || $globals.icons.tags"
|
:prepend-icon="icon || $globals.icons.tags"
|
||||||
auto-select-first
|
auto-select-first
|
||||||
clearable
|
clearable
|
||||||
color="primary"
|
color="primary"
|
||||||
hide-details
|
hide-details
|
||||||
|
:custom-filter="() => true"
|
||||||
@keyup.enter="emitCreate"
|
@keyup.enter="emitCreate"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||||
import { normalizeFilter } from "~/composables/use-utils";
|
import { useSearch } from "~/composables/use-search";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -85,7 +85,10 @@ export default defineNuxtComponent({
|
|||||||
emits: ["update:modelValue", "update:item-id", "create"],
|
emits: ["update:modelValue", "update:item-id", "create"],
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const autocompleteRef = ref<HTMLInputElement>();
|
const autocompleteRef = ref<HTMLInputElement>();
|
||||||
const searchInput = ref("");
|
|
||||||
|
// Use the search composable
|
||||||
|
const { search: searchInput, filtered: filteredItems } = useSearch(computed(() => props.items));
|
||||||
|
|
||||||
const itemIdVal = computed({
|
const itemIdVal = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return props.itemId || undefined;
|
return props.itemId || undefined;
|
||||||
@@ -123,8 +126,8 @@ export default defineNuxtComponent({
|
|||||||
itemVal,
|
itemVal,
|
||||||
itemIdVal,
|
itemIdVal,
|
||||||
searchInput,
|
searchInput,
|
||||||
|
filteredItems,
|
||||||
emitCreate,
|
emitCreate,
|
||||||
normalizeFilter,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
enum DOMPurifyHook {
|
||||||
|
UponSanitizeAttribute = "uponSanitizeAttribute",
|
||||||
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
source: {
|
source: {
|
||||||
@@ -15,14 +19,26 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
const ALLOWED_STYLE_TAGS = [
|
||||||
|
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||||
|
];
|
||||||
|
|
||||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||||
if (!rawHtml) {
|
if (!rawHtml) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||||
|
if (data.attrName === "style") {
|
||||||
|
const styles = data.attrValue.split(";").filter((style) => {
|
||||||
|
const [property] = style.split(":");
|
||||||
|
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||||
|
});
|
||||||
|
data.attrValue = styles.join(";");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||||
// List based on
|
|
||||||
// https://support.zendesk.com/hc/en-us/articles/4408824584602-Allowing-unsafe-HTML-in-help-center-articles
|
|
||||||
ALLOWED_TAGS: [
|
ALLOWED_TAGS: [
|
||||||
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
||||||
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
@@ -31,10 +47,14 @@ export default defineNuxtComponent({
|
|||||||
],
|
],
|
||||||
ALLOWED_ATTR: [
|
ALLOWED_ATTR: [
|
||||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start",
|
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||||
|
DOMPurify.removeHook(hook);
|
||||||
|
});
|
||||||
|
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
export const fieldTypes = {
|
export const fieldTypes = {
|
||||||
TEXT: "text",
|
TEXT: "text",
|
||||||
TEXT_AREA: "textarea",
|
TEXT_AREA: "textarea",
|
||||||
LIST: "list",
|
|
||||||
SELECT: "select",
|
SELECT: "select",
|
||||||
OBJECT: "object",
|
|
||||||
BOOLEAN: "boolean",
|
BOOLEAN: "boolean",
|
||||||
COLOR: "color",
|
|
||||||
PASSWORD: "password",
|
PASSWORD: "password",
|
||||||
|
COLOR: "color",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -165,14 +165,14 @@ export function clearPageState(slug: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* usePageUser provides a wrapper around $auth that provides a type-safe way to
|
* usePageUser provides a wrapper around auth that provides a type-safe way to
|
||||||
* access the UserOut type from the context. If no user is logged in then an empty
|
* access the UserOut type from the context. If no user is logged in then an empty
|
||||||
* object with all properties set to their zero value is returned.
|
* object with all properties set to their zero value is returned.
|
||||||
*/
|
*/
|
||||||
export function usePageUser(): { user: UserOut } {
|
export function usePageUser(): { user: UserOut } {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
|
|
||||||
if (!$auth.user.value) {
|
if (!auth.user.value) {
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: "",
|
id: "",
|
||||||
@@ -188,5 +188,5 @@ export function usePageUser(): { user: UserOut } {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user: $auth.user.value };
|
return { user: auth.user.value };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,82 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test, vi, beforeEach } from "vitest";
|
||||||
import { useExtractIngredientReferences } from "./use-extract-ingredient-references";
|
import { useExtractIngredientReferences } from "./use-extract-ingredient-references";
|
||||||
|
import { useLocales } from "../use-locales";
|
||||||
|
|
||||||
|
vi.mock("../use-locales");
|
||||||
|
|
||||||
const punctuationMarks = ["*", "?", "/", "!", "**", "&", "."];
|
const punctuationMarks = ["*", "?", "/", "!", "**", "&", "."];
|
||||||
|
|
||||||
describe("test use extract ingredient references", () => {
|
describe("test use extract ingredient references", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
|
||||||
test("when text empty return empty", () => {
|
test("when text empty return empty", () => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "");
|
||||||
expect(result).toStrictEqual(new Set());
|
expect(result).toStrictEqual(new Set());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when and ingredient matches exactly and has a reference id, return the referenceId", () => {
|
test("when and ingredient matches exactly and has a reference id, return the referenceId", () => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion");
|
||||||
|
|
||||||
expect(result).toEqual(new Set(["123"]));
|
expect(result).toEqual(new Set(["123"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(punctuationMarks)("when ingredient is suffixed by punctuation, return the referenceId", (suffix) => {
|
test.each(punctuationMarks)("when ingredient is suffixed by punctuation, return the referenceId", (suffix) => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion" + suffix, true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion" + suffix);
|
||||||
|
|
||||||
expect(result).toEqual(new Set(["123"]));
|
expect(result).toEqual(new Set(["123"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(punctuationMarks)("when ingredient is prefixed by punctuation, return the referenceId", (prefix) => {
|
test.each(punctuationMarks)("when ingredient is prefixed by punctuation, return the referenceId", (prefix) => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing " + prefix + "Onion", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing " + prefix + "Onion");
|
||||||
expect(result).toEqual(new Set(["123"]));
|
expect(result).toEqual(new Set(["123"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when ingredient is first on a multiline, return the referenceId", () => {
|
test("when ingredient is first on a multiline, return the referenceId", () => {
|
||||||
const multilineSting = "lksjdlk\nOnion";
|
const multilineSting = "lksjdlk\nOnion";
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], multilineSting, true);
|
|
||||||
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], multilineSting);
|
||||||
expect(result).toEqual(new Set(["123"]));
|
expect(result).toEqual(new Set(["123"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when the ingredient matches partially exactly and has a reference id, return the referenceId", () => {
|
test("when the ingredient matches partially exactly and has a reference id, return the referenceId", () => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onions", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onions");
|
||||||
expect(result).toEqual(new Set(["123"]));
|
expect(result).toEqual(new Set(["123"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when the ingredient matches with different casing and has a reference id, return the referenceId", () => {
|
test("when the ingredient matches with different casing and has a reference id, return the referenceId", () => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing oNions", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing oNions");
|
||||||
expect(result).toEqual(new Set(["123"]));
|
expect(result).toEqual(new Set(["123"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when no ingredients, return empty", () => {
|
test("when no ingredients, return empty", () => {
|
||||||
const result = useExtractIngredientReferences([], [], "A sentence containing oNions", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([], [], "A sentence containing oNions");
|
||||||
expect(result).toEqual(new Set());
|
expect(result).toEqual(new Set());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when and ingredient matches but in the existing referenceIds, do not return the referenceId", () => {
|
test("when and ingredient matches but in the existing referenceIds, do not return the referenceId", () => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], ["123"], "A sentence containing Onion", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onion", referenceId: "123" }], ["123"], "A sentence containing Onion");
|
||||||
|
|
||||||
expect(result).toEqual(new Set());
|
expect(result).toEqual(new Set());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("when an word is 2 letter of shorter, it is ignored", () => {
|
test("when an word is 2 letter of shorter, it is ignored", () => {
|
||||||
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], [], "A sentence containing On", true);
|
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||||
|
const result = extractIngredientReferences([{ note: "Onion", referenceId: "123" }], [], "A sentence containing On");
|
||||||
|
|
||||||
expect(result).toEqual(new Set());
|
expect(result).toEqual(new Set());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
import { useIngredientTextParser } from "~/composables/recipes";
|
||||||
|
|
||||||
function normalize(word: string): string {
|
function normalize(word: string): string {
|
||||||
let normalizing = word;
|
let normalizing = word;
|
||||||
@@ -18,11 +18,6 @@ function removeStartingPunctuation(word: string): string {
|
|||||||
return word.replace(punctuationAtBeginning, "");
|
return word.replace(punctuationAtBeginning, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string) {
|
|
||||||
const searchText = parseIngredientText(ingredient);
|
|
||||||
return searchText.toLowerCase().includes(word.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBlackListedWord(word: string) {
|
function isBlackListedWord(word: string) {
|
||||||
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
|
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
|
||||||
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
|
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
|
||||||
@@ -39,20 +34,33 @@ function isBlackListedWord(word: string) {
|
|||||||
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
|
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string): Set<string> {
|
export function useExtractIngredientReferences() {
|
||||||
const availableIngredients = recipeIngredients
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
.filter(ingredient => ingredient.referenceId !== undefined)
|
|
||||||
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
|
|
||||||
|
|
||||||
const allMatchedIngredientIds: string[] = text
|
function extractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string): Set<string> {
|
||||||
.toLowerCase()
|
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string) {
|
||||||
.split(/\s/)
|
const searchText = parseIngredientText(ingredient);
|
||||||
.map(normalize)
|
return searchText.toLowerCase().includes(word.toLowerCase());
|
||||||
.filter(word => word.length > 2)
|
}
|
||||||
.filter(word => !isBlackListedWord(word))
|
|
||||||
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word)))
|
|
||||||
.map(ingredient => ingredient.referenceId as string);
|
|
||||||
// deduplicate
|
|
||||||
|
|
||||||
return new Set<string>(allMatchedIngredientIds);
|
const availableIngredients = recipeIngredients
|
||||||
|
.filter(ingredient => ingredient.referenceId !== undefined)
|
||||||
|
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
|
||||||
|
|
||||||
|
const allMatchedIngredientIds: string[] = text
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s/)
|
||||||
|
.map(normalize)
|
||||||
|
.filter(word => word.length > 2)
|
||||||
|
.filter(word => !isBlackListedWord(word))
|
||||||
|
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word)))
|
||||||
|
.map(ingredient => ingredient.referenceId as string);
|
||||||
|
// deduplicate
|
||||||
|
|
||||||
|
return new Set<string>(allMatchedIngredientIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
extractIngredientReferences,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export { useFraction } from "./use-fraction";
|
export { useFraction } from "./use-fraction";
|
||||||
export { useRecipe } from "./use-recipe";
|
export { useRecipe } from "./use-recipe";
|
||||||
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
|
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
|
||||||
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
|
export { useIngredientTextParser } from "./use-recipe-ingredients";
|
||||||
export { useNutritionLabels } from "./use-recipe-nutrition";
|
export { useNutritionLabels } from "./use-recipe-nutrition";
|
||||||
export { useTools } from "./use-recipe-tools";
|
export { useTools } from "./use-recipe-tools";
|
||||||
export { useRecipePermissions } from "./use-recipe-permissions";
|
export { useRecipePermissions } from "./use-recipe-permissions";
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||||
import { parseIngredientText } from "./use-recipe-ingredients";
|
import { useIngredientTextParser } from "./use-recipe-ingredients";
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
|
import { useLocales } from "../use-locales";
|
||||||
|
|
||||||
|
vi.mock("../use-locales");
|
||||||
|
|
||||||
|
let parseIngredientText: (ingredient: RecipeIngredient, scale?: number, includeFormating?: boolean) => string;
|
||||||
|
|
||||||
|
describe("parseIngredientText", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "always" },
|
||||||
|
} as any);
|
||||||
|
({ parseIngredientText } = useIngredientTextParser());
|
||||||
|
});
|
||||||
|
|
||||||
describe(parseIngredientText.name, () => {
|
|
||||||
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
|
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
food: {
|
food: {
|
||||||
@@ -128,4 +141,98 @@ describe(parseIngredientText.name, () => {
|
|||||||
|
|
||||||
expect(parseIngredientText(ingredient, 2)).toEqual("2 tablespoons diced onions");
|
expect(parseIngredientText(ingredient, 2)).toEqual("2 tablespoons diced onions");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("plural handling: 'always' strategy uses plural food with unit", () => {
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "always" },
|
||||||
|
} as any);
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onions");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural handling: 'never' strategy never uses plural food", () => {
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "never" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "never" },
|
||||||
|
} as any);
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onion");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural handling: 'without-unit' strategy uses plural food without unit", () => {
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
|
||||||
|
} as any);
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
|
unit: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient)).toEqual("2 diced onions");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural handling: 'without-unit' strategy uses singular food with unit", () => {
|
||||||
|
vi.mocked(useLocales).mockReturnValue({
|
||||||
|
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
|
||||||
|
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
|
||||||
|
} as any);
|
||||||
|
const { parseIngredientText } = useIngredientTextParser();
|
||||||
|
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onion");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decimal below minimum precision shows < 0.001", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0.0001,
|
||||||
|
unit: { id: "1", name: "cup", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "salt" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient)).toEqual("< 0.001 cup salt");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fraction below minimum denominator shows < 1/10", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0.05,
|
||||||
|
unit: { id: "1", name: "cup", fraction: true, useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "salt" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient)).toEqual("< <sup>1</sup><span>⁄</span><sub>10</sub> cup salt");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fraction below minimum denominator without formatting shows < 1/10", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0.05,
|
||||||
|
unit: { id: "1", name: "cup", fraction: true, useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "salt" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, 1, false)).toEqual("< 1/10 cup salt");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { useFraction } from "./use-fraction";
|
import { useFraction } from "./use-fraction";
|
||||||
|
import { useLocales } from "../use-locales";
|
||||||
import type { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
const { frac } = useFraction();
|
const { frac } = useFraction();
|
||||||
|
|
||||||
|
const FRAC_MIN_DENOM = 10;
|
||||||
|
const DECIMAL_PRECISION = 3;
|
||||||
|
|
||||||
export function sanitizeIngredientHTML(rawHtml: string) {
|
export function sanitizeIngredientHTML(rawHtml: string) {
|
||||||
return DOMPurify.sanitize(rawHtml, {
|
return DOMPurify.sanitize(rawHtml, {
|
||||||
USE_PROFILES: { html: true },
|
USE_PROFILES: { html: true },
|
||||||
@@ -56,47 +60,90 @@ type ParsedIngredientText = {
|
|||||||
recipeLink?: string;
|
recipeLink?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true, groupSlug?: string): ParsedIngredientText {
|
function shouldUsePluralFood(quantity: number, hasUnit: boolean, pluralFoodHandling: string): boolean {
|
||||||
const { quantity, food, unit, note, referencedRecipe } = ingredient;
|
if (quantity && quantity <= 1) {
|
||||||
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
return false;
|
||||||
const usePluralFood = (!quantity) || quantity * scale > 1;
|
|
||||||
|
|
||||||
let returnQty = "";
|
|
||||||
|
|
||||||
// casting to number is required as sometimes quantity is a string
|
|
||||||
if (quantity && Number(quantity) !== 0) {
|
|
||||||
if (unit && !unit.fraction) {
|
|
||||||
returnQty = Number((quantity * scale).toPrecision(3)).toString();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const fraction = frac(quantity * scale, 10, true);
|
|
||||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
|
||||||
returnQty += fraction[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fraction[1] > 0) {
|
|
||||||
returnQty += includeFormating
|
|
||||||
? `<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>`
|
|
||||||
: ` ${fraction[1]}/${fraction[2]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unitName = useUnitName(unit || undefined, usePluralUnit);
|
switch (pluralFoodHandling) {
|
||||||
const ingName = referencedRecipe ? referencedRecipe.name || "" : useFoodName(food || undefined, usePluralFood);
|
case "always":
|
||||||
|
return true;
|
||||||
|
case "without-unit":
|
||||||
|
return !(quantity && hasUnit);
|
||||||
|
case "never":
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// same as without-unit
|
||||||
|
return !(quantity && hasUnit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIngredientTextParser() {
|
||||||
|
const { locales, locale } = useLocales();
|
||||||
|
|
||||||
|
function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true, groupSlug?: string): ParsedIngredientText {
|
||||||
|
const filteredLocales = locales.filter(lc => lc.value === locale.value);
|
||||||
|
const pluralFoodHandling = filteredLocales.length ? filteredLocales[0].pluralFoodHandling : "without-unit";
|
||||||
|
|
||||||
|
const { quantity, food, unit, note, referencedRecipe } = ingredient;
|
||||||
|
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
||||||
|
const usePluralFood = shouldUsePluralFood((quantity || 0) * scale, !!unit, pluralFoodHandling);
|
||||||
|
|
||||||
|
let returnQty = "";
|
||||||
|
|
||||||
|
// casting to number is required as sometimes quantity is a string
|
||||||
|
if (quantity && Number(quantity) !== 0) {
|
||||||
|
const scaledQuantity = Number((quantity * scale));
|
||||||
|
|
||||||
|
if (unit && !unit.fraction) {
|
||||||
|
const minVal = 10 ** -DECIMAL_PRECISION;
|
||||||
|
returnQty = scaledQuantity >= minVal
|
||||||
|
? Number(scaledQuantity.toPrecision(DECIMAL_PRECISION)).toString()
|
||||||
|
: `< ${minVal}`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const minVal = 1 / FRAC_MIN_DENOM;
|
||||||
|
const isUnderMinVal = !(scaledQuantity >= minVal);
|
||||||
|
|
||||||
|
const fraction = !isUnderMinVal ? frac(scaledQuantity, FRAC_MIN_DENOM, true) : [0, 1, FRAC_MIN_DENOM];
|
||||||
|
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||||
|
returnQty += fraction[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fraction[1] > 0) {
|
||||||
|
returnQty += includeFormating
|
||||||
|
? `<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>`
|
||||||
|
: ` ${fraction[1]}/${fraction[2]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnderMinVal) {
|
||||||
|
returnQty = `< ${returnQty}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitName = useUnitName(unit || undefined, usePluralUnit);
|
||||||
|
const ingName = referencedRecipe ? referencedRecipe.name || "" : useFoodName(food || undefined, usePluralFood);
|
||||||
|
|
||||||
|
return {
|
||||||
|
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
||||||
|
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
||||||
|
name: ingName ? sanitizeIngredientHTML(ingName) : undefined,
|
||||||
|
note: note ? sanitizeIngredientHTML(note) : undefined,
|
||||||
|
recipeLink: useRecipeLink(referencedRecipe || undefined, groupSlug),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
|
||||||
|
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
|
||||||
|
|
||||||
|
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
|
||||||
|
return sanitizeIngredientHTML(text);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
useParsedIngredientText,
|
||||||
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
parseIngredientText,
|
||||||
name: ingName ? sanitizeIngredientHTML(ingName) : undefined,
|
|
||||||
note: note ? sanitizeIngredientHTML(note) : undefined,
|
|
||||||
recipeLink: useRecipeLink(referencedRecipe || undefined, groupSlug),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
|
|
||||||
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
|
|
||||||
|
|
||||||
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
|
|
||||||
return sanitizeIngredientHTML(text);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,251 +5,293 @@ export const LOCALES = [
|
|||||||
value: "zh-TW",
|
value: "zh-TW",
|
||||||
progress: 9,
|
progress: 9,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "简体中文 (Chinese simplified)",
|
name: "简体中文 (Chinese simplified)",
|
||||||
value: "zh-CN",
|
value: "zh-CN",
|
||||||
progress: 38,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tiếng Việt (Vietnamese)",
|
name: "Tiếng Việt (Vietnamese)",
|
||||||
value: "vi-VN",
|
value: "vi-VN",
|
||||||
progress: 2,
|
progress: 2,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Українська (Ukrainian)",
|
name: "Українська (Ukrainian)",
|
||||||
value: "uk-UA",
|
value: "uk-UA",
|
||||||
progress: 83,
|
progress: 83,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Türkçe (Turkish)",
|
name: "Türkçe (Turkish)",
|
||||||
value: "tr-TR",
|
value: "tr-TR",
|
||||||
progress: 40,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Svenska (Swedish)",
|
name: "Svenska (Swedish)",
|
||||||
value: "sv-SE",
|
value: "sv-SE",
|
||||||
progress: 61,
|
progress: 61,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "српски (Serbian)",
|
name: "српски (Serbian)",
|
||||||
value: "sr-SP",
|
value: "sr-SP",
|
||||||
progress: 9,
|
progress: 16,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Slovenščina (Slovenian)",
|
name: "Slovenščina (Slovenian)",
|
||||||
value: "sl-SI",
|
value: "sl-SI",
|
||||||
progress: 40,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Slovenčina (Slovak)",
|
name: "Slovenčina (Slovak)",
|
||||||
value: "sk-SK",
|
value: "sk-SK",
|
||||||
progress: 47,
|
progress: 47,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Pусский (Russian)",
|
name: "Pусский (Russian)",
|
||||||
value: "ru-RU",
|
value: "ru-RU",
|
||||||
progress: 44,
|
progress: 44,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Română (Romanian)",
|
name: "Română (Romanian)",
|
||||||
value: "ro-RO",
|
value: "ro-RO",
|
||||||
progress: 44,
|
progress: 44,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Português (Portuguese)",
|
name: "Português (Portuguese)",
|
||||||
value: "pt-PT",
|
value: "pt-PT",
|
||||||
progress: 39,
|
progress: 39,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Português do Brasil (Brazilian Portuguese)",
|
name: "Português do Brasil (Brazilian Portuguese)",
|
||||||
value: "pt-BR",
|
value: "pt-BR",
|
||||||
progress: 46,
|
progress: 46,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Polski (Polish)",
|
name: "Polski (Polish)",
|
||||||
value: "pl-PL",
|
value: "pl-PL",
|
||||||
progress: 49,
|
progress: 49,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Norsk (Norwegian)",
|
name: "Norsk (Norwegian)",
|
||||||
value: "no-NO",
|
value: "no-NO",
|
||||||
progress: 42,
|
progress: 42,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nederlands (Dutch)",
|
name: "Nederlands (Dutch)",
|
||||||
value: "nl-NL",
|
value: "nl-NL",
|
||||||
progress: 54,
|
progress: 60,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Latviešu (Latvian)",
|
name: "Latviešu (Latvian)",
|
||||||
value: "lv-LV",
|
value: "lv-LV",
|
||||||
progress: 35,
|
progress: 35,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Lietuvių (Lithuanian)",
|
name: "Lietuvių (Lithuanian)",
|
||||||
value: "lt-LT",
|
value: "lt-LT",
|
||||||
progress: 30,
|
progress: 30,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "한국어 (Korean)",
|
name: "한국어 (Korean)",
|
||||||
value: "ko-KR",
|
value: "ko-KR",
|
||||||
progress: 38,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "日本語 (Japanese)",
|
name: "日本語 (Japanese)",
|
||||||
value: "ja-JP",
|
value: "ja-JP",
|
||||||
progress: 36,
|
progress: 36,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Italiano (Italian)",
|
name: "Italiano (Italian)",
|
||||||
value: "it-IT",
|
value: "it-IT",
|
||||||
progress: 49,
|
progress: 52,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Íslenska (Icelandic)",
|
name: "Íslenska (Icelandic)",
|
||||||
value: "is-IS",
|
value: "is-IS",
|
||||||
progress: 43,
|
progress: 43,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Magyar (Hungarian)",
|
name: "Magyar (Hungarian)",
|
||||||
value: "hu-HU",
|
value: "hu-HU",
|
||||||
progress: 46,
|
progress: 46,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Hrvatski (Croatian)",
|
name: "Hrvatski (Croatian)",
|
||||||
value: "hr-HR",
|
value: "hr-HR",
|
||||||
progress: 30,
|
progress: 30,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "עברית (Hebrew)",
|
name: "עברית (Hebrew)",
|
||||||
value: "he-IL",
|
value: "he-IL",
|
||||||
progress: 64,
|
progress: 64,
|
||||||
dir: "rtl",
|
dir: "rtl",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Galego (Galician)",
|
name: "Galego (Galician)",
|
||||||
value: "gl-ES",
|
value: "gl-ES",
|
||||||
progress: 38,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Français (French)",
|
name: "Français (French)",
|
||||||
value: "fr-FR",
|
value: "fr-FR",
|
||||||
progress: 67,
|
progress: 67,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Français canadien (Canadian French)",
|
name: "Français canadien (Canadian French)",
|
||||||
value: "fr-CA",
|
value: "fr-CA",
|
||||||
progress: 83,
|
progress: 83,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Belge (Belgian)",
|
name: "Belge (Belgian)",
|
||||||
value: "fr-BE",
|
value: "fr-BE",
|
||||||
progress: 39,
|
progress: 39,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Suomi (Finnish)",
|
name: "Suomi (Finnish)",
|
||||||
value: "fi-FI",
|
value: "fi-FI",
|
||||||
progress: 40,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Eesti (Estonian)",
|
name: "Eesti (Estonian)",
|
||||||
value: "et-EE",
|
value: "et-EE",
|
||||||
progress: 44,
|
progress: 45,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Español (Spanish)",
|
name: "Español (Spanish)",
|
||||||
value: "es-ES",
|
value: "es-ES",
|
||||||
progress: 45,
|
progress: 46,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "American English",
|
name: "American English",
|
||||||
value: "en-US",
|
value: "en-US",
|
||||||
progress: 100.0,
|
progress: 100,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "without-unit",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "British English",
|
name: "British English",
|
||||||
value: "en-GB",
|
value: "en-GB",
|
||||||
progress: 42,
|
progress: 42,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "without-unit",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ελληνικά (Greek)",
|
name: "Ελληνικά (Greek)",
|
||||||
value: "el-GR",
|
value: "el-GR",
|
||||||
progress: 41,
|
progress: 41,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Deutsch (German)",
|
name: "Deutsch (German)",
|
||||||
value: "de-DE",
|
value: "de-DE",
|
||||||
progress: 83,
|
progress: 85,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Dansk (Danish)",
|
name: "Dansk (Danish)",
|
||||||
value: "da-DK",
|
value: "da-DK",
|
||||||
progress: 63,
|
progress: 65,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Čeština (Czech)",
|
name: "Čeština (Czech)",
|
||||||
value: "cs-CZ",
|
value: "cs-CZ",
|
||||||
progress: 43,
|
progress: 43,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Català (Catalan)",
|
name: "Català (Catalan)",
|
||||||
value: "ca-ES",
|
value: "ca-ES",
|
||||||
progress: 40,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Български (Bulgarian)",
|
name: "Български (Bulgarian)",
|
||||||
value: "bg-BG",
|
value: "bg-BG",
|
||||||
progress: 49,
|
progress: 49,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "العربية (Arabic)",
|
name: "العربية (Arabic)",
|
||||||
value: "ar-SA",
|
value: "ar-SA",
|
||||||
progress: 25,
|
progress: 25,
|
||||||
dir: "rtl",
|
dir: "rtl",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Afrikaans (Afrikaans)",
|
name: "Afrikaans (Afrikaans)",
|
||||||
value: "af-ZA",
|
value: "af-ZA",
|
||||||
progress: 26,
|
progress: 26,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { LocaleObject } from "@nuxtjs/i18n";
|
import type { LocaleObject } from "@nuxtjs/i18n";
|
||||||
import { LOCALES } from "./available-locales";
|
import { LOCALES } from "./available-locales";
|
||||||
|
import { useGlobalI18n } from "../use-global-i18n";
|
||||||
|
|
||||||
export const useLocales = () => {
|
export const useLocales = () => {
|
||||||
const i18n = useI18n();
|
const i18n = useGlobalI18n();
|
||||||
const { current: vuetifyLocale } = useLocale();
|
const { current: vuetifyLocale } = useLocale();
|
||||||
|
|
||||||
const locale = computed<LocaleObject["code"]>({
|
const locale = computed<LocaleObject["code"]>({
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export const useLoggedInState = function () {
|
export const useLoggedInState = function () {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const loggedIn = computed(() => $auth.loggedIn.value);
|
const loggedIn = computed(() => auth.loggedIn.value);
|
||||||
const isOwnGroup = computed(() => {
|
const isOwnGroup = computed(() => {
|
||||||
if (!route.params.groupSlug) {
|
if (!route.params.groupSlug) {
|
||||||
return loggedIn.value;
|
return loggedIn.value;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return loggedIn.value && $auth.user.value?.groupSlug === route.params.groupSlug;
|
return loggedIn.value && auth.user.value?.groupSlug === route.params.groupSlug;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import type { LogicalOperator, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
|
import type { LogicalOperator, PlaceholderKeyword, RecipeOrganizer, RelationalKeyword, RelationalOperator } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
export interface FieldLogicalOperator {
|
export interface FieldLogicalOperator {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -11,6 +11,11 @@ export interface FieldRelationalOperator {
|
|||||||
value: RelationalKeyword | RelationalOperator;
|
value: RelationalKeyword | RelationalOperator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FieldPlaceholderKeyword {
|
||||||
|
label: string;
|
||||||
|
value: PlaceholderKeyword;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OrganizerBase {
|
export interface OrganizerBase {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -22,6 +27,7 @@ export type FieldType
|
|||||||
| "number"
|
| "number"
|
||||||
| "boolean"
|
| "boolean"
|
||||||
| "date"
|
| "date"
|
||||||
|
| "relativeDate"
|
||||||
| RecipeOrganizer;
|
| RecipeOrganizer;
|
||||||
|
|
||||||
export type FieldValue
|
export type FieldValue
|
||||||
@@ -41,8 +47,8 @@ export interface FieldDefinition {
|
|||||||
label: string;
|
label: string;
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
|
|
||||||
// only for select/organizer fields
|
// Select/Organizer
|
||||||
fieldOptions?: SelectableItem[];
|
fieldChoices?: SelectableItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Field extends FieldDefinition {
|
export interface Field extends FieldDefinition {
|
||||||
@@ -50,10 +56,10 @@ export interface Field extends FieldDefinition {
|
|||||||
logicalOperator?: FieldLogicalOperator;
|
logicalOperator?: FieldLogicalOperator;
|
||||||
value: FieldValue;
|
value: FieldValue;
|
||||||
relationalOperatorValue: FieldRelationalOperator;
|
relationalOperatorValue: FieldRelationalOperator;
|
||||||
relationalOperatorOptions: FieldRelationalOperator[];
|
relationalOperatorChoices: FieldRelationalOperator[];
|
||||||
rightParenthesis?: string;
|
rightParenthesis?: string;
|
||||||
|
|
||||||
// only for select/organizer fields
|
// Select/Organizer
|
||||||
values: FieldValue[];
|
values: FieldValue[];
|
||||||
organizers: OrganizerBase[];
|
organizers: OrganizerBase[];
|
||||||
}
|
}
|
||||||
@@ -161,6 +167,36 @@ export function useQueryFilterBuilder() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const placeholderKeywords = computed<Record<PlaceholderKeyword, FieldPlaceholderKeyword>>(() => {
|
||||||
|
const NOW = {
|
||||||
|
label: "Now",
|
||||||
|
value: "$NOW",
|
||||||
|
} as FieldPlaceholderKeyword;
|
||||||
|
|
||||||
|
return {
|
||||||
|
$NOW: NOW,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const relativeDateRelOps = computed<Record<RelationalKeyword | RelationalOperator, FieldRelationalOperator>>(() => {
|
||||||
|
const ops = { ...relOps.value };
|
||||||
|
|
||||||
|
ops[">="] = { ...relOps.value[">="], label: i18n.t("query-filter.relational-operators.is-newer-than") };
|
||||||
|
ops["<="] = { ...relOps.value["<="], label: i18n.t("query-filter.relational-operators.is-older-than") };
|
||||||
|
|
||||||
|
return ops;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRelOps(fieldType: FieldType): typeof relOps | typeof relativeDateRelOps {
|
||||||
|
switch (fieldType) {
|
||||||
|
case "relativeDate":
|
||||||
|
return relativeDateRelOps;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return relOps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isOrganizerType(type: FieldType): type is Organizer {
|
function isOrganizerType(type: FieldType): type is Organizer {
|
||||||
return (
|
return (
|
||||||
type === Organizer.Category
|
type === Organizer.Category
|
||||||
@@ -173,10 +209,14 @@ export function useQueryFilterBuilder() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getFieldFromFieldDef(field: Field | FieldDefinition, resetValue = false): Field {
|
function getFieldFromFieldDef(field: Field | FieldDefinition, resetValue = false): Field {
|
||||||
const updatedField = { logicalOperator: logOps.value.AND, ...field } as Field;
|
const updatedField = {
|
||||||
let operatorOptions: FieldRelationalOperator[];
|
logicalOperator: logOps.value.AND,
|
||||||
if (updatedField.fieldOptions?.length || isOrganizerType(updatedField.type)) {
|
...field,
|
||||||
operatorOptions = [
|
} as Field;
|
||||||
|
|
||||||
|
let operatorChoices: FieldRelationalOperator[];
|
||||||
|
if (updatedField.fieldChoices?.length || isOrganizerType(updatedField.type)) {
|
||||||
|
operatorChoices = [
|
||||||
relOps.value["IN"],
|
relOps.value["IN"],
|
||||||
relOps.value["NOT IN"],
|
relOps.value["NOT IN"],
|
||||||
relOps.value["CONTAINS ALL"],
|
relOps.value["CONTAINS ALL"],
|
||||||
@@ -185,7 +225,7 @@ export function useQueryFilterBuilder() {
|
|||||||
else {
|
else {
|
||||||
switch (updatedField.type) {
|
switch (updatedField.type) {
|
||||||
case "string":
|
case "string":
|
||||||
operatorOptions = [
|
operatorChoices = [
|
||||||
relOps.value["="],
|
relOps.value["="],
|
||||||
relOps.value["<>"],
|
relOps.value["<>"],
|
||||||
relOps.value["LIKE"],
|
relOps.value["LIKE"],
|
||||||
@@ -193,7 +233,7 @@ export function useQueryFilterBuilder() {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "number":
|
case "number":
|
||||||
operatorOptions = [
|
operatorChoices = [
|
||||||
relOps.value["="],
|
relOps.value["="],
|
||||||
relOps.value["<>"],
|
relOps.value["<>"],
|
||||||
relOps.value[">"],
|
relOps.value[">"],
|
||||||
@@ -203,10 +243,10 @@ export function useQueryFilterBuilder() {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "boolean":
|
case "boolean":
|
||||||
operatorOptions = [relOps.value["="]];
|
operatorChoices = [relOps.value["="]];
|
||||||
break;
|
break;
|
||||||
case "date":
|
case "date":
|
||||||
operatorOptions = [
|
operatorChoices = [
|
||||||
relOps.value["="],
|
relOps.value["="],
|
||||||
relOps.value["<>"],
|
relOps.value["<>"],
|
||||||
relOps.value[">"],
|
relOps.value[">"],
|
||||||
@@ -215,13 +255,20 @@ export function useQueryFilterBuilder() {
|
|||||||
relOps.value["<="],
|
relOps.value["<="],
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
case "relativeDate":
|
||||||
|
operatorChoices = [
|
||||||
|
// "<=" is first since "older than" is the most common operator
|
||||||
|
relativeDateRelOps.value["<="],
|
||||||
|
relativeDateRelOps.value[">="],
|
||||||
|
];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
operatorOptions = [relOps.value["="], relOps.value["<>"]];
|
operatorChoices = [relOps.value["="], relOps.value["<>"]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatedField.relationalOperatorOptions = operatorOptions;
|
updatedField.relationalOperatorChoices = operatorChoices;
|
||||||
if (!operatorOptions.includes(updatedField.relationalOperatorValue)) {
|
if (!operatorChoices.includes(updatedField.relationalOperatorValue)) {
|
||||||
updatedField.relationalOperatorValue = operatorOptions[0];
|
updatedField.relationalOperatorValue = operatorChoices[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resetValue) {
|
if (resetValue) {
|
||||||
@@ -271,7 +318,7 @@ export function useQueryFilterBuilder() {
|
|||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
|
||||||
if (field.values?.length) {
|
if (field.values?.length) {
|
||||||
let val: string;
|
let val: string;
|
||||||
if (field.type === "string" || field.type === "date" || isOrganizerType(field.type)) {
|
if (field.type === "string" || field.type === "date" || isOrganizerType(field.type)) {
|
||||||
@@ -316,7 +363,8 @@ export function useQueryFilterBuilder() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
logOps,
|
logOps,
|
||||||
relOps,
|
placeholderKeywords,
|
||||||
|
getRelOps,
|
||||||
buildQueryFilterString,
|
buildQueryFilterString,
|
||||||
getFieldFromFieldDef,
|
getFieldFromFieldDef,
|
||||||
isOrganizerType,
|
isOrganizerType,
|
||||||
|
|||||||
117
frontend/composables/use-search.ts
Normal file
117
frontend/composables/use-search.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { watchDebounced } from "@vueuse/core";
|
||||||
|
import type { IFuseOptions } from "fuse.js";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
|
export interface IAlias {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchableItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
aliases?: IAlias[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISearchItemInternal extends ISearchableItem {
|
||||||
|
aliasesText?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchOptions {
|
||||||
|
debounceMs?: number;
|
||||||
|
maxWaitMs?: number;
|
||||||
|
minSearchLength?: number;
|
||||||
|
fuseOptions?: Partial<IFuseOptions<ISearchItemInternal>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearch<T extends ISearchableItem>(
|
||||||
|
items: ComputedRef<T[]> | Ref<T[]> | T[],
|
||||||
|
options: ISearchOptions = {},
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
debounceMs = 0,
|
||||||
|
maxWaitMs = 1500,
|
||||||
|
minSearchLength = 1,
|
||||||
|
fuseOptions: customFuseOptions = {},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// State
|
||||||
|
const search = ref("");
|
||||||
|
const debouncedSearch = shallowRef("");
|
||||||
|
|
||||||
|
// Flatten item aliases to include as searchable text
|
||||||
|
const searchItems = computed(() => {
|
||||||
|
const itemsArray = Array.isArray(items) ? items : items.value;
|
||||||
|
return itemsArray.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
aliasesText: item.aliases ? item.aliases.map(a => a.name).join(" ") : "",
|
||||||
|
} as ISearchItemInternal;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default Fuse options
|
||||||
|
const defaultFuseOptions: IFuseOptions<ISearchItemInternal> = {
|
||||||
|
keys: [
|
||||||
|
{ name: "name", weight: 3 },
|
||||||
|
{ name: "pluralName", weight: 3 },
|
||||||
|
{ name: "abbreviation", weight: 2 },
|
||||||
|
{ name: "pluralAbbreviation", weight: 2 },
|
||||||
|
{ name: "aliasesText", weight: 1 },
|
||||||
|
],
|
||||||
|
ignoreLocation: true,
|
||||||
|
shouldSort: true,
|
||||||
|
threshold: 0.3,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
findAllMatches: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge custom options with defaults
|
||||||
|
const fuseOptions = computed(() => ({
|
||||||
|
...defaultFuseOptions,
|
||||||
|
...customFuseOptions,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Debounce search input
|
||||||
|
watchDebounced(
|
||||||
|
() => search.value,
|
||||||
|
(newSearch) => {
|
||||||
|
debouncedSearch.value = newSearch;
|
||||||
|
},
|
||||||
|
{ debounce: debounceMs, maxWait: maxWaitMs, immediate: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize Fuse instance
|
||||||
|
const fuse = computed(() => {
|
||||||
|
return new Fuse(searchItems.value || [], fuseOptions.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compute filtered results
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const itemsArray = Array.isArray(items) ? items : items.value;
|
||||||
|
const searchTerm = debouncedSearch.value.trim();
|
||||||
|
|
||||||
|
// If no search query or less than minSearchLength characters, return all items
|
||||||
|
if (!searchTerm || searchTerm.length < minSearchLength) {
|
||||||
|
return itemsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemsArray || itemsArray.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = fuse.value.search(searchTerm);
|
||||||
|
return results.map(result => result.item as T);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
search.value = "";
|
||||||
|
debouncedSearch.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
search,
|
||||||
|
debouncedSearch,
|
||||||
|
filtered,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fieldTypes } from "../forms";
|
import { fieldTypes } from "../forms";
|
||||||
|
import { validators } from "../use-validators";
|
||||||
import type { AutoFormItems } from "~/types/auto-forms";
|
import type { AutoFormItems } from "~/types/auto-forms";
|
||||||
|
|
||||||
export const useCommonSettingsForm = () => {
|
export const useCommonSettingsForm = () => {
|
||||||
@@ -11,7 +12,7 @@ export const useCommonSettingsForm = () => {
|
|||||||
hint: i18n.t("group.enable-public-access-description"),
|
hint: i18n.t("group.enable-public-access-description"),
|
||||||
varName: "makeGroupRecipesPublic",
|
varName: "makeGroupRecipesPublic",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: i18n.t("data-pages.data-management"),
|
section: i18n.t("data-pages.data-management"),
|
||||||
@@ -19,7 +20,7 @@ export const useCommonSettingsForm = () => {
|
|||||||
hint: i18n.t("user-registration.use-seed-data-description"),
|
hint: i18n.t("user-registration.use-seed-data-description"),
|
||||||
varName: "useSeedData",
|
varName: "useSeedData",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
||||||
import { ActivityKey } from "~/lib/api/types/activity";
|
import { ActivityKey } from "~/lib/api/types/activity";
|
||||||
import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
|
import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
|
||||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
import type { QueryFilterJSON } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
export interface UserPrintPreferences {
|
export interface UserPrintPreferences {
|
||||||
imagePosition: string;
|
imagePosition: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fieldTypes } from "../forms";
|
import { fieldTypes } from "../forms";
|
||||||
|
import { validators } from "../use-validators";
|
||||||
import type { AutoFormItems } from "~/types/auto-forms";
|
import type { AutoFormItems } from "~/types/auto-forms";
|
||||||
|
|
||||||
export const useUserForm = () => {
|
export const useUserForm = () => {
|
||||||
@@ -10,26 +11,26 @@ export const useUserForm = () => {
|
|||||||
label: i18n.t("user.user-name"),
|
label: i18n.t("user.user-name"),
|
||||||
varName: "username",
|
varName: "username",
|
||||||
type: fieldTypes.TEXT,
|
type: fieldTypes.TEXT,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.full-name"),
|
label: i18n.t("user.full-name"),
|
||||||
varName: "fullName",
|
varName: "fullName",
|
||||||
type: fieldTypes.TEXT,
|
type: fieldTypes.TEXT,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.email"),
|
label: i18n.t("user.email"),
|
||||||
varName: "email",
|
varName: "email",
|
||||||
type: fieldTypes.TEXT,
|
type: fieldTypes.TEXT,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.password"),
|
label: i18n.t("user.password"),
|
||||||
varName: "password",
|
varName: "password",
|
||||||
disableUpdate: true,
|
disableUpdate: true,
|
||||||
type: fieldTypes.PASSWORD,
|
type: fieldTypes.PASSWORD,
|
||||||
rules: ["required", "minLength:8"],
|
rules: [validators.required, validators.minLength(8)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.authentication-method"),
|
label: i18n.t("user.authentication-method"),
|
||||||
@@ -44,37 +45,37 @@ export const useUserForm = () => {
|
|||||||
label: i18n.t("user.administrator"),
|
label: i18n.t("user.administrator"),
|
||||||
varName: "admin",
|
varName: "admin",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.user-can-invite-other-to-group"),
|
label: i18n.t("user.user-can-invite-other-to-group"),
|
||||||
varName: "canInvite",
|
varName: "canInvite",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.user-can-manage-group"),
|
label: i18n.t("user.user-can-manage-group"),
|
||||||
varName: "canManage",
|
varName: "canManage",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.user-can-organize-group-data"),
|
label: i18n.t("user.user-can-organize-group-data"),
|
||||||
varName: "canOrganize",
|
varName: "canOrganize",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.user-can-manage-household"),
|
label: i18n.t("user.user-can-manage-household"),
|
||||||
varName: "canManageHousehold",
|
varName: "canManageHousehold",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.t("user.enable-advanced-features"),
|
label: i18n.t("user.enable-advanced-features"),
|
||||||
varName: "advanced",
|
varName: "advanced",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: [validators.required],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ const loading = ref(false);
|
|||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
|
||||||
export const useUserSelfRatings = function () {
|
export const useUserSelfRatings = function () {
|
||||||
const $auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
|
|
||||||
async function refreshUserRatings() {
|
async function refreshUserRatings() {
|
||||||
if (!$auth.user.value || loading.value) {
|
if (!auth.user.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export const useUserSelfRatings = function () {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
const userId = $auth.user.value?.id || "";
|
const userId = auth.user.value?.id || "";
|
||||||
await api.users.setRating(userId, slug, rating, isFavorite);
|
await api.users.setRating(userId, slug, rating, isFavorite);
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const normalizeLigatures = replaceAllBuilder(new Map([
|
|||||||
["st", "st"],
|
["st", "st"],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated prefer fuse.js/use-search.ts
|
||||||
|
*/
|
||||||
export const normalize = (str: string) => {
|
export const normalize = (str: string) => {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return "";
|
return "";
|
||||||
@@ -45,6 +48,9 @@ export const normalize = (str: string) => {
|
|||||||
return normalized;
|
return normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated prefer fuse.js/use-search.ts
|
||||||
|
*/
|
||||||
export const normalizeFilter: FilterFunction = (value: string, query: string) => {
|
export const normalizeFilter: FilterFunction = (value: string, query: string) => {
|
||||||
const normalizedValue = normalize(value);
|
const normalizedValue = normalize(value);
|
||||||
const normalizeQuery = normalize(query);
|
const normalizeQuery = normalize(query);
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export const validators = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useAsyncValidator us a factory function that returns an async function that
|
* useAsyncValidator us a factory function that returns an async function that
|
||||||
* when called will validate the input against the backend database and set the
|
* when called will validate the input against the backend database and set the
|
||||||
* error messages when applicable to the ref.
|
* error messages when applicable to the ref.
|
||||||
*/
|
*/
|
||||||
export const useAsyncValidator = (
|
export const useAsyncValidator = (
|
||||||
value: Ref<string>,
|
value: Ref<string>,
|
||||||
validatorFunc: (v: string) => Promise<RequestResponse<ValidationResponse>>,
|
validatorFunc: (v: string) => Promise<RequestResponse<ValidationResponse>>,
|
||||||
|
|||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Resepreëls",
|
"recipe-rules": "Resepreëls",
|
||||||
"applies-to-all-days": "Van toepassing op alle dae",
|
"applies-to-all-days": "Van toepassing op alle dae",
|
||||||
"applies-on-days": "Van toepassing op {0}s",
|
"applies-on-days": "Van toepassing op {0}s",
|
||||||
"meal-plan-settings": "Maaltydplan verstellings"
|
"meal-plan-settings": "Maaltydplan verstellings",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Migrasiedata is uitgevee",
|
"migration-data-removed": "Migrasiedata is uitgevee",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "is greater than",
|
"is-greater-than": "is greater than",
|
||||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||||
"is-less-than": "is less than",
|
"is-less-than": "is less than",
|
||||||
"is-less-than-or-equal-to": "is less than or equal to"
|
"is-less-than-or-equal-to": "is less than or equal to",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "is",
|
"is": "is",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contains all of",
|
"contains-all-of": "contains all of",
|
||||||
"is-like": "is like",
|
"is-like": "is like",
|
||||||
"is-not-like": "is not like"
|
"is-not-like": "is not like"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "قواعد الوصفات",
|
"recipe-rules": "قواعد الوصفات",
|
||||||
"applies-to-all-days": "ينطبق على جميع الأيام",
|
"applies-to-all-days": "ينطبق على جميع الأيام",
|
||||||
"applies-on-days": "يطبق على أيام {0}",
|
"applies-on-days": "يطبق على أيام {0}",
|
||||||
"meal-plan-settings": "إعدادات خِطَّة الوجبات الغذائية"
|
"meal-plan-settings": "إعدادات خِطَّة الوجبات الغذائية",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "حذف بيانات الهجرة",
|
"migration-data-removed": "حذف بيانات الهجرة",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "أكبر من",
|
"is-greater-than": "أكبر من",
|
||||||
"is-greater-than-or-equal-to": "أكبر من أو يساوي",
|
"is-greater-than-or-equal-to": "أكبر من أو يساوي",
|
||||||
"is-less-than": "أقل من",
|
"is-less-than": "أقل من",
|
||||||
"is-less-than-or-equal-to": "أقل من أو يساوي"
|
"is-less-than-or-equal-to": "أقل من أو يساوي",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "هو",
|
"is": "هو",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "يحتوي على كل من",
|
"contains-all-of": "يحتوي على كل من",
|
||||||
"is-like": "هو مثل",
|
"is-like": "هو مثل",
|
||||||
"is-not-like": "ليس مثل"
|
"is-not-like": "ليس مثل"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "يجب أن يكون عنوان URL صالحًا",
|
"invalid-url": "يجب أن يكون عنوان URL صالحًا",
|
||||||
"no-whitespace": "لا يسمح باستخدام المسافات",
|
"no-whitespace": "لا يسمح باستخدام المسافات",
|
||||||
"min-length": "يجب أن يكون على الأقل {min} أحرف",
|
"min-length": "يجب أن يكون على الأقل {min} أحرف",
|
||||||
"max-length": "يجب أن يكون على الأكثر {max} أحرف"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Правила на рецептата",
|
"recipe-rules": "Правила на рецептата",
|
||||||
"applies-to-all-days": "Прилага се за всички дни",
|
"applies-to-all-days": "Прилага се за всички дни",
|
||||||
"applies-on-days": "Всеки/всяка {0}",
|
"applies-on-days": "Всеки/всяка {0}",
|
||||||
"meal-plan-settings": "Настройки на плана за хранене"
|
"meal-plan-settings": "Настройки на плана за хранене",
|
||||||
|
"add-all-to-list": "Добавяне на всички към списъка за пазаруване",
|
||||||
|
"add-day-to-list": "Добавяне на ден към списъка за пазаруване"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Данните за мигриране са премахнати",
|
"migration-data-removed": "Данните за мигриране са премахнати",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Блокиран ли е уебсайтът?",
|
"scrape-recipe-website-being-blocked": "Блокиран ли е уебсайтът?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Опитайте вместо това да импортирате суровия HTML код.",
|
"scrape-recipe-try-importing-raw-html-instead": "Опитайте вместо това да импортирате суровия HTML код.",
|
||||||
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
|
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Импортиране на оригиналните категории",
|
||||||
"stay-in-edit-mode": "Остани в режим на редакция",
|
"stay-in-edit-mode": "Остани в режим на редакция",
|
||||||
"parse-recipe-ingredients-after-import": "Анализиране на съставките на рецептата след импортиране",
|
"parse-recipe-ingredients-after-import": "Анализиране на съставките на рецептата след импортиране",
|
||||||
"import-from-zip": "Импортирай от Zip",
|
"import-from-zip": "Импортирай от Zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "е по-голямо от",
|
"is-greater-than": "е по-голямо от",
|
||||||
"is-greater-than-or-equal-to": "е по-голямо от или равно на",
|
"is-greater-than-or-equal-to": "е по-голямо от или равно на",
|
||||||
"is-less-than": "е по-малко от",
|
"is-less-than": "е по-малко от",
|
||||||
"is-less-than-or-equal-to": "e по-малко или равно на"
|
"is-less-than-or-equal-to": "e по-малко или равно на",
|
||||||
|
"is-older-than": "е по-стар от",
|
||||||
|
"is-newer-than": "е по-нов от"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "е",
|
"is": "е",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "съдържа всички от",
|
"contains-all-of": "съдържа всички от",
|
||||||
"is-like": "е като",
|
"is-like": "е като",
|
||||||
"is-not-like": "не е като"
|
"is-not-like": "не е като"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "преди дни|преди ден|преди дни"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Линкът трябва да е валиден",
|
"invalid-url": "Линкът трябва да е валиден",
|
||||||
"no-whitespace": "Не са позволени интервали",
|
"no-whitespace": "Не са позволени интервали",
|
||||||
"min-length": "Трябва да съдържа поне {min} знака",
|
"min-length": "Трябва да съдържа поне {min} знака",
|
||||||
"max-length": "Трябва да съдържа най-много {max} знака"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Normes per la recepta",
|
"recipe-rules": "Normes per la recepta",
|
||||||
"applies-to-all-days": "Aplica a tots els dies",
|
"applies-to-all-days": "Aplica a tots els dies",
|
||||||
"applies-on-days": "S'aplicarà en {0}s",
|
"applies-on-days": "S'aplicarà en {0}s",
|
||||||
"meal-plan-settings": "Opcions de planificació de menús"
|
"meal-plan-settings": "Opcions de planificació de menús",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "S'han suprimit les dades migrades",
|
"migration-data-removed": "S'han suprimit les dades migrades",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "és més gran que",
|
"is-greater-than": "és més gran que",
|
||||||
"is-greater-than-or-equal-to": "és més gran o igual a",
|
"is-greater-than-or-equal-to": "és més gran o igual a",
|
||||||
"is-less-than": "és menys que",
|
"is-less-than": "és menys que",
|
||||||
"is-less-than-or-equal-to": "és menor o igual a"
|
"is-less-than-or-equal-to": "és menor o igual a",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "és",
|
"is": "és",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "conté tots de",
|
"contains-all-of": "conté tots de",
|
||||||
"is-like": "és com",
|
"is-like": "és com",
|
||||||
"is-not-like": "no és com"
|
"is-not-like": "no és com"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "La URL ha de ser vàlida",
|
"invalid-url": "La URL ha de ser vàlida",
|
||||||
"no-whitespace": "No es permeten espais en blanc",
|
"no-whitespace": "No es permeten espais en blanc",
|
||||||
"min-length": "Ha de tenir almenys {min} caràcters",
|
"min-length": "Ha de tenir almenys {min} caràcters",
|
||||||
"max-length": "Ha de tenir com a màxim {max} caràcters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Nahrát soubor",
|
"upload-file": "Nahrát soubor",
|
||||||
"created-on-date": "Vytvořeno dne: {0}",
|
"created-on-date": "Vytvořeno dne: {0}",
|
||||||
"unsaved-changes": "Máte neuložené změny. Chcete je uložit před odchodem? Klikněte Okay pro uložení, Cancel pro smazání změn.",
|
"unsaved-changes": "Máte neuložené změny. Chcete je uložit před odchodem? Klikněte Okay pro uložení, Cancel pro smazání změn.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Zahodit změny",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Máte neuložené změny. Určitě je chcete zahodit?",
|
||||||
"clipboard-copy-failure": "Zkopírování do schránky se nezdařilo.",
|
"clipboard-copy-failure": "Zkopírování do schránky se nezdařilo.",
|
||||||
"confirm-delete-generic-items": "Opravdu chcete smazat následující položky?",
|
"confirm-delete-generic-items": "Opravdu chcete smazat následující položky?",
|
||||||
"organizers": "Organizace",
|
"organizers": "Organizace",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Pravidla receptu",
|
"recipe-rules": "Pravidla receptu",
|
||||||
"applies-to-all-days": "Použije se na všechny dny",
|
"applies-to-all-days": "Použije se na všechny dny",
|
||||||
"applies-on-days": "Platí pro {0}",
|
"applies-on-days": "Platí pro {0}",
|
||||||
"meal-plan-settings": "Nastavení jídelníčku"
|
"meal-plan-settings": "Nastavení jídelníčku",
|
||||||
|
"add-all-to-list": "Přidat vše do seznamu",
|
||||||
|
"add-day-to-list": "Přidat den do seznamu"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Data z migrace byla smazána",
|
"migration-data-removed": "Data z migrace byla smazána",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Webové stránky jsou blokovány?",
|
"scrape-recipe-website-being-blocked": "Webové stránky jsou blokovány?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Zkuste namísto toho importovat raw HTML.",
|
"scrape-recipe-try-importing-raw-html-instead": "Zkuste namísto toho importovat raw HTML.",
|
||||||
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
|
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importovat původní kategorie",
|
||||||
"stay-in-edit-mode": "Zůstat v režimu úprav",
|
"stay-in-edit-mode": "Zůstat v režimu úprav",
|
||||||
"parse-recipe-ingredients-after-import": "Po importu analyzovat ingredience receptu",
|
"parse-recipe-ingredients-after-import": "Po importu analyzovat ingredience receptu",
|
||||||
"import-from-zip": "Importovat ze zipu",
|
"import-from-zip": "Importovat ze zipu",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "je větší než",
|
"is-greater-than": "je větší než",
|
||||||
"is-greater-than-or-equal-to": "je větší než nebo rovno",
|
"is-greater-than-or-equal-to": "je větší než nebo rovno",
|
||||||
"is-less-than": "je menší než",
|
"is-less-than": "je menší než",
|
||||||
"is-less-than-or-equal-to": "je menší než nebo rovno"
|
"is-less-than-or-equal-to": "je menší než nebo rovno",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "je",
|
"is": "je",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "obsahuje všechny z",
|
"contains-all-of": "obsahuje všechny z",
|
||||||
"is-like": "je jako",
|
"is-like": "je jako",
|
||||||
"is-not-like": "není jako"
|
"is-not-like": "není jako"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Musí být platná URL adresa",
|
"invalid-url": "Musí být platná URL adresa",
|
||||||
"no-whitespace": "Mezery nejsou povoleny",
|
"no-whitespace": "Mezery nejsou povoleny",
|
||||||
"min-length": "Musí být alespoň {min} znaků",
|
"min-length": "Musí být alespoň {min} znaků",
|
||||||
"max-length": "Musí být nejvíce {max} znaků"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Upload fil",
|
"upload-file": "Upload fil",
|
||||||
"created-on-date": "Oprettet den: {0}",
|
"created-on-date": "Oprettet den: {0}",
|
||||||
"unsaved-changes": "Du har ændringer som ikke er gemt. Vil du gemme før du forlader? Vælg \"Okay\" for at gemme, eller \"Annullér\" for at kassere ændringer.",
|
"unsaved-changes": "Du har ændringer som ikke er gemt. Vil du gemme før du forlader? Vælg \"Okay\" for at gemme, eller \"Annullér\" for at kassere ændringer.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Kassér ændringer",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Du har ændringer, der ikke er gemt. Er du sikker på, at du vil kassere dem?",
|
||||||
"clipboard-copy-failure": "Kopiering til udklipsholderen mislykkedes.",
|
"clipboard-copy-failure": "Kopiering til udklipsholderen mislykkedes.",
|
||||||
"confirm-delete-generic-items": "Er du sikker på at du ønsker at slette de valgte emner?",
|
"confirm-delete-generic-items": "Er du sikker på at du ønsker at slette de valgte emner?",
|
||||||
"organizers": "Organisatorer",
|
"organizers": "Organisatorer",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Opskriftsregler",
|
"recipe-rules": "Opskriftsregler",
|
||||||
"applies-to-all-days": "Gælder for alle dage",
|
"applies-to-all-days": "Gælder for alle dage",
|
||||||
"applies-on-days": "Gælder for {0}e",
|
"applies-on-days": "Gælder for {0}e",
|
||||||
"meal-plan-settings": "Indstillinger for madplanlægning"
|
"meal-plan-settings": "Indstillinger for madplanlægning",
|
||||||
|
"add-all-to-list": "Tilføj alle til liste",
|
||||||
|
"add-day-to-list": "Tilføj dag til liste"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Migreringsdata fjernet",
|
"migration-data-removed": "Migreringsdata fjernet",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Bliver hjemmesiden blokeret?",
|
"scrape-recipe-website-being-blocked": "Bliver hjemmesiden blokeret?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Forsøg at importere den rå HTML i stedet.",
|
"scrape-recipe-try-importing-raw-html-instead": "Forsøg at importere den rå HTML i stedet.",
|
||||||
"import-original-keywords-as-tags": "Importér originale nøgleord som mærker",
|
"import-original-keywords-as-tags": "Importér originale nøgleord som mærker",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importér originale kategorier",
|
||||||
"stay-in-edit-mode": "Bliv i redigeringstilstand",
|
"stay-in-edit-mode": "Bliv i redigeringstilstand",
|
||||||
"parse-recipe-ingredients-after-import": "Fortolk opskrift ingredienser efter import",
|
"parse-recipe-ingredients-after-import": "Fortolk opskrift ingredienser efter import",
|
||||||
"import-from-zip": "Importer fra zip-fil",
|
"import-from-zip": "Importer fra zip-fil",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "er større end",
|
"is-greater-than": "er større end",
|
||||||
"is-greater-than-or-equal-to": "er større end eller lig med (Automatic Translation)",
|
"is-greater-than-or-equal-to": "er større end eller lig med (Automatic Translation)",
|
||||||
"is-less-than": "er mindre end (Automatic Translation)",
|
"is-less-than": "er mindre end (Automatic Translation)",
|
||||||
"is-less-than-or-equal-to": "er mindre end eller lig med (Automatic Translation)"
|
"is-less-than-or-equal-to": "er mindre end eller lig med (Automatic Translation)",
|
||||||
|
"is-older-than": "er ældre end",
|
||||||
|
"is-newer-than": "er nyere end"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "er",
|
"is": "er",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "indeholder alle af",
|
"contains-all-of": "indeholder alle af",
|
||||||
"is-like": "er ligesom",
|
"is-like": "er ligesom",
|
||||||
"is-not-like": "er ikke som"
|
"is-not-like": "er ikke som"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "dage siden|dag siden|dage siden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "URL'en skal være gyldig",
|
"invalid-url": "URL'en skal være gyldig",
|
||||||
"no-whitespace": "Mellemrum er ikke tilladt",
|
"no-whitespace": "Mellemrum er ikke tilladt",
|
||||||
"min-length": "Der skal mindst være {min} tegn",
|
"min-length": "Der skal mindst være {min} tegn",
|
||||||
"max-length": "Der må højst være {max} tegn"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Datei hochladen",
|
"upload-file": "Datei hochladen",
|
||||||
"created-on-date": "Erstellt am: {0}",
|
"created-on-date": "Erstellt am: {0}",
|
||||||
"unsaved-changes": "Du hast ungespeicherte Änderungen. Möchtest du vor dem Verlassen speichern? OK um zu speichern, Cancel um Änderungen zu verwerfen.",
|
"unsaved-changes": "Du hast ungespeicherte Änderungen. Möchtest du vor dem Verlassen speichern? OK um zu speichern, Cancel um Änderungen zu verwerfen.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Änderungen verwerfen",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Du hast ungespeicherte Änderungen. Bist du sicher, dass du sie verwerfen möchtest?",
|
||||||
"clipboard-copy-failure": "Fehler beim Kopieren in die Zwischenablage.",
|
"clipboard-copy-failure": "Fehler beim Kopieren in die Zwischenablage.",
|
||||||
"confirm-delete-generic-items": "Bist du dir sicher, dass du die folgenden Einträge löschen möchtest?",
|
"confirm-delete-generic-items": "Bist du dir sicher, dass du die folgenden Einträge löschen möchtest?",
|
||||||
"organizers": "Organisieren",
|
"organizers": "Organisieren",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Rezeptregeln",
|
"recipe-rules": "Rezeptregeln",
|
||||||
"applies-to-all-days": "Gilt an allen Tagen",
|
"applies-to-all-days": "Gilt an allen Tagen",
|
||||||
"applies-on-days": "Gilt {0}s",
|
"applies-on-days": "Gilt {0}s",
|
||||||
"meal-plan-settings": "Essensplan Einstellungen"
|
"meal-plan-settings": "Essensplan Einstellungen",
|
||||||
|
"add-all-to-list": "Alle zur Einkaufsliste hinzufügen",
|
||||||
|
"add-day-to-list": "Tag zur Einkaufsliste hinzufügen"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Migrationsdaten entfernt",
|
"migration-data-removed": "Migrationsdaten entfernt",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Die Website wird blockiert?",
|
"scrape-recipe-website-being-blocked": "Die Website wird blockiert?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Versuche stattdessen das reine HTML zu importieren.",
|
"scrape-recipe-try-importing-raw-html-instead": "Versuche stattdessen das reine HTML zu importieren.",
|
||||||
"import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter",
|
"import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importiere ursprüngliche Kategorien",
|
||||||
"stay-in-edit-mode": "Im Bearbeitungsmodus bleiben",
|
"stay-in-edit-mode": "Im Bearbeitungsmodus bleiben",
|
||||||
"parse-recipe-ingredients-after-import": "Zutaten nach dem Import parsen",
|
"parse-recipe-ingredients-after-import": "Zutaten nach dem Import parsen",
|
||||||
"import-from-zip": "Von Zip importieren",
|
"import-from-zip": "Von Zip importieren",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "ist größer als",
|
"is-greater-than": "ist größer als",
|
||||||
"is-greater-than-or-equal-to": "ist größer gleich",
|
"is-greater-than-or-equal-to": "ist größer gleich",
|
||||||
"is-less-than": "ist weniger als",
|
"is-less-than": "ist weniger als",
|
||||||
"is-less-than-or-equal-to": "ist kleiner gleich"
|
"is-less-than-or-equal-to": "ist kleiner gleich",
|
||||||
|
"is-older-than": "Ist älter als",
|
||||||
|
"is-newer-than": "Ist neuer als"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "ist",
|
"is": "ist",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "enthält alle",
|
"contains-all-of": "enthält alle",
|
||||||
"is-like": "ist wie",
|
"is-like": "ist wie",
|
||||||
"is-not-like": "ist nicht wie"
|
"is-not-like": "ist nicht wie"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "vor Tagen|vor Tag|vor Tagen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Muss eine gültige URL sein",
|
"invalid-url": "Muss eine gültige URL sein",
|
||||||
"no-whitespace": "Kein Leerzeichen erlaubt",
|
"no-whitespace": "Kein Leerzeichen erlaubt",
|
||||||
"min-length": "Muss mindestens {min} Zeichen haben",
|
"min-length": "Muss mindestens {min} Zeichen haben",
|
||||||
"max-length": "Darf mindestens {max} Zeichen haben"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Κανόνες Συνταγής",
|
"recipe-rules": "Κανόνες Συνταγής",
|
||||||
"applies-to-all-days": "Εφαρμόζεται για όλες τις ημέρες",
|
"applies-to-all-days": "Εφαρμόζεται για όλες τις ημέρες",
|
||||||
"applies-on-days": "Εφαρμόζεται κάθε {0}",
|
"applies-on-days": "Εφαρμόζεται κάθε {0}",
|
||||||
"meal-plan-settings": "Ρυθμίσεις προγράμματος γευμάτων"
|
"meal-plan-settings": "Ρυθμίσεις προγράμματος γευμάτων",
|
||||||
|
"add-all-to-list": "Προσθήκη όλων στη λίστα",
|
||||||
|
"add-day-to-list": "Προσθήκη ημέρας στη λίστα"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Τα δεδομένα μετεγκατάστασης καταργήθηκαν",
|
"migration-data-removed": "Τα δεδομένα μετεγκατάστασης καταργήθηκαν",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Η ιστοσελίδα μπλοκάρεται;",
|
"scrape-recipe-website-being-blocked": "Η ιστοσελίδα μπλοκάρεται;",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Δοκιμάστε να εισάγετε τον ακατέργαστο κώδικα HTML.",
|
"scrape-recipe-try-importing-raw-html-instead": "Δοκιμάστε να εισάγετε τον ακατέργαστο κώδικα HTML.",
|
||||||
"import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες",
|
"import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Εισαγωγή αρχικών κατηγοριών",
|
||||||
"stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας",
|
"stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας",
|
||||||
"parse-recipe-ingredients-after-import": "Ανάλυση συστατικών συνταγής μετά την εισαγωγή",
|
"parse-recipe-ingredients-after-import": "Ανάλυση συστατικών συνταγής μετά την εισαγωγή",
|
||||||
"import-from-zip": "Εισαγωγή μέσω zip",
|
"import-from-zip": "Εισαγωγή μέσω zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "είναι μεγαλύτερο από",
|
"is-greater-than": "είναι μεγαλύτερο από",
|
||||||
"is-greater-than-or-equal-to": "είναι μεγαλύτερο από ή ίσο με",
|
"is-greater-than-or-equal-to": "είναι μεγαλύτερο από ή ίσο με",
|
||||||
"is-less-than": "είναι μικρότερο από",
|
"is-less-than": "είναι μικρότερο από",
|
||||||
"is-less-than-or-equal-to": "είναι μικρότερο από ή ίσο με"
|
"is-less-than-or-equal-to": "είναι μικρότερο από ή ίσο με",
|
||||||
|
"is-older-than": "είναι παλαιότερο από",
|
||||||
|
"is-newer-than": "είναι νεότερο από"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "είναι",
|
"is": "είναι",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "περιέχει όλα τα",
|
"contains-all-of": "περιέχει όλα τα",
|
||||||
"is-like": "είναι όμοιο με",
|
"is-like": "είναι όμοιο με",
|
||||||
"is-not-like": "δεν είναι όμοιο με"
|
"is-not-like": "δεν είναι όμοιο με"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "ημέρες πριν|ημέρα πριν|ημέρες πριν"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Πρέπει να είναι μια έγκυρη διεύθυνση URL",
|
"invalid-url": "Πρέπει να είναι μια έγκυρη διεύθυνση URL",
|
||||||
"no-whitespace": "Δεν επιτρέπονται κενοί χαρακτήρες",
|
"no-whitespace": "Δεν επιτρέπονται κενοί χαρακτήρες",
|
||||||
"min-length": "Πρέπει να αποτελείται από τουλάχιστον {min} χαρακτήρες",
|
"min-length": "Πρέπει να αποτελείται από τουλάχιστον {min} χαρακτήρες",
|
||||||
"max-length": "Πρέπει να αποτελείται το πολύ από {max} χαρακτήρες"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Recipe Rules",
|
"recipe-rules": "Recipe Rules",
|
||||||
"applies-to-all-days": "Applies to all days",
|
"applies-to-all-days": "Applies to all days",
|
||||||
"applies-on-days": "Applies on {0}s",
|
"applies-on-days": "Applies on {0}s",
|
||||||
"meal-plan-settings": "Meal Plan Settings"
|
"meal-plan-settings": "Meal Plan Settings",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Migration data removed",
|
"migration-data-removed": "Migration data removed",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "is greater than",
|
"is-greater-than": "is greater than",
|
||||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||||
"is-less-than": "is less than",
|
"is-less-than": "is less than",
|
||||||
"is-less-than-or-equal-to": "is less than or equal to"
|
"is-less-than-or-equal-to": "is less than or equal to",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "is",
|
"is": "is",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contains all of",
|
"contains-all-of": "contains all of",
|
||||||
"is-like": "is like",
|
"is-like": "is like",
|
||||||
"is-not-like": "is not like"
|
"is-not-like": "is not like"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Recipe Rules",
|
"recipe-rules": "Recipe Rules",
|
||||||
"applies-to-all-days": "Applies to all days",
|
"applies-to-all-days": "Applies to all days",
|
||||||
"applies-on-days": "Applies on {0}s",
|
"applies-on-days": "Applies on {0}s",
|
||||||
"meal-plan-settings": "Meal Plan Settings"
|
"meal-plan-settings": "Meal Plan Settings",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Migration data removed",
|
"migration-data-removed": "Migration data removed",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "is greater than",
|
"is-greater-than": "is greater than",
|
||||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||||
"is-less-than": "is less than",
|
"is-less-than": "is less than",
|
||||||
"is-less-than-or-equal-to": "is less than or equal to"
|
"is-less-than-or-equal-to": "is less than or equal to",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "is",
|
"is": "is",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contains all of",
|
"contains-all-of": "contains all of",
|
||||||
"is-like": "is like",
|
"is-like": "is like",
|
||||||
"is-not-like": "is not like"
|
"is-not-like": "is not like"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Subir Archivo",
|
"upload-file": "Subir Archivo",
|
||||||
"created-on-date": "Creado el {0}",
|
"created-on-date": "Creado el {0}",
|
||||||
"unsaved-changes": "Tienes cambios sin guardar. ¿Quieres guardar antes de salir? Aceptar para guardar, Cancelar para descartar cambios.",
|
"unsaved-changes": "Tienes cambios sin guardar. ¿Quieres guardar antes de salir? Aceptar para guardar, Cancelar para descartar cambios.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Descartar Cambios",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Tiene cambios sin guardar. ¿Está seguro que desea descartarlos?",
|
||||||
"clipboard-copy-failure": "No se pudo copiar al portapapeles.",
|
"clipboard-copy-failure": "No se pudo copiar al portapapeles.",
|
||||||
"confirm-delete-generic-items": "¿Estás seguro que quieres eliminar los siguientes elementos?",
|
"confirm-delete-generic-items": "¿Estás seguro que quieres eliminar los siguientes elementos?",
|
||||||
"organizers": "Organizadores",
|
"organizers": "Organizadores",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Reglas de Recetas",
|
"recipe-rules": "Reglas de Recetas",
|
||||||
"applies-to-all-days": "Aplica para todos los días",
|
"applies-to-all-days": "Aplica para todos los días",
|
||||||
"applies-on-days": "Se aplica en {0}s",
|
"applies-on-days": "Se aplica en {0}s",
|
||||||
"meal-plan-settings": "Configuración del Plan de Comidas"
|
"meal-plan-settings": "Configuración del Plan de Comidas",
|
||||||
|
"add-all-to-list": "Añadir todos a la lista",
|
||||||
|
"add-day-to-list": "Añadir Día a la Lista"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Datos de migración eliminados",
|
"migration-data-removed": "Datos de migración eliminados",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "¿Sitio web bloqueado?",
|
"scrape-recipe-website-being-blocked": "¿Sitio web bloqueado?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Intenta importar el HTML en bruto.",
|
"scrape-recipe-try-importing-raw-html-instead": "Intenta importar el HTML en bruto.",
|
||||||
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
|
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importar categorías originales",
|
||||||
"stay-in-edit-mode": "Permanecer en modo edición",
|
"stay-in-edit-mode": "Permanecer en modo edición",
|
||||||
"parse-recipe-ingredients-after-import": "Analizar los ingredientes de la receta después de importarla",
|
"parse-recipe-ingredients-after-import": "Analizar los ingredientes de la receta después de importarla",
|
||||||
"import-from-zip": "Importar desde zip",
|
"import-from-zip": "Importar desde zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "es mayor que",
|
"is-greater-than": "es mayor que",
|
||||||
"is-greater-than-or-equal-to": "es mayor que o igual a",
|
"is-greater-than-or-equal-to": "es mayor que o igual a",
|
||||||
"is-less-than": "es menor que",
|
"is-less-than": "es menor que",
|
||||||
"is-less-than-or-equal-to": "es menor que o igual a"
|
"is-less-than-or-equal-to": "es menor que o igual a",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "es",
|
"is": "es",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contiene todo de",
|
"contains-all-of": "contiene todo de",
|
||||||
"is-like": "es como",
|
"is-like": "es como",
|
||||||
"is-not-like": "no es como"
|
"is-not-like": "no es como"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Debe ser una URL válida",
|
"invalid-url": "Debe ser una URL válida",
|
||||||
"no-whitespace": "No se permiten espacios en blanco",
|
"no-whitespace": "No se permiten espacios en blanco",
|
||||||
"min-length": "Debe ser como mínimo {min} caracteres",
|
"min-length": "Debe ser como mínimo {min} caracteres",
|
||||||
"max-length": "Debe ser como máximo {max} caracteres"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
"upload-file": "Lae fail üles",
|
"upload-file": "Lae fail üles",
|
||||||
"created-on-date": "Loodud: {0}",
|
"created-on-date": "Loodud: {0}",
|
||||||
"unsaved-changes": "Sul on salvestamata muudatusi. Kas sa tahad salvestada enne lehelt lahkumist? Vajuta OK salvestamiseks või Tühista, et muudatused tühistada.",
|
"unsaved-changes": "Sul on salvestamata muudatusi. Kas sa tahad salvestada enne lehelt lahkumist? Vajuta OK salvestamiseks või Tühista, et muudatused tühistada.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Loobu muudatustest",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||||
"clipboard-copy-failure": "Lõikepuhvrisse kopeerimine ebaõnnestus.",
|
"clipboard-copy-failure": "Lõikepuhvrisse kopeerimine ebaõnnestus.",
|
||||||
"confirm-delete-generic-items": "Kas oled kindel, et tahad kustutada järgnevad asjad?",
|
"confirm-delete-generic-items": "Kas oled kindel, et tahad kustutada järgnevad asjad?",
|
||||||
@@ -344,9 +344,9 @@
|
|||||||
"breakfast": "Hommikusöök",
|
"breakfast": "Hommikusöök",
|
||||||
"lunch": "Lõuna",
|
"lunch": "Lõuna",
|
||||||
"dinner": "Õhtusöök",
|
"dinner": "Õhtusöök",
|
||||||
"snack": "Snack",
|
"snack": "Snäkk",
|
||||||
"drink": "Drink",
|
"drink": "Jook",
|
||||||
"dessert": "Dessert",
|
"dessert": "Magustoit",
|
||||||
"type-any": "Kõik",
|
"type-any": "Kõik",
|
||||||
"day-any": "Kõik",
|
"day-any": "Kõik",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Retsepti reeglid",
|
"recipe-rules": "Retsepti reeglid",
|
||||||
"applies-to-all-days": "Kehtib kõikide päevade kohta",
|
"applies-to-all-days": "Kehtib kõikide päevade kohta",
|
||||||
"applies-on-days": "Kehtib {0}l",
|
"applies-on-days": "Kehtib {0}l",
|
||||||
"meal-plan-settings": "Toitumisplaani sätted"
|
"meal-plan-settings": "Toitumisplaani sätted",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Ületoomiste andmed eemaldatud",
|
"migration-data-removed": "Ületoomiste andmed eemaldatud",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "on suurem kui",
|
"is-greater-than": "on suurem kui",
|
||||||
"is-greater-than-or-equal-to": "on suurem või võrdne kui",
|
"is-greater-than-or-equal-to": "on suurem või võrdne kui",
|
||||||
"is-less-than": "on vähem kui",
|
"is-less-than": "on vähem kui",
|
||||||
"is-less-than-or-equal-to": "on väiksem või võrdne kui"
|
"is-less-than-or-equal-to": "on väiksem või võrdne kui",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "on",
|
"is": "on",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "sisaldab kõiki",
|
"contains-all-of": "sisaldab kõiki",
|
||||||
"is-like": "on nagu",
|
"is-like": "on nagu",
|
||||||
"is-not-like": "ei ole nagu"
|
"is-not-like": "ei ole nagu"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,18 +6,18 @@
|
|||||||
"api-port": "API-portti",
|
"api-port": "API-portti",
|
||||||
"application-mode": "Sovellustila",
|
"application-mode": "Sovellustila",
|
||||||
"database-type": "Tietokannan tyyppi",
|
"database-type": "Tietokannan tyyppi",
|
||||||
"database-url": "Tietokannan URL",
|
"database-url": "Tietokannan URL-osoite",
|
||||||
"default-group": "Oletusryhmä",
|
"default-group": "Oletusryhmä",
|
||||||
"default-household": "Oletuskotitalous",
|
"default-household": "Oletuskotitalous",
|
||||||
"demo": "Demo",
|
"demo": "Esittelytila",
|
||||||
"demo-status": "Demon tila",
|
"demo-status": "Esittelytila",
|
||||||
"development": "Kehitys",
|
"development": "Kehitys",
|
||||||
"docs": "Dokumentit",
|
"docs": "Dokumentit",
|
||||||
"download-log": "Latausloki",
|
"download-log": "Latausloki",
|
||||||
"download-recipe-json": "Viimeisin haettu JSON",
|
"download-recipe-json": "Viimeisin haettu JSON",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"log-lines": "Lokirivit",
|
"log-lines": "Lokirivit",
|
||||||
"not-demo": "Ei demotilassa",
|
"not-demo": "Ei käytössä",
|
||||||
"portfolio": "Portfolio",
|
"portfolio": "Portfolio",
|
||||||
"production": "Tuotanto",
|
"production": "Tuotanto",
|
||||||
"support": "Tuki",
|
"support": "Tuki",
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
"print": "Tulosta",
|
"print": "Tulosta",
|
||||||
"print-preferences": "Tulostusasetukset",
|
"print-preferences": "Tulostusasetukset",
|
||||||
"random": "Satunnainen",
|
"random": "Satunnainen",
|
||||||
"rating": "Arvio",
|
"rating": "Arvosana",
|
||||||
"recent": "Viimeisimmät",
|
"recent": "Viimeisimmät",
|
||||||
"recipe": "Resepti",
|
"recipe": "Resepti",
|
||||||
"recipes": "Reseptit",
|
"recipes": "Reseptit",
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
"sort": "Järjestä",
|
"sort": "Järjestä",
|
||||||
"sort-ascending": "Järjestä nousevasti",
|
"sort-ascending": "Järjestä nousevasti",
|
||||||
"sort-descending": "Järjestä laskevasti",
|
"sort-descending": "Järjestä laskevasti",
|
||||||
"sort-alphabetically": "Aakkosjärjestyksessä",
|
"sort-alphabetically": "Aakkosjärjestys",
|
||||||
"status": "Tila",
|
"status": "Tila",
|
||||||
"subject": "Aihe",
|
"subject": "Aihe",
|
||||||
"submit": "Lähetä",
|
"submit": "Lähetä",
|
||||||
@@ -205,15 +205,15 @@
|
|||||||
"copied-to-clipboard": "Kopioitu leikepöydälle",
|
"copied-to-clipboard": "Kopioitu leikepöydälle",
|
||||||
"your-browser-does-not-support-clipboard": "Selaimesi ei tue leikepöytää",
|
"your-browser-does-not-support-clipboard": "Selaimesi ei tue leikepöytää",
|
||||||
"copied-items-to-clipboard": "Mitään ei kopioitu leikepöydälle|Kohde kopioitu leikepöydälle|{count} kohdetta kopioitu leikepöydälle",
|
"copied-items-to-clipboard": "Mitään ei kopioitu leikepöydälle|Kohde kopioitu leikepöydälle|{count} kohdetta kopioitu leikepöydälle",
|
||||||
"actions": "Toimet",
|
"actions": "Toiminnot",
|
||||||
"selected-count": "Valittu {count}",
|
"selected-count": "Valittu {count}",
|
||||||
"export-all": "Vie kaikki",
|
"export-all": "Vie kaikki",
|
||||||
"refresh": "Päivitä",
|
"refresh": "Päivitä",
|
||||||
"upload-file": "Tuo tiedosto",
|
"upload-file": "Tuo tiedosto",
|
||||||
"created-on-date": "Luotu {0}",
|
"created-on-date": "Luotu {0}",
|
||||||
"unsaved-changes": "Et ole tallentanut tekemiäsi muutoksia. Tallennetaanko ne? Paina \"ok\" tallentaaksesi ja \"peruuta\", jos et halua tallentaa.",
|
"unsaved-changes": "Tallenna muutokset? ”Ok” tallentaa, ”Peruuta” hylkää muutokset.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Hylkää muutokset",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Muutoksia ei ole tallennettu. Hylätäänkö muutokset?",
|
||||||
"clipboard-copy-failure": "Kopioiminen leikepöydälle epäonnistui.",
|
"clipboard-copy-failure": "Kopioiminen leikepöydälle epäonnistui.",
|
||||||
"confirm-delete-generic-items": "Haluatko varmasti poistaa seuraavat kohteet?",
|
"confirm-delete-generic-items": "Haluatko varmasti poistaa seuraavat kohteet?",
|
||||||
"organizers": "Järjestäjät",
|
"organizers": "Järjestäjät",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Reseptimääritykset",
|
"recipe-rules": "Reseptimääritykset",
|
||||||
"applies-to-all-days": "Sovelletaan kaikkiin päiviin",
|
"applies-to-all-days": "Sovelletaan kaikkiin päiviin",
|
||||||
"applies-on-days": "Käytetään {0}",
|
"applies-on-days": "Käytetään {0}",
|
||||||
"meal-plan-settings": "Ateriasuunnitelman asetukset"
|
"meal-plan-settings": "Ateriasuunnitelman asetukset",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Tuodut tiedot poistettu",
|
"migration-data-removed": "Tuodut tiedot poistettu",
|
||||||
@@ -710,8 +712,8 @@
|
|||||||
"toggle-recipe": "Vaihda osio"
|
"toggle-recipe": "Vaihda osio"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Reseptin etsijä",
|
"recipe-finder": "Reseptihaku",
|
||||||
"recipe-finder-description": "Etsi sopivia reseptejä saatavilla olevien ainesosien perusteella. Voit myös suodattaa tulokset saatavilla olevien ruoanvalmistusvälineiden perusteella, ja asettaa enimmäismäärän puuttuvia ainesosia tai välineitä.",
|
"recipe-finder-description": "Etsi sopivia reseptejä saatavilla olevien ainesosien perusteella. Voit myös suodattaa tulokset saatavilla olevien keittiövälineiden perusteella, ja asettaa enimmäismäärän puuttuvia ainesosia tai välineitä.",
|
||||||
"selected-ingredients": "Valitut ainesosat",
|
"selected-ingredients": "Valitut ainesosat",
|
||||||
"no-ingredients-selected": "Ei valittuja ainesosia",
|
"no-ingredients-selected": "Ei valittuja ainesosia",
|
||||||
"missing": "Puuttuu",
|
"missing": "Puuttuu",
|
||||||
@@ -721,7 +723,7 @@
|
|||||||
"include-tools-on-hand": "Sisällytä saatavilla olevat välineet",
|
"include-tools-on-hand": "Sisällytä saatavilla olevat välineet",
|
||||||
"max-missing-ingredients": "Puuttuvien ainesten enimmäismäärä",
|
"max-missing-ingredients": "Puuttuvien ainesten enimmäismäärä",
|
||||||
"max-missing-tools": "Puuttuvien välineiden enimmäismäärä",
|
"max-missing-tools": "Puuttuvien välineiden enimmäismäärä",
|
||||||
"selected-tools": "Valitut välineet",
|
"selected-tools": "Valitut keittiövälineet",
|
||||||
"other-filters": "Muut suodattimet",
|
"other-filters": "Muut suodattimet",
|
||||||
"ready-to-make": "Valmis tekemään",
|
"ready-to-make": "Valmis tekemään",
|
||||||
"almost-ready-to-make": "Melkein valmis tekemään"
|
"almost-ready-to-make": "Melkein valmis tekemään"
|
||||||
@@ -978,14 +980,14 @@
|
|||||||
"tag": "Tunniste"
|
"tag": "Tunniste"
|
||||||
},
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"tools": "Työkalut",
|
"tools": "Keittiövälineet",
|
||||||
"on-hand": "Minulla on tämä työkalu",
|
"on-hand": "Omistan välineen",
|
||||||
"create-a-tool": "Luo työkalu",
|
"create-a-tool": "Lisää keittiöväline",
|
||||||
"tool-name": "Työkalun Nimi",
|
"tool-name": "Keittiöväline",
|
||||||
"create-new-tool": "Luo Uusi Työkalu",
|
"create-new-tool": "Lisää keittiöväline",
|
||||||
"on-hand-checkbox-label": "Näytä työkalut, jotka omistan jo (valittu)",
|
"on-hand-checkbox-label": "Näytä keittiövälineeni (valittu)",
|
||||||
"required-tools": "Tarvittavat Työkalut",
|
"required-tools": "Tarvittavat keittiövälineet",
|
||||||
"tool": "Työkalu"
|
"tool": "Keittiöväline"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"admin": "Ylläpitäjä",
|
"admin": "Ylläpitäjä",
|
||||||
@@ -1189,9 +1191,9 @@
|
|||||||
"tag-data": "Tunnisteen tiedot"
|
"tag-data": "Tunnisteen tiedot"
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"new-tool": "Uusi työkalu",
|
"new-tool": "Lisää keittiöväline",
|
||||||
"edit-tool": "Muokkaa työkalua",
|
"edit-tool": "Muokkaa keittiövälinettä",
|
||||||
"tool-data": "Työkalun tiedot"
|
"tool-data": "Keittiövälineen tiedot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user-registration": {
|
"user-registration": {
|
||||||
@@ -1237,7 +1239,7 @@
|
|||||||
"preview-markdown-button-label": "Esikatsele Markdownia"
|
"preview-markdown-button-label": "Esikatsele Markdownia"
|
||||||
},
|
},
|
||||||
"demo": {
|
"demo": {
|
||||||
"info_message_with_version": "Tämä on demo: {version}",
|
"info_message_with_version": "Mealie on esittelytilassa. Mealien versio: {version}",
|
||||||
"demo_username": "Käyttäjätunnus: {username}",
|
"demo_username": "Käyttäjätunnus: {username}",
|
||||||
"demo_password": "Salasana: {password}"
|
"demo_password": "Salasana: {password}"
|
||||||
},
|
},
|
||||||
@@ -1402,7 +1404,7 @@
|
|||||||
"filter-options-description": "Kun vaaditaan kaikki on valittu, keittokirja sisältää vain reseptejä, joissa on kaikki valitut tuotteet. Tämä koskee jokaista valitsimien osajoukkoa, ei valittujen kohteiden poikkileikkausta.",
|
"filter-options-description": "Kun vaaditaan kaikki on valittu, keittokirja sisältää vain reseptejä, joissa on kaikki valitut tuotteet. Tämä koskee jokaista valitsimien osajoukkoa, ei valittujen kohteiden poikkileikkausta.",
|
||||||
"require-all-categories": "Vaadi Kaikki Kategoriat",
|
"require-all-categories": "Vaadi Kaikki Kategoriat",
|
||||||
"require-all-tags": "Vaadi Kaikki Tunnisteet",
|
"require-all-tags": "Vaadi Kaikki Tunnisteet",
|
||||||
"require-all-tools": "Vaadi Kaikki Työkalut",
|
"require-all-tools": "Kaikki keittiövälineet tulee löytyä",
|
||||||
"cookbook-name": "Keittokirjan Nimi",
|
"cookbook-name": "Keittokirjan Nimi",
|
||||||
"cookbook-with-name": "Keittokirja {0}",
|
"cookbook-with-name": "Keittokirja {0}",
|
||||||
"household-cookbook-name": "{0} Keittokirja {1}",
|
"household-cookbook-name": "{0} Keittokirja {1}",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "on suurempi kuin",
|
"is-greater-than": "on suurempi kuin",
|
||||||
"is-greater-than-or-equal-to": "on suurempi tai yhtäsuuri kuin",
|
"is-greater-than-or-equal-to": "on suurempi tai yhtäsuuri kuin",
|
||||||
"is-less-than": "on vähemmän kuin",
|
"is-less-than": "on vähemmän kuin",
|
||||||
"is-less-than-or-equal-to": "on vähemmän tai yhtäsuuri kuin"
|
"is-less-than-or-equal-to": "on vähemmän tai yhtäsuuri kuin",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "on",
|
"is": "on",
|
||||||
@@ -1430,14 +1434,17 @@
|
|||||||
"contains-all-of": "sisältää kaikki nämä",
|
"contains-all-of": "sisältää kaikki nämä",
|
||||||
"is-like": "on kuin",
|
"is-like": "on kuin",
|
||||||
"is-not-like": "ei ole kuin"
|
"is-not-like": "ei ole kuin"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
"required": "Tämä kenttä on pakollinen",
|
"required": "Tämä kenttä on pakollinen",
|
||||||
"invalid-email": "Sähköpostiosoite ei ole kelvollinen",
|
"invalid-email": "Sähköpostiosoite ei ole kelvollinen",
|
||||||
"invalid-url": "URL ei ole kelvollinen",
|
"invalid-url": "URL ei ole kelvollinen",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "Tekstissä ei saa olla välilyöntejä",
|
||||||
"min-length": "Vähimmäispituus on {min} merkkiä",
|
"min-length": "Vähimmäispituus on {min} merkkiä",
|
||||||
"max-length": "Enimmäispituus on {max} merkkiä"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Transférer un fichier",
|
"upload-file": "Transférer un fichier",
|
||||||
"created-on-date": "Créé le {0}",
|
"created-on-date": "Créé le {0}",
|
||||||
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir ? OK pour enregistrer, Annuler pour ignorer les modifications.",
|
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir ? OK pour enregistrer, Annuler pour ignorer les modifications.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Annuler les modifications",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Vous avez des changements non sauvegardés. Êtes-vous sûr de vouloir les annuler ?",
|
||||||
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
|
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
|
||||||
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
||||||
"organizers": "Classification",
|
"organizers": "Classification",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Règles de recette",
|
"recipe-rules": "Règles de recette",
|
||||||
"applies-to-all-days": "S'applique à tous les jours",
|
"applies-to-all-days": "S'applique à tous les jours",
|
||||||
"applies-on-days": "S'applique les {0}s",
|
"applies-on-days": "S'applique les {0}s",
|
||||||
"meal-plan-settings": "Paramètres des menus"
|
"meal-plan-settings": "Paramètres des menus",
|
||||||
|
"add-all-to-list": "Tout ajouter a une liste",
|
||||||
|
"add-day-to-list": "Ajouter un jour à la liste"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Données de migration supprimées",
|
"migration-data-removed": "Données de migration supprimées",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importer les catégories originales",
|
||||||
"stay-in-edit-mode": "Rester en mode édition",
|
"stay-in-edit-mode": "Rester en mode édition",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||||
"import-from-zip": "Importer depuis un zip",
|
"import-from-zip": "Importer depuis un zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "est supérieur à",
|
"is-greater-than": "est supérieur à",
|
||||||
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
||||||
"is-less-than": "est inférieur à",
|
"is-less-than": "est inférieur à",
|
||||||
"is-less-than-or-equal-to": "est inférieur ou égal à"
|
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||||
|
"is-older-than": "est plus ancien que",
|
||||||
|
"is-newer-than": "est plus récent que"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "est",
|
"is": "est",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contient tout",
|
"contains-all-of": "contient tout",
|
||||||
"is-like": "est comme",
|
"is-like": "est comme",
|
||||||
"is-not-like": "n'est pas similaire à"
|
"is-not-like": "n'est pas similaire à"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "jours|jour|jours"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Doit être une URL valide",
|
"invalid-url": "Doit être une URL valide",
|
||||||
"no-whitespace": "Aucun espace n'est autorisé",
|
"no-whitespace": "Aucun espace n'est autorisé",
|
||||||
"min-length": "Doit contenir au moins {min} caractères",
|
"min-length": "Doit contenir au moins {min} caractères",
|
||||||
"max-length": "Doit contenir au maximum {max} caractères"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Téléverser un fichier",
|
"upload-file": "Téléverser un fichier",
|
||||||
"created-on-date": "Créé le {0}",
|
"created-on-date": "Créé le {0}",
|
||||||
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous les enregistrer ? Ok pour enregistrer, annuler pour ignorer les modifications.",
|
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous les enregistrer ? Ok pour enregistrer, annuler pour ignorer les modifications.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Annuler les modifications",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Vous avez des changements non sauvegardés. Êtes-vous sûr de vouloir les annuler ?",
|
||||||
"clipboard-copy-failure": "Échec de la copie vers le presse-papiers.",
|
"clipboard-copy-failure": "Échec de la copie vers le presse-papiers.",
|
||||||
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
||||||
"organizers": "Classification",
|
"organizers": "Classification",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Règles de recette",
|
"recipe-rules": "Règles de recette",
|
||||||
"applies-to-all-days": "S'applique à tous les jours",
|
"applies-to-all-days": "S'applique à tous les jours",
|
||||||
"applies-on-days": "S'applique les {0}s",
|
"applies-on-days": "S'applique les {0}s",
|
||||||
"meal-plan-settings": "Paramètres des menus"
|
"meal-plan-settings": "Paramètres des menus",
|
||||||
|
"add-all-to-list": "Ajouter tout à la liste",
|
||||||
|
"add-day-to-list": "Ajouter un jour à la liste"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Données de migration supprimées",
|
"migration-data-removed": "Données de migration supprimées",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importer les catégories originales",
|
||||||
"stay-in-edit-mode": "Rester en mode édition",
|
"stay-in-edit-mode": "Rester en mode édition",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||||
"import-from-zip": "Importer depuis un zip",
|
"import-from-zip": "Importer depuis un zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "est supérieur à",
|
"is-greater-than": "est supérieur à",
|
||||||
"is-greater-than-or-equal-to": "est supérieur ou égal à",
|
"is-greater-than-or-equal-to": "est supérieur ou égal à",
|
||||||
"is-less-than": "est inférieure à",
|
"is-less-than": "est inférieure à",
|
||||||
"is-less-than-or-equal-to": "est inférieur ou égal à"
|
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||||
|
"is-older-than": "est plus ancien que",
|
||||||
|
"is-newer-than": "est plus récent que"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "est",
|
"is": "est",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contient tous les",
|
"contains-all-of": "contient tous les",
|
||||||
"is-like": "est similaire à",
|
"is-like": "est similaire à",
|
||||||
"is-not-like": "n'est pas similaire à"
|
"is-not-like": "n'est pas similaire à"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "jours|jour|jours"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Doit être une URL valide",
|
"invalid-url": "Doit être une URL valide",
|
||||||
"no-whitespace": "Aucun espace n'est autorisé",
|
"no-whitespace": "Aucun espace n'est autorisé",
|
||||||
"min-length": "",
|
"min-length": "",
|
||||||
"max-length": "Doit contenir au maximum {max} caractères"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Téléverser un fichier",
|
"upload-file": "Téléverser un fichier",
|
||||||
"created-on-date": "Créé le {0}",
|
"created-on-date": "Créé le {0}",
|
||||||
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir ? OK pour enregistrer, Annuler pour ignorer les modifications.",
|
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir ? OK pour enregistrer, Annuler pour ignorer les modifications.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Annuler les modifications",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Vous avez des changements non sauvegardés. Êtes-vous sûr de vouloir les annuler ?",
|
||||||
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
|
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
|
||||||
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
||||||
"organizers": "Classification",
|
"organizers": "Classification",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Règles de recette",
|
"recipe-rules": "Règles de recette",
|
||||||
"applies-to-all-days": "S'applique à tous les jours",
|
"applies-to-all-days": "S'applique à tous les jours",
|
||||||
"applies-on-days": "S'applique les {0}s",
|
"applies-on-days": "S'applique les {0}s",
|
||||||
"meal-plan-settings": "Paramètres des menus"
|
"meal-plan-settings": "Paramètres des menus",
|
||||||
|
"add-all-to-list": "Ajouter tout à la liste",
|
||||||
|
"add-day-to-list": "Ajouter un jour à la liste"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Données de migration supprimées",
|
"migration-data-removed": "Données de migration supprimées",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Importer les catégories originales",
|
||||||
"stay-in-edit-mode": "Rester en mode édition",
|
"stay-in-edit-mode": "Rester en mode édition",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||||
"import-from-zip": "Importer depuis un zip",
|
"import-from-zip": "Importer depuis un zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "est supérieur à",
|
"is-greater-than": "est supérieur à",
|
||||||
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
||||||
"is-less-than": "est inférieur à",
|
"is-less-than": "est inférieur à",
|
||||||
"is-less-than-or-equal-to": "est inférieur ou égal à"
|
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||||
|
"is-older-than": "est plus ancien que",
|
||||||
|
"is-newer-than": "est plus récent que"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "est",
|
"is": "est",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contient tout",
|
"contains-all-of": "contient tout",
|
||||||
"is-like": "est comme",
|
"is-like": "est comme",
|
||||||
"is-not-like": "n'est pas similaire à"
|
"is-not-like": "n'est pas similaire à"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "jours|jour|jours"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Doit être une URL valide",
|
"invalid-url": "Doit être une URL valide",
|
||||||
"no-whitespace": "Aucun espace n'est autorisé",
|
"no-whitespace": "Aucun espace n'est autorisé",
|
||||||
"min-length": "Doit contenir au moins {min} caractères",
|
"min-length": "Doit contenir au moins {min} caractères",
|
||||||
"max-length": "Doit contenir au maximum {max} caractères"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Regras da Receita",
|
"recipe-rules": "Regras da Receita",
|
||||||
"applies-to-all-days": "Aplícase a todos os días",
|
"applies-to-all-days": "Aplícase a todos os días",
|
||||||
"applies-on-days": "Aplícase en {0}s",
|
"applies-on-days": "Aplícase en {0}s",
|
||||||
"meal-plan-settings": "Axustes do Menú"
|
"meal-plan-settings": "Axustes do Menú",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Elimináronse os datos de migración",
|
"migration-data-removed": "Elimináronse os datos de migración",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "é maior que",
|
"is-greater-than": "é maior que",
|
||||||
"is-greater-than-or-equal-to": "é maior ou igual a",
|
"is-greater-than-or-equal-to": "é maior ou igual a",
|
||||||
"is-less-than": "é menor que",
|
"is-less-than": "é menor que",
|
||||||
"is-less-than-or-equal-to": "é menor ou igual a"
|
"is-less-than-or-equal-to": "é menor ou igual a",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "é",
|
"is": "é",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contén todos os",
|
"contains-all-of": "contén todos os",
|
||||||
"is-like": "é como",
|
"is-like": "é como",
|
||||||
"is-not-like": "non é como"
|
"is-not-like": "non é como"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "חוקי מתכון",
|
"recipe-rules": "חוקי מתכון",
|
||||||
"applies-to-all-days": "החל על כל הימים",
|
"applies-to-all-days": "החל על כל הימים",
|
||||||
"applies-on-days": "חל על {0}",
|
"applies-on-days": "חל על {0}",
|
||||||
"meal-plan-settings": "הגדרות תכנון ארוחות"
|
"meal-plan-settings": "הגדרות תכנון ארוחות",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "מידע ממוגרץ נמחק",
|
"migration-data-removed": "מידע ממוגרץ נמחק",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "גדול מ-",
|
"is-greater-than": "גדול מ-",
|
||||||
"is-greater-than-or-equal-to": "גדול או שווה ל-",
|
"is-greater-than-or-equal-to": "גדול או שווה ל-",
|
||||||
"is-less-than": "קטן מ-",
|
"is-less-than": "קטן מ-",
|
||||||
"is-less-than-or-equal-to": "קטן או שווה ל-"
|
"is-less-than-or-equal-to": "קטן או שווה ל-",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "זהה ל-",
|
"is": "זהה ל-",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "מכיל הכל מתוך",
|
"contains-all-of": "מכיל הכל מתוך",
|
||||||
"is-like": "דומה ל-",
|
"is-like": "דומה ל-",
|
||||||
"is-not-like": "לא דומה לא-"
|
"is-not-like": "לא דומה לא-"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Pravila Recepata",
|
"recipe-rules": "Pravila Recepata",
|
||||||
"applies-to-all-days": "Primjeni na sve dane",
|
"applies-to-all-days": "Primjeni na sve dane",
|
||||||
"applies-on-days": "Primjeni na {0}",
|
"applies-on-days": "Primjeni na {0}",
|
||||||
"meal-plan-settings": "Postavke Plana Obroka"
|
"meal-plan-settings": "Postavke Plana Obroka",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Podaci o migraciji su uklonjeni",
|
"migration-data-removed": "Podaci o migraciji su uklonjeni",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "is greater than",
|
"is-greater-than": "is greater than",
|
||||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||||
"is-less-than": "is less than",
|
"is-less-than": "is less than",
|
||||||
"is-less-than-or-equal-to": "is less than or equal to"
|
"is-less-than-or-equal-to": "is less than or equal to",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "is",
|
"is": "is",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contains all of",
|
"contains-all-of": "contains all of",
|
||||||
"is-like": "is like",
|
"is-like": "is like",
|
||||||
"is-not-like": "is not like"
|
"is-not-like": "is not like"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Recept szabályok",
|
"recipe-rules": "Recept szabályok",
|
||||||
"applies-to-all-days": "Minden napra vonatkozóan",
|
"applies-to-all-days": "Minden napra vonatkozóan",
|
||||||
"applies-on-days": "Érvényes {0}-ként",
|
"applies-on-days": "Érvényes {0}-ként",
|
||||||
"meal-plan-settings": "Menütervező beállításai"
|
"meal-plan-settings": "Menütervező beállításai",
|
||||||
|
"add-all-to-list": "Összes hozzáadása a listához",
|
||||||
|
"add-day-to-list": "Nap hozzáadása a listához"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Migrációs adatok eltávolítva",
|
"migration-data-removed": "Migrációs adatok eltávolítva",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "nagyobb, mint",
|
"is-greater-than": "nagyobb, mint",
|
||||||
"is-greater-than-or-equal-to": "nagyobb vagy egyenlő",
|
"is-greater-than-or-equal-to": "nagyobb vagy egyenlő",
|
||||||
"is-less-than": "kevesebb, mint",
|
"is-less-than": "kevesebb, mint",
|
||||||
"is-less-than-or-equal-to": "kevesebb vagy egyenlő"
|
"is-less-than-or-equal-to": "kevesebb vagy egyenlő",
|
||||||
|
"is-older-than": "régebbi, mint",
|
||||||
|
"is-newer-than": "újabb, mint"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "van",
|
"is": "van",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "tartalmazza az összes",
|
"contains-all-of": "tartalmazza az összes",
|
||||||
"is-like": "hasonló",
|
"is-like": "hasonló",
|
||||||
"is-not-like": "nem hasonló"
|
"is-not-like": "nem hasonló"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "napja|napja|napja"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Érvényes URL-nek kell lennie",
|
"invalid-url": "Érvényes URL-nek kell lennie",
|
||||||
"no-whitespace": "Szóközt nem tartalmazhat",
|
"no-whitespace": "Szóközt nem tartalmazhat",
|
||||||
"min-length": "Legalább {min} karakter legyen",
|
"min-length": "Legalább {min} karakter legyen",
|
||||||
"max-length": "Legfeljebb {max} karakter legyen"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
"upload-file": "Hlaða upp skrá",
|
"upload-file": "Hlaða upp skrá",
|
||||||
"created-on-date": "Búið til: {0}",
|
"created-on-date": "Búið til: {0}",
|
||||||
"unsaved-changes": "Þú hefur ekki vistað breytingar. Viltu vista áður en þú ferð? Ýttu á \"Í lagi\" til að vista, \"Hætta við\" til að henda breytingum.",
|
"unsaved-changes": "Þú hefur ekki vistað breytingar. Viltu vista áður en þú ferð? Ýttu á \"Í lagi\" til að vista, \"Hætta við\" til að henda breytingum.",
|
||||||
"discard-changes": "Discard Changes",
|
"discard-changes": "Henda breytingum",
|
||||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
"discard-changes-description": "Þú ert með óvistaðar breytingar, ertu viss um að þú viljir henda þeim?",
|
||||||
"clipboard-copy-failure": "Mistókst að afrita klippispjaldið.",
|
"clipboard-copy-failure": "Mistókst að afrita klippispjaldið.",
|
||||||
"confirm-delete-generic-items": "Ertu viss um að þú viljir eyða eftirfylgjandi atriðum?",
|
"confirm-delete-generic-items": "Ertu viss um að þú viljir eyða eftirfylgjandi atriðum?",
|
||||||
"organizers": "Skipuleggjarar",
|
"organizers": "Skipuleggjarar",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Uppskriftar reglur",
|
"recipe-rules": "Uppskriftar reglur",
|
||||||
"applies-to-all-days": "Á við alla daga",
|
"applies-to-all-days": "Á við alla daga",
|
||||||
"applies-on-days": "Gildir þegar er {0},",
|
"applies-on-days": "Gildir þegar er {0},",
|
||||||
"meal-plan-settings": "Stillingar matarplans"
|
"meal-plan-settings": "Stillingar matarplans",
|
||||||
|
"add-all-to-list": "Bæta öllum á innkaupalista",
|
||||||
|
"add-day-to-list": "Bæta deginum á innkaupalista"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Gagnaflutningur fjarlægður",
|
"migration-data-removed": "Gagnaflutningur fjarlægður",
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
"scrape-recipe-website-being-blocked": "Er vefsíðan lokuð?",
|
"scrape-recipe-website-being-blocked": "Er vefsíðan lokuð?",
|
||||||
"scrape-recipe-try-importing-raw-html-instead": "Reyndu að flytja inn HTML kóðann í staðinn.",
|
"scrape-recipe-try-importing-raw-html-instead": "Reyndu að flytja inn HTML kóðann í staðinn.",
|
||||||
"import-original-keywords-as-tags": "Nota upprunanleg merki",
|
"import-original-keywords-as-tags": "Nota upprunanleg merki",
|
||||||
"import-original-categories": "Import original categories",
|
"import-original-categories": "Flytja inn upprunalega flokka",
|
||||||
"stay-in-edit-mode": "Vera í breytingarham",
|
"stay-in-edit-mode": "Vera í breytingarham",
|
||||||
"parse-recipe-ingredients-after-import": "Greina innhald uppskriftar eftir að búið er að hlaða inn uppskrift",
|
"parse-recipe-ingredients-after-import": "Greina innhald uppskriftar eftir að búið er að hlaða inn uppskrift",
|
||||||
"import-from-zip": "Hlaða inn frá .zip",
|
"import-from-zip": "Hlaða inn frá .zip",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "eftir þann",
|
"is-greater-than": "eftir þann",
|
||||||
"is-greater-than-or-equal-to": "þann eða eftir þann",
|
"is-greater-than-or-equal-to": "þann eða eftir þann",
|
||||||
"is-less-than": "fyrir þann",
|
"is-less-than": "fyrir þann",
|
||||||
"is-less-than-or-equal-to": "fyrir þann eða þann"
|
"is-less-than-or-equal-to": "fyrir þann eða þann",
|
||||||
|
"is-older-than": "er eldra en",
|
||||||
|
"is-newer-than": "er nýrra en"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "er",
|
"is": "er",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "inniheldur alla af",
|
"contains-all-of": "inniheldur alla af",
|
||||||
"is-like": "is like",
|
"is-like": "is like",
|
||||||
"is-not-like": "is not like"
|
"is-not-like": "is not like"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "daga gamalt|dags gamalt|daga gamalt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Verður að vera gild vefslóð",
|
"invalid-url": "Verður að vera gild vefslóð",
|
||||||
"no-whitespace": "Engin bil leyfð",
|
"no-whitespace": "Engin bil leyfð",
|
||||||
"min-length": "Verður að vera að lágmarki {min} stafir",
|
"min-length": "Verður að vera að lágmarki {min} stafir",
|
||||||
"max-length": "Má vera að hámarki {max} stafir"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"about": "Dettagli",
|
"about": "Dettagli",
|
||||||
"about-mealie": "Info su Mealie",
|
"about-mealie": "Info su Mealie",
|
||||||
"api-docs": "Documentazione API",
|
"api-docs": "Documentazione API",
|
||||||
"api-port": "Porta API",
|
"api-port": "Importazione API",
|
||||||
"application-mode": "Modalità",
|
"application-mode": "Modalità",
|
||||||
"database-type": "Tipo Database",
|
"database-type": "Tipo Database",
|
||||||
"database-url": "URL Database",
|
"database-url": "URL Database",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
"custom": "Personalizzato",
|
"custom": "Personalizzato",
|
||||||
"dashboard": "Pannello di controllo",
|
"dashboard": "Pannello di controllo",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
"disabled": "Disabilitato",
|
"disabled": "Disabilita",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"duplicate": "Duplicato",
|
"duplicate": "Duplicato",
|
||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "Regole per le ricette",
|
"recipe-rules": "Regole per le ricette",
|
||||||
"applies-to-all-days": "Si applica a ogni giorno",
|
"applies-to-all-days": "Si applica a ogni giorno",
|
||||||
"applies-on-days": "Si applica ai {0}",
|
"applies-on-days": "Si applica ai {0}",
|
||||||
"meal-plan-settings": "Impostazioni del piano alimentare"
|
"meal-plan-settings": "Impostazioni del piano alimentare",
|
||||||
|
"add-all-to-list": "Aggiungi tutto alla lista",
|
||||||
|
"add-day-to-list": "Aggiungi Giorno"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "Dati di migrazione rimossi",
|
"migration-data-removed": "Dati di migrazione rimossi",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "è maggiore di",
|
"is-greater-than": "è maggiore di",
|
||||||
"is-greater-than-or-equal-to": "è maggiore o uguale di",
|
"is-greater-than-or-equal-to": "è maggiore o uguale di",
|
||||||
"is-less-than": "è minore di",
|
"is-less-than": "è minore di",
|
||||||
"is-less-than-or-equal-to": "è minore o uguale di"
|
"is-less-than-or-equal-to": "è minore o uguale di",
|
||||||
|
"is-older-than": "è più vecchio di",
|
||||||
|
"is-newer-than": "è più recente di"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "è",
|
"is": "è",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "contiene tutti i",
|
"contains-all-of": "contiene tutti i",
|
||||||
"is-like": "è simile",
|
"is-like": "è simile",
|
||||||
"is-not-like": "non è come"
|
"is-not-like": "non è come"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "giorni fa|giorno fa|giorni fa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Lo URL Deve Essere Valido",
|
"invalid-url": "Lo URL Deve Essere Valido",
|
||||||
"no-whitespace": "Gli Spazi Non Sono Ammessi",
|
"no-whitespace": "Gli Spazi Non Sono Ammessi",
|
||||||
"min-length": "Deve Essere Almeno {min} Caratteri",
|
"min-length": "Deve Essere Almeno {min} Caratteri",
|
||||||
"max-length": "Deve Essere Al Massimo {max} Caratteri"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,9 @@
|
|||||||
"recipe-rules": "レシピのルール",
|
"recipe-rules": "レシピのルール",
|
||||||
"applies-to-all-days": "すべての日に適用",
|
"applies-to-all-days": "すべての日に適用",
|
||||||
"applies-on-days": "{0}曜日に適用",
|
"applies-on-days": "{0}曜日に適用",
|
||||||
"meal-plan-settings": "献立設定"
|
"meal-plan-settings": "献立設定",
|
||||||
|
"add-all-to-list": "Add All to List",
|
||||||
|
"add-day-to-list": "Add Day to List"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"migration-data-removed": "移行データが削除されました",
|
"migration-data-removed": "移行データが削除されました",
|
||||||
@@ -1420,7 +1422,9 @@
|
|||||||
"is-greater-than": "より大きい",
|
"is-greater-than": "より大きい",
|
||||||
"is-greater-than-or-equal-to": "以上",
|
"is-greater-than-or-equal-to": "以上",
|
||||||
"is-less-than": "より小さい",
|
"is-less-than": "より小さい",
|
||||||
"is-less-than-or-equal-to": "以下"
|
"is-less-than-or-equal-to": "以下",
|
||||||
|
"is-older-than": "is older than",
|
||||||
|
"is-newer-than": "is newer than"
|
||||||
},
|
},
|
||||||
"relational-keywords": {
|
"relational-keywords": {
|
||||||
"is": "は",
|
"is": "は",
|
||||||
@@ -1430,6 +1434,9 @@
|
|||||||
"contains-all-of": "すべてを含む",
|
"contains-all-of": "すべてを含む",
|
||||||
"is-like": "次のようなものです",
|
"is-like": "次のようなものです",
|
||||||
"is-not-like": "というわけではありません"
|
"is-not-like": "というわけではありません"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"days-ago": "days ago|day ago|days ago"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validators": {
|
"validators": {
|
||||||
@@ -1438,6 +1445,6 @@
|
|||||||
"invalid-url": "Must Be A Valid URL",
|
"invalid-url": "Must Be A Valid URL",
|
||||||
"no-whitespace": "No Whitespace Allowed",
|
"no-whitespace": "No Whitespace Allowed",
|
||||||
"min-length": "Must Be At Least {min} Characters",
|
"min-length": "Must Be At Least {min} Characters",
|
||||||
"max-length": "Must Be At Most {max} Characters"
|
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user