mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-13 20:40:18 -04:00
Compare commits
385 Commits
v1.0.0beta
...
v1.0.0-RC1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b153ddf858 | ||
|
|
f35bc77a7d | ||
|
|
db021023fb | ||
|
|
954a2f5113 | ||
|
|
45022e1f1b | ||
|
|
bd79c1db2f | ||
|
|
b86c4e5865 | ||
|
|
a98e863bca | ||
|
|
247a4de283 | ||
|
|
26ef351ae7 | ||
|
|
84b477edf6 | ||
|
|
a331042e6d | ||
|
|
40ab328bb9 | ||
|
|
b32e2f1bf7 | ||
|
|
1074cad5dc | ||
|
|
4bd7bda60d | ||
|
|
484c60c7ea | ||
|
|
ef805c4b57 | ||
|
|
222d583d6f | ||
|
|
b7a6bff5aa | ||
|
|
bb9afd86c1 | ||
|
|
a22d500603 | ||
|
|
3e5596f898 | ||
|
|
15c6df88ab | ||
|
|
9a04b11ee5 | ||
|
|
2dfbe9f08d | ||
|
|
084ad4228b | ||
|
|
86b0b6761d | ||
|
|
2ad6af2cce | ||
|
|
aec4cb4f31 | ||
|
|
2c5e5a8421 | ||
|
|
e28b830cd4 | ||
|
|
71f1607b58 | ||
|
|
0a00a6ea0d | ||
|
|
408ca88cb2 | ||
|
|
30717e50d3 | ||
|
|
693608d59f | ||
|
|
5c5432304f | ||
|
|
5213a61d9b | ||
|
|
ae502c5652 | ||
|
|
c9cc7a93c8 | ||
|
|
99e7717fec | ||
|
|
1e693fdca6 | ||
|
|
d6e4829e6f | ||
|
|
50a92c165c | ||
|
|
2151451634 | ||
|
|
e24e28ae03 | ||
|
|
c60c63852b | ||
|
|
095edef95e | ||
|
|
99372aa2b6 | ||
|
|
76ae0bafc7 | ||
|
|
1aa7344688 | ||
|
|
da17f2a410 | ||
|
|
4169c3e743 | ||
|
|
3ccae9e366 | ||
|
|
26ab73a827 | ||
|
|
d8a6f900b4 | ||
|
|
f6e7eba6fc | ||
|
|
e55258c5be | ||
|
|
dd947c316a | ||
|
|
04c7e068d8 | ||
|
|
bdf6b1054e | ||
|
|
dfe4942451 | ||
|
|
06962cf865 | ||
|
|
3fb60339a6 | ||
|
|
89a6b32a3f | ||
|
|
6901747a26 | ||
|
|
e71155a55d | ||
|
|
b1eb156145 | ||
|
|
e434029bac | ||
|
|
a75116bc62 | ||
|
|
c86406e027 | ||
|
|
0f896107f9 | ||
|
|
c25b58e404 | ||
|
|
c783d86a2a | ||
|
|
e64265615e | ||
|
|
9f1f87bcd0 | ||
|
|
a7d8fc8d25 | ||
|
|
8584ca666f | ||
|
|
50a06f3553 | ||
|
|
9f9bdec886 | ||
|
|
e3652a0876 | ||
|
|
4af9eec89d | ||
|
|
d1b06e7339 | ||
|
|
9557b3f0b6 | ||
|
|
5e904d19b4 | ||
|
|
eea38a5b1e | ||
|
|
674774d073 | ||
|
|
e1d3a247c7 | ||
|
|
7e0d29afc7 | ||
|
|
27ebb4c462 | ||
|
|
8e2d50054c | ||
|
|
5d87b7e411 | ||
|
|
9b726126ed | ||
|
|
c6346ed2a5 | ||
|
|
20a78677ef | ||
|
|
23786c1f5e | ||
|
|
f27bb00dc3 | ||
|
|
2e43b51882 | ||
|
|
a2e69b5565 | ||
|
|
75698c531a | ||
|
|
fe17922bb8 | ||
|
|
0e397b34fd | ||
|
|
fa59c1eb0b | ||
|
|
998029da6f | ||
|
|
d117b4075b | ||
|
|
e8de70c894 | ||
|
|
16b47a3c10 | ||
|
|
05a7047ae3 | ||
|
|
de4debe749 | ||
|
|
fcdb11d8a1 | ||
|
|
6c625e402c | ||
|
|
750072344d | ||
|
|
d735f1c0d8 | ||
|
|
29956142e1 | ||
|
|
0ff46cda92 | ||
|
|
4e251b3092 | ||
|
|
10730bfa77 | ||
|
|
03b7d9e200 | ||
|
|
8a683e7344 | ||
|
|
f4dbbcc439 | ||
|
|
4126681f8c | ||
|
|
13aa57a3d2 | ||
|
|
73476c10f8 | ||
|
|
8a0ee351c5 | ||
|
|
5d61b3a822 | ||
|
|
721a1cdeae | ||
|
|
f7310bc7c3 | ||
|
|
b0b06200f8 | ||
|
|
027fcbdcef | ||
|
|
ab04d1f595 | ||
|
|
d56604d19b | ||
|
|
8a0552c902 | ||
|
|
4c0e27b849 | ||
|
|
04c7928963 | ||
|
|
4b426ddf2f | ||
|
|
fae62ecb19 | ||
|
|
82ce4b5e7a | ||
|
|
905b2ad8a9 | ||
|
|
7880735ee4 | ||
|
|
73e584b59e | ||
|
|
71ce2be17c | ||
|
|
667a231f4e | ||
|
|
4a08ae6517 | ||
|
|
6e8628de25 | ||
|
|
50f50b2b9a | ||
|
|
329d2c020d | ||
|
|
f206583150 | ||
|
|
b52848595c | ||
|
|
b460ae9f25 | ||
|
|
9199ae4901 | ||
|
|
a025996b94 | ||
|
|
d2134b27ba | ||
|
|
f9acba34cc | ||
|
|
97d9d10b1f | ||
|
|
37505d896c | ||
|
|
9fd1ba6e46 | ||
|
|
6b63c751b1 | ||
|
|
8d79773bf6 | ||
|
|
9650ba9b00 | ||
|
|
b06517fdf4 | ||
|
|
42d078ef02 | ||
|
|
ccb0b43cef | ||
|
|
3118b0e423 | ||
|
|
3ce8fa9492 | ||
|
|
7d9be67432 | ||
|
|
93eb2af087 | ||
|
|
1b26ca0cb3 | ||
|
|
11cd277975 | ||
|
|
5b487464ea | ||
|
|
85bc3a9b15 | ||
|
|
01e3bb04b9 | ||
|
|
d3f893dec2 | ||
|
|
ac0432d47a | ||
|
|
8b181ec28a | ||
|
|
a28133ea5d | ||
|
|
8824aec066 | ||
|
|
84d55eb920 | ||
|
|
da791ec9c5 | ||
|
|
30d8e0b16d | ||
|
|
541cdc79aa | ||
|
|
afbee3a078 | ||
|
|
796dcd9995 | ||
|
|
8ca0fe42de | ||
|
|
2e6ad5da8e | ||
|
|
39012adcc1 | ||
|
|
6418a10428 | ||
|
|
6a1503d1f6 | ||
|
|
076410071c | ||
|
|
9d35b0923a | ||
|
|
666085b9ca | ||
|
|
fd03d468d4 | ||
|
|
a6c46a7420 | ||
|
|
e14851531d | ||
|
|
fb830189d2 | ||
|
|
eaa6ee57a9 | ||
|
|
df8459a95c | ||
|
|
4f14058251 | ||
|
|
09e58b36af | ||
|
|
5562effd66 | ||
|
|
89b003589d | ||
|
|
6c0fae51b7 | ||
|
|
0a54285674 | ||
|
|
35124ea875 | ||
|
|
c6d53fe8b1 | ||
|
|
d639bdcfe9 | ||
|
|
05e2566c35 | ||
|
|
53fe5921d2 | ||
|
|
6a5f9d7f6b | ||
|
|
670907b563 | ||
|
|
b25cc70963 | ||
|
|
f241228b94 | ||
|
|
8e43fc6848 | ||
|
|
3a4c37e04f | ||
|
|
4d8dc7a63c | ||
|
|
94abd04d38 | ||
|
|
71f8c1066a | ||
|
|
fc105dcebc | ||
|
|
7fe74cf997 | ||
|
|
7d4a379f0d | ||
|
|
f9ba7711ec | ||
|
|
da60e56982 | ||
|
|
2a929865e2 | ||
|
|
1162021977 | ||
|
|
92662b3780 | ||
|
|
2ec88fd010 | ||
|
|
5f7ac92c96 | ||
|
|
9e77a9f367 | ||
|
|
91cd00976a | ||
|
|
a35bc71e53 | ||
|
|
f4b819899d | ||
|
|
f3a26f864d | ||
|
|
d5efaad2c3 | ||
|
|
2b8cffd31d | ||
|
|
4fc4ba934d | ||
|
|
20160346d7 | ||
|
|
766267961c | ||
|
|
5dc253799d | ||
|
|
7c766af848 | ||
|
|
530f7c9d6b | ||
|
|
9ddb27b2e3 | ||
|
|
f8b8680b45 | ||
|
|
754d4c3937 | ||
|
|
59f43a58d3 | ||
|
|
0acc260447 | ||
|
|
28d24875a3 | ||
|
|
9dbba60379 | ||
|
|
49bd420c10 | ||
|
|
2340ee5bfb | ||
|
|
ed797ef8b2 | ||
|
|
8cac921272 | ||
|
|
91f0a92838 | ||
|
|
617cc1fdfb | ||
|
|
3415a9c310 | ||
|
|
7275dd2696 | ||
|
|
17f7df0747 | ||
|
|
518f9a8228 | ||
|
|
c8be922111 | ||
|
|
f1a31ce33d | ||
|
|
9b05d0dec0 | ||
|
|
1e4230093e | ||
|
|
611a105988 | ||
|
|
0cd892059b | ||
|
|
a72f038247 | ||
|
|
ae59f04b9f | ||
|
|
61ccaded2c | ||
|
|
856a009dd8 | ||
|
|
7d94209f3e | ||
|
|
d6e4fd424e | ||
|
|
7fdab33368 | ||
|
|
75a65cec95 | ||
|
|
9774f17f64 | ||
|
|
5e97c32e68 | ||
|
|
da5b87950f | ||
|
|
ace1d2f9ee | ||
|
|
fc92c39b7c | ||
|
|
c33b14e3d2 | ||
|
|
fd71352a1b | ||
|
|
e1bc009faf | ||
|
|
55378efe84 | ||
|
|
5dab70fe7e | ||
|
|
4d98d2174e | ||
|
|
d0b6c2f131 | ||
|
|
929c67b1a7 | ||
|
|
3b8684265b | ||
|
|
3e41bba8fb | ||
|
|
fcf680fe0b | ||
|
|
24f266c7ab | ||
|
|
3fadb82c75 | ||
|
|
ab13e8e42e | ||
|
|
0700220418 | ||
|
|
2fc3873bad | ||
|
|
4d5550ad85 | ||
|
|
e329e1cd15 | ||
|
|
013d67fa15 | ||
|
|
51cc034ef9 | ||
|
|
25ebe2d6fb | ||
|
|
a71067e3ec | ||
|
|
540ac0a36a | ||
|
|
b50f92420b | ||
|
|
be2399e993 | ||
|
|
22024a7738 | ||
|
|
745a7f481b | ||
|
|
cd45f09075 | ||
|
|
05a092215e | ||
|
|
e281f53488 | ||
|
|
c4eebaccca | ||
|
|
46cc3898ab | ||
|
|
d9c39cc1d0 | ||
|
|
28cdd485f3 | ||
|
|
e08cccb1f6 | ||
|
|
2e51a581f0 | ||
|
|
7b5f42f9c0 | ||
|
|
37d0283426 | ||
|
|
dc6cb5a0c3 | ||
|
|
befb922739 | ||
|
|
36b6f1eecd | ||
|
|
3ed7bc14d8 | ||
|
|
9236c1c09a | ||
|
|
3ae72cfda1 | ||
|
|
81e0c56484 | ||
|
|
741471c670 | ||
|
|
8732dc546d | ||
|
|
5992bc843a | ||
|
|
c712e108bd | ||
|
|
49e05801c1 | ||
|
|
c9b268041c | ||
|
|
643f4f5af3 | ||
|
|
d3fb16b64a | ||
|
|
83b8ce659e | ||
|
|
19ae89a195 | ||
|
|
bd2bad29a8 | ||
|
|
a58701a297 | ||
|
|
35e73eb530 | ||
|
|
4e8e2d7510 | ||
|
|
f5d401a6a6 | ||
|
|
58e91dc62a | ||
|
|
0e1d778d3a | ||
|
|
6111f9c88c | ||
|
|
1b9ff454fb | ||
|
|
33dffccaa5 | ||
|
|
e73a72959c | ||
|
|
05dfe38f19 | ||
|
|
b7ab16a7df | ||
|
|
1c87a87627 | ||
|
|
0801f0a908 | ||
|
|
82dc586bac | ||
|
|
fd0e02a5c6 | ||
|
|
5bbc65d610 | ||
|
|
b93ea9c747 | ||
|
|
d541870c39 | ||
|
|
54f9d1df15 | ||
|
|
e968cca30a | ||
|
|
ba8f021c14 | ||
|
|
782b2ee827 | ||
|
|
fcf785d3a3 | ||
|
|
8c04888476 | ||
|
|
5fbd0a42fd | ||
|
|
4b7a1af10b | ||
|
|
32c4ce4536 | ||
|
|
3ad3d92e5f | ||
|
|
ac6554efdd | ||
|
|
01aac0ca5d | ||
|
|
8ec60668e6 | ||
|
|
46f9ef19c8 | ||
|
|
a2dcdc1adf | ||
|
|
f0e6496001 | ||
|
|
cb36258c0f | ||
|
|
703cfd7da2 | ||
|
|
2a797a210b | ||
|
|
00ab10b8a6 | ||
|
|
d30c633928 | ||
|
|
ca47838920 | ||
|
|
f8900531e5 | ||
|
|
f0ed2ad115 | ||
|
|
e7de0c90a1 | ||
|
|
89d0cae51d | ||
|
|
77316d639b | ||
|
|
6ee64535df | ||
|
|
714a080ecb | ||
|
|
0dfb3a84cb | ||
|
|
1e3c0b0859 | ||
|
|
49e24140bb | ||
|
|
0978d8675d | ||
|
|
c12ef60038 |
@@ -21,12 +21,12 @@ ENV PYTHONUNBUFFERED=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_VIRTUALENVS_CREATE=false
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
|
||||
# prepend poetry and venv to path
|
||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python -
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
# RUN poetry config virtualenvs.create false
|
||||
|
||||
RUN apt-get update \
|
||||
|
||||
10
.flake8
10
.flake8
@@ -1,10 +0,0 @@
|
||||
[flake8]
|
||||
extend-ignore = [
|
||||
E501 # Line Length - See Black Config in pyproject.toml
|
||||
E402 # Import Not at Top of File
|
||||
]
|
||||
exclude = _all_models.py
|
||||
|
||||
|
||||
per-file-ignores =
|
||||
__init__.py:F403,F401
|
||||
41
.github/DISCUSSION_TEMPLATE/feature-request.yaml
vendored
Normal file
41
.github/DISCUSSION_TEMPLATE/feature-request.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "YOUR TITLE"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: First Check
|
||||
description: |
|
||||
Please confirm and check all the following prior to submission. If you do not do this, your
|
||||
issue may be closed.
|
||||
options:
|
||||
- label: I used the GitHub search to find a similar requests and didn't find it.
|
||||
required: true
|
||||
- label: Checked the [tasks tagged](https://github.com/hay-kot/mealie/issues?q=is%3Aissue+is%3Aopen+label%3Atask+) issues and verified my feature is not covered
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Please provide a concise description of the problem that would be addressed by this feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Please provide a concise description of the feature that would resolve your issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: considerations
|
||||
attributes:
|
||||
label: Please consider and list out some caveats or tradeoffs made in your design decision
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
options:
|
||||
- label: If this is accepted I'm willing to submit a PR to provide this feature
|
||||
- label: If this is accepted I'm willing to help maintain this feature
|
||||
- label: I'm willing to sponsor/pay a developer to do this work
|
||||
42
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
42
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -1,12 +1,16 @@
|
||||
---
|
||||
name: "[v0.5.x] Bug Report"
|
||||
description: "submit a bug report for the current release"
|
||||
name: Bug Report
|
||||
description: "Submit a bug for the latest version of Mealie"
|
||||
title: "[BUG] - YOUR TITLE"
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: First Check
|
||||
description: Please confirm and check all the following options.
|
||||
description: |
|
||||
Please confirm and check all the following prior to submission. If you do not do this, your
|
||||
issue may be closed.
|
||||
options:
|
||||
- label: This is not a feature request
|
||||
required: true
|
||||
@@ -18,8 +22,8 @@ body:
|
||||
required: true
|
||||
- label: I already read the docs and didn't find an answer.
|
||||
required: true
|
||||
- label: I have checked for existing issues that have been resolved in v1-beta
|
||||
required: true
|
||||
- label: This issue can be replicated on the demo site (https://demo.mealie.io/)
|
||||
required: false
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@@ -27,6 +31,23 @@ body:
|
||||
placeholder: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
placeholder: 1) ... 2) ... 3) ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Please provide relevent logs
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Mealie Version
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
@@ -42,13 +63,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os-details
|
||||
id: other
|
||||
attributes:
|
||||
label: Deployment Details
|
||||
description: You can add more details about your operating system here, in particular if you chose "Other". If you are experiencing issues with deployment, please provide your docker-compose or docker commands
|
||||
- type: input
|
||||
id: mealie-version
|
||||
attributes:
|
||||
label: Mealie Version
|
||||
validations:
|
||||
required: true
|
||||
label: Additional Deployment Details
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://github.com/hay-kot/mealie/issues/122
|
||||
about: Please add any Feature Requests here.
|
||||
url: https://github.com/hay-kot/mealie/discussions/new?category=feature-request
|
||||
about: Please add any Feature Requests as a Github Discussion using the for in this issue.
|
||||
|
||||
53
.github/ISSUE_TEMPLATE/recipe-scraper-bug.yaml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/recipe-scraper-bug.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Error Scraping Recipe
|
||||
description: "Submit a bug or issues related to scraping recipes"
|
||||
title: "[SCRAPER] - YOUR TITLE"
|
||||
labels: ["bug", "triage", "scraper"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: First Check
|
||||
description: |
|
||||
Please confirm and check all the following prior to submission. If you do not do this, your
|
||||
issue may be closed.
|
||||
options:
|
||||
- label: I used the GitHub search to find a similar issue and didn't find it.
|
||||
required: true
|
||||
- label: |
|
||||
I have verified that this issue _is not_ related to the underlying library
|
||||
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers) by **1)** checking
|
||||
the [debugger](https://demo.mealie.io/recipe/create/debug) and data is returned, **2)**
|
||||
verifying that there _are_ errors in the log related to application level code, or
|
||||
**3)** verified that the site provides recipe data, or is otherwise supported by
|
||||
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers)
|
||||
required: true
|
||||
- label: This issue can be replicated on the demo site (https://demo.mealie.io/)
|
||||
required: false
|
||||
- type: textarea
|
||||
id: urls
|
||||
attributes:
|
||||
label: Please provide 1-5 example URLs that are having errors
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: |
|
||||
Please provide your logs for the Mealie container `docker logs <container-id> > mealie.logs`
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Deployment
|
||||
description: What Deployment system are you using?
|
||||
multiple: true
|
||||
options:
|
||||
- Docker (Linux)
|
||||
- Docker (Windows)
|
||||
- Docker (Synology)
|
||||
- Unraid
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
47
.github/ISSUE_TEMPLATE/v1-bug-report.yaml
vendored
47
.github/ISSUE_TEMPLATE/v1-bug-report.yaml
vendored
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: v1.0.0b Bug Report
|
||||
description: "submit a bug report for the v1 beta"
|
||||
title: "[v1.0.0b] - YOUR TITLE"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: First Check
|
||||
description: Please confirm and check all the following options.
|
||||
options:
|
||||
- label: This is not a feature request
|
||||
required: true
|
||||
- label: I added a very descriptive title to this issue.
|
||||
required: true
|
||||
- label: I used the GitHub search to find a similar issue and didn't find it.
|
||||
required: true
|
||||
- label: I searched the Mealie documentation, with the integrated search.
|
||||
required: true
|
||||
- label: I already read the docs and didn't find an answer.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What is the issue you are experiencing?
|
||||
placeholder: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Deployment
|
||||
description: What Deployment system are you using?
|
||||
multiple: true
|
||||
options:
|
||||
- Docker (Linux)
|
||||
- Docker (Windows)
|
||||
- Docker (Synology)
|
||||
- Unraid
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os-details
|
||||
attributes:
|
||||
label: Deployment Details
|
||||
description: You can add more details about your operating system here, in particular if you chose "Other". If you are experiencing issues with deployment, please provide your docker-compose or docker commands
|
||||
46
.github/dependabot.yml
vendored
46
.github/dependabot.yml
vendored
@@ -1,46 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Fetch and update latest `npm` packages
|
||||
- package-ecosystem: npm
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "00:00"
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- hay-kot
|
||||
assignees:
|
||||
- hay-kot
|
||||
commit-message:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
# Fetch and update latest `github-actions` pkgs
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "00:00"
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- hay-kot
|
||||
assignees:
|
||||
- hay-kot
|
||||
commit-message:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
- package-ecosystem: pip
|
||||
directory: "/mealie"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "00:00"
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- hay-kot
|
||||
assignees:
|
||||
- hay-kot
|
||||
commit-message:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
25
.github/workflows/nightly.yml
vendored
25
.github/workflows/nightly.yml
vendored
@@ -19,6 +19,9 @@ jobs:
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
|
||||
build-release:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
name: Build Tagged Release
|
||||
uses: ./.github/workflows/partial-builder.yml
|
||||
needs:
|
||||
@@ -41,24 +44,4 @@ jobs:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 A New build of mealie:api-nightly and mealie:frontend-nightly is available"
|
||||
|
||||
deploy-demo:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy Demo
|
||||
needs:
|
||||
- build-release
|
||||
steps:
|
||||
- name: Clean and Deploy Demo
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.DEMO_SERVER_IP }}
|
||||
username: ${{ secrets.DEMO_SERVER_USER }}
|
||||
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
||||
port: ${{ secrets.DEMO_SERVER_PORT }}
|
||||
script_stop: true
|
||||
script: |
|
||||
cd ~/docker/mealie-next
|
||||
docker-compose pull
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
args: "🚀 New builds of ghcr.io/mealie-recipes/mealie:nightly"
|
||||
|
||||
31
.github/workflows/partial-backend.yml
vendored
31
.github/workflows/partial-backend.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
# Database ENV Variablse as Specified by Mealie
|
||||
# Database ENV Variables as Specified by Mealie
|
||||
Database: [sqlite, postgres]
|
||||
|
||||
# Services
|
||||
@@ -27,13 +27,19 @@ jobs:
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
ldap:
|
||||
image: rroemhild/test-openldap
|
||||
ports:
|
||||
- 10389:10389
|
||||
- 10636:10636
|
||||
|
||||
# Steps
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
@@ -54,7 +60,7 @@ jobs:
|
||||
id: cache-validate
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "print('venv good?')" > test.py && poetry run python test.py && echo ::set-output name=cache-hit-success::true
|
||||
echo "import black;print('venv good?')" > test.py && poetry run python test.py && echo ::set-output name=cache-hit-success::true
|
||||
rm test.py
|
||||
continue-on-error: true
|
||||
|
||||
@@ -66,12 +72,11 @@ jobs:
|
||||
poetry add "psycopg2-binary==2.8.6"
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' || steps.cache-validate.outputs.cache-hit-success != 'true'
|
||||
|
||||
- name: Formatting (Black & isort)
|
||||
- name: Formatting (Black)
|
||||
run: |
|
||||
poetry run black . --check
|
||||
poetry run isort . --check-only
|
||||
|
||||
- name: Lint (Flake8)
|
||||
- name: Lint (Ruff)
|
||||
run: |
|
||||
make backend-lint
|
||||
|
||||
@@ -83,5 +88,17 @@ jobs:
|
||||
env:
|
||||
DB_ENGINE: ${{ matrix.Database }}
|
||||
POSTGRES_SERVER: localhost
|
||||
LDAP_AUTH_ENABLED: True
|
||||
LDAP_SERVER_URL: ldap://localhost:10389
|
||||
LDAP_TLS_INSECURE: true
|
||||
LDAP_ENABLE_STARTTLS: false
|
||||
LDAP_BASE_DN: "ou=people,dc=planetexpress,dc=com"
|
||||
LDAP_QUERY_BIND: "cn=admin,dc=planetexpress,dc=com"
|
||||
LDAP_QUERY_PASSWORD: "GoodNewsEveryone"
|
||||
LDAP_USER_FILTER: "(&(|({id_attribute}={input})({mail_attribute}={input}))(|(memberOf=cn=ship_crew,ou=people,dc=planetexpress,dc=com)(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)))"
|
||||
LDAP_ADMIN_FILTER: "memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com"
|
||||
LDAP_ID_ATTRIBUTE: uid
|
||||
LDAP_NAME_ATTRIBUTE: cn
|
||||
LDAP_MAIL_ATTRIBUTE: mail
|
||||
run: |
|
||||
make backend-test
|
||||
|
||||
76
.github/workflows/partial-builder.yml
vendored
76
.github/workflows/partial-builder.yml
vendored
@@ -13,68 +13,34 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-frontend:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Frontend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build Frontend Image
|
||||
working-directory: "frontend"
|
||||
- name: Override __init__.py
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:frontend-${{ inputs.tag }} \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
|
||||
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Backend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build Backend Image
|
||||
run: |
|
||||
docker build --push --no-cache \
|
||||
--tag hkotel/mealie:api-${{ inputs.tag }} \
|
||||
--build-arg COMMIT=$(git rev-parse HEAD) \
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
file: ./docker/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
||||
|
||||
8
.github/workflows/partial-frontend.yml
vendored
8
.github/workflows/partial-frontend.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v2.1.4
|
||||
uses: actions/cache@v3.3.1
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v2.1.4
|
||||
uses: actions/cache@v3.3.1
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
31
.github/workflows/partial-trivy-container-scanning.yml
vendored
Normal file
31
.github/workflows/partial-trivy-container-scanning.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Trivy Container Scanning
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Scan Container
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Dockerfile
|
||||
run: |
|
||||
docker build -t mealie --file=./docker/Dockerfile .
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
ignore-unfixed: true
|
||||
image-ref: "mealie"
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
||||
4
.github/workflows/pull-requests.yml
vendored
4
.github/workflows/pull-requests.yml
vendored
@@ -13,3 +13,7 @@ jobs:
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
|
||||
container-scanning:
|
||||
name: "Trivy Container Scanning"
|
||||
uses: ./.github/workflows/partial-trivy-container-scanning.yml
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- build-release
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,6 +12,7 @@ docs/site/
|
||||
frontend/dist/
|
||||
|
||||
dev/code-generation/generated/*
|
||||
dev/data/mealie.db-journal
|
||||
dev/data/backups/*
|
||||
dev/data/debug/*
|
||||
dev/data/img/*
|
||||
@@ -86,6 +87,7 @@ coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
test.db
|
||||
|
||||
# Translations
|
||||
@@ -158,3 +160,5 @@ dev/code-generation/generated/test_routes.py
|
||||
mealie/services/parser_services/crfpp/model.crfmodel
|
||||
lcov.info
|
||||
dev/code-generation/openapi.json
|
||||
|
||||
.run/
|
||||
|
||||
@@ -10,22 +10,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "4.0.1"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- "flake8-print==4.0.0"
|
||||
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["lokalise.i18n-ally"]
|
||||
}
|
||||
42
.vscode/settings.json
vendored
42
.vscode/settings.json
vendored
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
|
||||
"cSpell.enableFiletypes": [
|
||||
"!javascript",
|
||||
"!python",
|
||||
"!yaml"
|
||||
],
|
||||
"cSpell.words": [
|
||||
"chowdown",
|
||||
"compression",
|
||||
@@ -14,7 +18,9 @@
|
||||
"source.organizeImports": false
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.workingDirectories": ["./frontend"],
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
],
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/.DS_Store": true,
|
||||
@@ -23,30 +29,44 @@
|
||||
"**/.svn": true,
|
||||
"**/CVS": true
|
||||
},
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"vue"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": "frontend/lang/messages",
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
||||
"python.formatting.provider": "black",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.flake8Enabled": false,
|
||||
"python.linting.pylintEnabled": false,
|
||||
"python.linting.pylintArgs": ["--rcfile=${workspaceFolder}/.pylintrc"],
|
||||
"python.linting.pylintArgs": [
|
||||
"--rcfile=${workspaceFolder}/.pylintrc"
|
||||
],
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||
"python.testing.pytestArgs": ["tests"],
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"python.linting.mypyEnabled": true,
|
||||
"isort.path": ["${workspaceFolder}/.venv/bin/isort"],
|
||||
"search.mode": "reuseEditor",
|
||||
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test_*.py"],
|
||||
"python.testing.unittestArgs": [
|
||||
"-v",
|
||||
"-s",
|
||||
"./tests",
|
||||
"-p",
|
||||
"test_*.py"
|
||||
],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig",
|
||||
"pyproject.toml": "poetry.lock, alembic.ini, .pylintrc, .flake8",
|
||||
"pyproject.toml": "poetry.lock, alembic.ini, .pylintrc",
|
||||
"netlify.toml": "runtime.txt",
|
||||
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
|
||||
"README.md": "LICENSE, SECURITY.md"
|
||||
}
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![MIT License][license-shield]][license-url]
|
||||
[![AGPL License][license-shield]][license-url]
|
||||
[![Docker Pulls][docker-pull]][docker-pull]
|
||||
[](https://www.codefactor.io/repository/github/hay-kot/mealie)
|
||||
|
||||
@@ -59,7 +59,7 @@ If you are not a coder, you can still contribute financially. Financial contribu
|
||||
|
||||
<!-- LICENSE -->
|
||||
## License
|
||||
Distributed under the MIT License. See `LICENSE` for more information.
|
||||
Distributed under the AGPL License. See `LICENSE` for more information.
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Add is_ocr_recipe column to recipes
|
||||
|
||||
Revision ID: 089bfa50d0ed
|
||||
Revises: f30cf048c228
|
||||
Revises: 188374910655
|
||||
Create Date: 2022-08-05 17:07:07.389271
|
||||
|
||||
"""
|
||||
@@ -1,7 +1,7 @@
|
||||
"""add extras to shopping lists, list items, and ingredient foods
|
||||
|
||||
Revision ID: 44e8d670719d
|
||||
Revises: 188374910655
|
||||
Revises: 089bfa50d0ed
|
||||
Create Date: 2022-08-29 13:57:40.452245
|
||||
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""add recipe_timeline_events table
|
||||
|
||||
Revision ID: 2ea7a807915c
|
||||
Revises: 44e8d670719d
|
||||
Create Date: 2022-09-27 14:53:14.111054
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2ea7a807915c"
|
||||
down_revision = "44e8d670719d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"recipe_timeline_events",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("subject", sa.String(), nullable=False),
|
||||
sa.Column("message", sa.String(), nullable=True),
|
||||
sa.Column("event_type", sa.String(), nullable=True),
|
||||
sa.Column("image", sa.String(), nullable=True),
|
||||
sa.Column("timestamp", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["recipe_id"],
|
||||
["recipes.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("recipe_timeline_events")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,28 @@
|
||||
"""added recipe last made timestamp
|
||||
|
||||
Revision ID: 1923519381ad
|
||||
Revises: 2ea7a807915c
|
||||
Create Date: 2022-11-03 13:10:24.811134
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1923519381ad"
|
||||
down_revision = "2ea7a807915c"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("recipes", sa.Column("last_made", sa.DateTime(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("recipes", "last_made")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,29 @@
|
||||
"""add recipe_scale to shopping list item ref
|
||||
|
||||
Revision ID: 167eb69066ad
|
||||
Revises: 1923519381ad
|
||||
Create Date: 2022-11-22 03:42:45.494567
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "167eb69066ad"
|
||||
down_revision = "1923519381ad"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("shopping_list_item_recipe_reference", sa.Column("recipe_scale", sa.Float(), nullable=True))
|
||||
op.execute("UPDATE shopping_list_item_recipe_reference SET recipe_scale = 1")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("shopping_list_item_recipe_reference", "recipe_scale")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,43 @@
|
||||
"""add related user to mealplan
|
||||
|
||||
Revision ID: 165d943c64ee
|
||||
Revises: 167eb69066ad
|
||||
Create Date: 2023-01-21 16:54:44.368768
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "165d943c64ee"
|
||||
down_revision = "167eb69066ad"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("group_meal_plans", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(batch_op.f("ix_group_meal_plans_user_id"), ["user_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_user_mealplans", "users", ["user_id"], ["id"])
|
||||
|
||||
with op.batch_alter_table("shopping_list_item_recipe_reference", schema=None) as batch_op:
|
||||
batch_op.alter_column("recipe_scale", existing_type=sa.FLOAT(), nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("shopping_list_item_recipe_reference", schema=None) as batch_op:
|
||||
batch_op.alter_column("recipe_scale", existing_type=sa.FLOAT(), nullable=True)
|
||||
|
||||
with op.batch_alter_table("group_meal_plans", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_user_mealplans", type_="foreignkey")
|
||||
batch_op.drop_index(batch_op.f("ix_group_meal_plans_user_id"))
|
||||
batch_op.drop_column("user_id")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,280 @@
|
||||
"""add missing foreign key and order indices
|
||||
|
||||
Revision ID: ff5f73b01a7a
|
||||
Revises: 165d943c64ee
|
||||
Create Date: 2023-02-07 20:57:21.066927
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ff5f73b01a7a"
|
||||
down_revision = "165d943c64ee"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index(op.f("ix_api_extras_created_at"), "api_extras", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_api_extras_recipee_id"), "api_extras", ["recipee_id"], unique=False)
|
||||
op.create_index(op.f("ix_categories_created_at"), "categories", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_cookbooks_created_at"), "cookbooks", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_cookbooks_group_id"), "cookbooks", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_cookbooks_slug"), "cookbooks", ["slug"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_cookbooks_to_categories_category_id"), "cookbooks_to_categories", ["category_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_cookbooks_to_categories_cookbook_id"), "cookbooks_to_categories", ["cookbook_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_cookbooks_to_tags_cookbook_id"), "cookbooks_to_tags", ["cookbook_id"], unique=False)
|
||||
op.create_index(op.f("ix_cookbooks_to_tags_tag_id"), "cookbooks_to_tags", ["tag_id"], unique=False)
|
||||
op.create_index(op.f("ix_cookbooks_to_tools_cookbook_id"), "cookbooks_to_tools", ["cookbook_id"], unique=False)
|
||||
op.create_index(op.f("ix_cookbooks_to_tools_tool_id"), "cookbooks_to_tools", ["tool_id"], unique=False)
|
||||
op.create_index(op.f("ix_group_data_exports_created_at"), "group_data_exports", ["created_at"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_group_events_notifier_options_created_at"),
|
||||
"group_events_notifier_options",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_group_events_notifiers_created_at"), "group_events_notifiers", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_group_meal_plan_rules_created_at"), "group_meal_plan_rules", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_group_meal_plan_rules_group_id"), "group_meal_plan_rules", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_group_meal_plans_created_at"), "group_meal_plans", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_group_preferences_created_at"), "group_preferences", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_group_reports_created_at"), "group_reports", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_group_to_categories_category_id"), "group_to_categories", ["category_id"], unique=False)
|
||||
op.create_index(op.f("ix_group_to_categories_group_id"), "group_to_categories", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_groups_created_at"), "groups", ["created_at"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_food_extras_created_at"), "ingredient_food_extras", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_food_extras_ingredient_food_id"),
|
||||
"ingredient_food_extras",
|
||||
["ingredient_food_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_ingredient_foods_created_at"), "ingredient_foods", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_ingredient_foods_group_id"), "ingredient_foods", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_ingredient_foods_label_id"), "ingredient_foods", ["label_id"], unique=False)
|
||||
op.create_index(op.f("ix_ingredient_units_created_at"), "ingredient_units", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_ingredient_units_group_id"), "ingredient_units", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_invite_tokens_created_at"), "invite_tokens", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_invite_tokens_group_id"), "invite_tokens", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_long_live_tokens_created_at"), "long_live_tokens", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_long_live_tokens_token"), "long_live_tokens", ["token"], unique=False)
|
||||
op.create_index(op.f("ix_long_live_tokens_user_id"), "long_live_tokens", ["user_id"], unique=False)
|
||||
op.create_index(op.f("ix_multi_purpose_labels_created_at"), "multi_purpose_labels", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_notes_created_at"), "notes", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_notes_recipe_id"), "notes", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_password_reset_tokens_created_at"), "password_reset_tokens", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_password_reset_tokens_user_id"), "password_reset_tokens", ["user_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_plan_rules_to_categories_category_id"), "plan_rules_to_categories", ["category_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_plan_rules_to_categories_group_plan_rule_id"),
|
||||
"plan_rules_to_categories",
|
||||
["group_plan_rule_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_plan_rules_to_tags_plan_rule_id"), "plan_rules_to_tags", ["plan_rule_id"], unique=False)
|
||||
op.create_index(op.f("ix_plan_rules_to_tags_tag_id"), "plan_rules_to_tags", ["tag_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_assets_created_at"), "recipe_assets", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_assets_recipe_id"), "recipe_assets", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_comments_created_at"), "recipe_comments", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_comments_recipe_id"), "recipe_comments", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_comments_user_id"), "recipe_comments", ["user_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_recipe_ingredient_ref_link_created_at"), "recipe_ingredient_ref_link", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_recipe_ingredient_ref_link_instruction_id"),
|
||||
"recipe_ingredient_ref_link",
|
||||
["instruction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_recipe_ingredient_ref_link_reference_id"), "recipe_ingredient_ref_link", ["reference_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_recipe_instructions_created_at"), "recipe_instructions", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_instructions_position"), "recipe_instructions", ["position"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_instructions_recipe_id"), "recipe_instructions", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_nutrition_created_at"), "recipe_nutrition", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_nutrition_recipe_id"), "recipe_nutrition", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_settings_created_at"), "recipe_settings", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_settings_recipe_id"), "recipe_settings", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_share_tokens_created_at"), "recipe_share_tokens", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_share_tokens_recipe_id"), "recipe_share_tokens", ["recipe_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_recipe_timeline_events_created_at"), "recipe_timeline_events", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_recipe_timeline_events_recipe_id"), "recipe_timeline_events", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_timeline_events_timestamp"), "recipe_timeline_events", ["timestamp"], unique=False)
|
||||
op.create_index(op.f("ix_recipe_timeline_events_user_id"), "recipe_timeline_events", ["user_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_created_at"), "recipes", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_name"), "recipes", ["name"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_ingredients_created_at"), "recipes_ingredients", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_ingredients_food_id"), "recipes_ingredients", ["food_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_ingredients_position"), "recipes_ingredients", ["position"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_ingredients_unit_id"), "recipes_ingredients", ["unit_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_recipes_to_categories_category_id"), "recipes_to_categories", ["category_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_recipes_to_categories_recipe_id"), "recipes_to_categories", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_to_tags_recipe_id"), "recipes_to_tags", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_to_tags_tag_id"), "recipes_to_tags", ["tag_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_to_tools_recipe_id"), "recipes_to_tools", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_to_tools_tool_id"), "recipes_to_tools", ["tool_id"], unique=False)
|
||||
op.create_index(op.f("ix_report_entries_created_at"), "report_entries", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_report_entries_report_id"), "report_entries", ["report_id"], unique=False)
|
||||
op.create_index(op.f("ix_server_tasks_created_at"), "server_tasks", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_shopping_list_extras_created_at"), "shopping_list_extras", ["created_at"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_shopping_list_extras_shopping_list_id"), "shopping_list_extras", ["shopping_list_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_shopping_list_item_extras_created_at"), "shopping_list_item_extras", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_shopping_list_item_extras_shopping_list_item_id"),
|
||||
"shopping_list_item_extras",
|
||||
["shopping_list_item_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_shopping_list_item_recipe_reference_created_at"),
|
||||
"shopping_list_item_recipe_reference",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_shopping_list_items_created_at"), "shopping_list_items", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_shopping_list_items_position"), "shopping_list_items", ["position"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_shopping_list_items_shopping_list_id"), "shopping_list_items", ["shopping_list_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_shopping_list_recipe_reference_created_at"),
|
||||
"shopping_list_recipe_reference",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_shopping_lists_created_at"), "shopping_lists", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_tags_created_at"), "tags", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_tools_created_at"), "tools", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_tools_group_id"), "tools", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_users_created_at"), "users", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_users_to_favorites_recipe_id"), "users_to_favorites", ["recipe_id"], unique=False)
|
||||
op.create_index(op.f("ix_users_to_favorites_user_id"), "users_to_favorites", ["user_id"], unique=False)
|
||||
op.create_index(op.f("ix_webhook_urls_created_at"), "webhook_urls", ["created_at"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_webhook_urls_created_at"), table_name="webhook_urls")
|
||||
op.drop_index(op.f("ix_users_to_favorites_user_id"), table_name="users_to_favorites")
|
||||
op.drop_index(op.f("ix_users_to_favorites_recipe_id"), table_name="users_to_favorites")
|
||||
op.drop_index(op.f("ix_users_created_at"), table_name="users")
|
||||
op.drop_index(op.f("ix_tools_group_id"), table_name="tools")
|
||||
op.drop_index(op.f("ix_tools_created_at"), table_name="tools")
|
||||
op.drop_index(op.f("ix_tags_created_at"), table_name="tags")
|
||||
op.drop_index(op.f("ix_shopping_lists_created_at"), table_name="shopping_lists")
|
||||
op.drop_index(op.f("ix_shopping_list_recipe_reference_created_at"), table_name="shopping_list_recipe_reference")
|
||||
op.drop_index(op.f("ix_shopping_list_items_shopping_list_id"), table_name="shopping_list_items")
|
||||
op.drop_index(op.f("ix_shopping_list_items_position"), table_name="shopping_list_items")
|
||||
op.drop_index(op.f("ix_shopping_list_items_created_at"), table_name="shopping_list_items")
|
||||
op.drop_index(
|
||||
op.f("ix_shopping_list_item_recipe_reference_created_at"), table_name="shopping_list_item_recipe_reference"
|
||||
)
|
||||
op.drop_index(op.f("ix_shopping_list_item_extras_shopping_list_item_id"), table_name="shopping_list_item_extras")
|
||||
op.drop_index(op.f("ix_shopping_list_item_extras_created_at"), table_name="shopping_list_item_extras")
|
||||
op.drop_index(op.f("ix_shopping_list_extras_shopping_list_id"), table_name="shopping_list_extras")
|
||||
op.drop_index(op.f("ix_shopping_list_extras_created_at"), table_name="shopping_list_extras")
|
||||
op.drop_index(op.f("ix_server_tasks_created_at"), table_name="server_tasks")
|
||||
op.drop_index(op.f("ix_report_entries_report_id"), table_name="report_entries")
|
||||
op.drop_index(op.f("ix_report_entries_created_at"), table_name="report_entries")
|
||||
op.drop_index(op.f("ix_recipes_to_tools_tool_id"), table_name="recipes_to_tools")
|
||||
op.drop_index(op.f("ix_recipes_to_tools_recipe_id"), table_name="recipes_to_tools")
|
||||
op.drop_index(op.f("ix_recipes_to_tags_tag_id"), table_name="recipes_to_tags")
|
||||
op.drop_index(op.f("ix_recipes_to_tags_recipe_id"), table_name="recipes_to_tags")
|
||||
op.drop_index(op.f("ix_recipes_to_categories_recipe_id"), table_name="recipes_to_categories")
|
||||
op.drop_index(op.f("ix_recipes_to_categories_category_id"), table_name="recipes_to_categories")
|
||||
op.drop_index(op.f("ix_recipes_ingredients_unit_id"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_ingredients_position"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_ingredients_food_id"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_ingredients_created_at"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_name"), table_name="recipes")
|
||||
op.drop_index(op.f("ix_recipes_created_at"), table_name="recipes")
|
||||
op.drop_index(op.f("ix_recipe_timeline_events_user_id"), table_name="recipe_timeline_events")
|
||||
op.drop_index(op.f("ix_recipe_timeline_events_timestamp"), table_name="recipe_timeline_events")
|
||||
op.drop_index(op.f("ix_recipe_timeline_events_recipe_id"), table_name="recipe_timeline_events")
|
||||
op.drop_index(op.f("ix_recipe_timeline_events_created_at"), table_name="recipe_timeline_events")
|
||||
op.drop_index(op.f("ix_recipe_share_tokens_recipe_id"), table_name="recipe_share_tokens")
|
||||
op.drop_index(op.f("ix_recipe_share_tokens_created_at"), table_name="recipe_share_tokens")
|
||||
op.drop_index(op.f("ix_recipe_settings_recipe_id"), table_name="recipe_settings")
|
||||
op.drop_index(op.f("ix_recipe_settings_created_at"), table_name="recipe_settings")
|
||||
op.drop_index(op.f("ix_recipe_nutrition_recipe_id"), table_name="recipe_nutrition")
|
||||
op.drop_index(op.f("ix_recipe_nutrition_created_at"), table_name="recipe_nutrition")
|
||||
op.drop_index(op.f("ix_recipe_instructions_recipe_id"), table_name="recipe_instructions")
|
||||
op.drop_index(op.f("ix_recipe_instructions_position"), table_name="recipe_instructions")
|
||||
op.drop_index(op.f("ix_recipe_instructions_created_at"), table_name="recipe_instructions")
|
||||
op.drop_index(op.f("ix_recipe_ingredient_ref_link_reference_id"), table_name="recipe_ingredient_ref_link")
|
||||
op.drop_index(op.f("ix_recipe_ingredient_ref_link_instruction_id"), table_name="recipe_ingredient_ref_link")
|
||||
op.drop_index(op.f("ix_recipe_ingredient_ref_link_created_at"), table_name="recipe_ingredient_ref_link")
|
||||
op.drop_index(op.f("ix_recipe_comments_user_id"), table_name="recipe_comments")
|
||||
op.drop_index(op.f("ix_recipe_comments_recipe_id"), table_name="recipe_comments")
|
||||
op.drop_index(op.f("ix_recipe_comments_created_at"), table_name="recipe_comments")
|
||||
op.drop_index(op.f("ix_recipe_assets_recipe_id"), table_name="recipe_assets")
|
||||
op.drop_index(op.f("ix_recipe_assets_created_at"), table_name="recipe_assets")
|
||||
op.drop_index(op.f("ix_plan_rules_to_tags_tag_id"), table_name="plan_rules_to_tags")
|
||||
op.drop_index(op.f("ix_plan_rules_to_tags_plan_rule_id"), table_name="plan_rules_to_tags")
|
||||
op.drop_index(op.f("ix_plan_rules_to_categories_group_plan_rule_id"), table_name="plan_rules_to_categories")
|
||||
op.drop_index(op.f("ix_plan_rules_to_categories_category_id"), table_name="plan_rules_to_categories")
|
||||
op.drop_index(op.f("ix_password_reset_tokens_user_id"), table_name="password_reset_tokens")
|
||||
op.drop_index(op.f("ix_password_reset_tokens_created_at"), table_name="password_reset_tokens")
|
||||
op.drop_index(op.f("ix_notes_recipe_id"), table_name="notes")
|
||||
op.drop_index(op.f("ix_notes_created_at"), table_name="notes")
|
||||
op.drop_index(op.f("ix_multi_purpose_labels_created_at"), table_name="multi_purpose_labels")
|
||||
op.drop_index(op.f("ix_long_live_tokens_user_id"), table_name="long_live_tokens")
|
||||
op.drop_index(op.f("ix_long_live_tokens_token"), table_name="long_live_tokens")
|
||||
op.drop_index(op.f("ix_long_live_tokens_created_at"), table_name="long_live_tokens")
|
||||
op.drop_index(op.f("ix_invite_tokens_group_id"), table_name="invite_tokens")
|
||||
op.drop_index(op.f("ix_invite_tokens_created_at"), table_name="invite_tokens")
|
||||
op.drop_index(op.f("ix_ingredient_units_group_id"), table_name="ingredient_units")
|
||||
op.drop_index(op.f("ix_ingredient_units_created_at"), table_name="ingredient_units")
|
||||
op.drop_index(op.f("ix_ingredient_foods_label_id"), table_name="ingredient_foods")
|
||||
op.drop_index(op.f("ix_ingredient_foods_group_id"), table_name="ingredient_foods")
|
||||
op.drop_index(op.f("ix_ingredient_foods_created_at"), table_name="ingredient_foods")
|
||||
op.drop_index(op.f("ix_ingredient_food_extras_ingredient_food_id"), table_name="ingredient_food_extras")
|
||||
op.drop_index(op.f("ix_ingredient_food_extras_created_at"), table_name="ingredient_food_extras")
|
||||
op.drop_index(op.f("ix_groups_created_at"), table_name="groups")
|
||||
op.drop_index(op.f("ix_group_to_categories_group_id"), table_name="group_to_categories")
|
||||
op.drop_index(op.f("ix_group_to_categories_category_id"), table_name="group_to_categories")
|
||||
op.drop_index(op.f("ix_group_reports_created_at"), table_name="group_reports")
|
||||
op.drop_index(op.f("ix_group_preferences_created_at"), table_name="group_preferences")
|
||||
op.drop_index(op.f("ix_group_meal_plans_created_at"), table_name="group_meal_plans")
|
||||
op.drop_index(op.f("ix_group_meal_plan_rules_group_id"), table_name="group_meal_plan_rules")
|
||||
op.drop_index(op.f("ix_group_meal_plan_rules_created_at"), table_name="group_meal_plan_rules")
|
||||
op.drop_index(op.f("ix_group_events_notifiers_created_at"), table_name="group_events_notifiers")
|
||||
op.drop_index(op.f("ix_group_events_notifier_options_created_at"), table_name="group_events_notifier_options")
|
||||
op.drop_index(op.f("ix_group_data_exports_created_at"), table_name="group_data_exports")
|
||||
op.drop_index(op.f("ix_cookbooks_to_tools_tool_id"), table_name="cookbooks_to_tools")
|
||||
op.drop_index(op.f("ix_cookbooks_to_tools_cookbook_id"), table_name="cookbooks_to_tools")
|
||||
op.drop_index(op.f("ix_cookbooks_to_tags_tag_id"), table_name="cookbooks_to_tags")
|
||||
op.drop_index(op.f("ix_cookbooks_to_tags_cookbook_id"), table_name="cookbooks_to_tags")
|
||||
op.drop_index(op.f("ix_cookbooks_to_categories_cookbook_id"), table_name="cookbooks_to_categories")
|
||||
op.drop_index(op.f("ix_cookbooks_to_categories_category_id"), table_name="cookbooks_to_categories")
|
||||
op.drop_index(op.f("ix_cookbooks_slug"), table_name="cookbooks")
|
||||
op.drop_index(op.f("ix_cookbooks_group_id"), table_name="cookbooks")
|
||||
op.drop_index(op.f("ix_cookbooks_created_at"), table_name="cookbooks")
|
||||
op.drop_index(op.f("ix_categories_created_at"), table_name="categories")
|
||||
op.drop_index(op.f("ix_api_extras_recipee_id"), table_name="api_extras")
|
||||
op.drop_index(op.f("ix_api_extras_created_at"), table_name="api_extras")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,37 @@
|
||||
"""add more indices necessary for search
|
||||
|
||||
Revision ID: 16160bf731a0
|
||||
Revises: ff5f73b01a7a
|
||||
Create Date: 2023-02-10 21:18:32.405130
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "16160bf731a0"
|
||||
down_revision = "ff5f73b01a7a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index(op.f("ix_recipe_instructions_text"), "recipe_instructions", ["text"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_description"), "recipes", ["description"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_ingredients_note"), "recipes_ingredients", ["note"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_recipes_ingredients_original_text"), "recipes_ingredients", ["original_text"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_recipes_ingredients_original_text"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_ingredients_note"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_description"), table_name="recipes")
|
||||
op.drop_index(op.f("ix_recipe_instructions_text"), table_name="recipe_instructions")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,118 @@
|
||||
"""add normalized search properties
|
||||
|
||||
Revision ID: 5ab195a474eb
|
||||
Revises: 16160bf731a0
|
||||
Create Date: 2023-02-14 20:45:41.102571
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
|
||||
from text_unidecode import unidecode
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
from mealie.db.models._model_utils import GUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5ab195a474eb"
|
||||
down_revision = "16160bf731a0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
class SqlAlchemyBase(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
# Intermediate table definitions
|
||||
class RecipeModel(SqlAlchemyBase):
|
||||
__tablename__ = "recipes"
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(sa.String)
|
||||
name_normalized: Mapped[str] = mapped_column(sa.String, nullable=False, index=True)
|
||||
description_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
|
||||
class RecipeIngredient(SqlAlchemyBase):
|
||||
__tablename__ = "recipes_ingredients"
|
||||
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
note: Mapped[str | None] = mapped_column(sa.String)
|
||||
original_text: Mapped[str | None] = mapped_column(sa.String)
|
||||
note_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
original_text_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
|
||||
def do_data_migration():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
recipes = session.execute(select(RecipeModel)).scalars().all()
|
||||
ingredients = session.execute(select(RecipeIngredient)).scalars().all()
|
||||
for recipe in recipes:
|
||||
if recipe.name is not None:
|
||||
recipe.name_normalized = unidecode(recipe.name).lower().strip()
|
||||
|
||||
if recipe.description is not None:
|
||||
recipe.description_normalized = unidecode(recipe.description).lower().strip()
|
||||
session.add(recipe)
|
||||
|
||||
for ingredient in ingredients:
|
||||
if ingredient.note is not None:
|
||||
ingredient.note_normalized = unidecode(ingredient.note).lower().strip()
|
||||
|
||||
if ingredient.original_text is not None:
|
||||
ingredient.original_text_normalized = unidecode(ingredient.original_text).lower().strip()
|
||||
session.add(ingredient)
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# Set column to nullable first, since we do not have values here yet
|
||||
op.add_column("recipes", sa.Column("name_normalized", sa.String(), nullable=True))
|
||||
op.add_column("recipes", sa.Column("description_normalized", sa.String(), nullable=True))
|
||||
op.drop_index("ix_recipes_description", table_name="recipes")
|
||||
op.drop_index("ix_recipes_name", table_name="recipes")
|
||||
op.create_index(op.f("ix_recipes_description_normalized"), "recipes", ["description_normalized"], unique=False)
|
||||
op.create_index(op.f("ix_recipes_name_normalized"), "recipes", ["name_normalized"], unique=False)
|
||||
op.add_column("recipes_ingredients", sa.Column("note_normalized", sa.String(), nullable=True))
|
||||
op.add_column("recipes_ingredients", sa.Column("original_text_normalized", sa.String(), nullable=True))
|
||||
op.drop_index("ix_recipes_ingredients_note", table_name="recipes_ingredients")
|
||||
op.drop_index("ix_recipes_ingredients_original_text", table_name="recipes_ingredients")
|
||||
op.create_index(
|
||||
op.f("ix_recipes_ingredients_note_normalized"), "recipes_ingredients", ["note_normalized"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_recipes_ingredients_original_text_normalized"),
|
||||
"recipes_ingredients",
|
||||
["original_text_normalized"],
|
||||
unique=False,
|
||||
)
|
||||
do_data_migration()
|
||||
# Make recipes.name_normalized not nullable now that column should be filled for all rows
|
||||
with op.batch_alter_table("recipes", schema=None) as batch_op:
|
||||
batch_op.alter_column("name_normalized", nullable=False, existing_type=sa.String())
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_recipes_ingredients_original_text_normalized"), table_name="recipes_ingredients")
|
||||
op.drop_index(op.f("ix_recipes_ingredients_note_normalized"), table_name="recipes_ingredients")
|
||||
op.create_index("ix_recipes_ingredients_original_text", "recipes_ingredients", ["original_text"], unique=False)
|
||||
op.create_index("ix_recipes_ingredients_note", "recipes_ingredients", ["note"], unique=False)
|
||||
op.drop_column("recipes_ingredients", "original_text_normalized")
|
||||
op.drop_column("recipes_ingredients", "note_normalized")
|
||||
op.drop_index(op.f("ix_recipes_name_normalized"), table_name="recipes")
|
||||
op.drop_index(op.f("ix_recipes_description_normalized"), table_name="recipes")
|
||||
op.create_index("ix_recipes_name", "recipes", ["name"], unique=False)
|
||||
op.create_index("ix_recipes_description", "recipes", ["description"], unique=False)
|
||||
op.drop_column("recipes", "description_normalized")
|
||||
op.drop_column("recipes", "name_normalized")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,69 @@
|
||||
"""added shopping list label settings
|
||||
|
||||
Revision ID: b04a08da2108
|
||||
Revises: 5ab195a474eb
|
||||
Create Date: 2023-21-02 22:03:19.837244
|
||||
|
||||
"""
|
||||
from uuid import uuid4
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models.group.shopping_list import ShoppingList
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b04a08da2108"
|
||||
down_revision = "5ab195a474eb"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table: sa.Table, session: Session):
|
||||
shopping_lists = session.query(ShoppingList).all()
|
||||
labels = session.query(MultiPurposeLabel).all()
|
||||
|
||||
shopping_lists_labels_data: list[dict] = []
|
||||
for shopping_list in shopping_lists:
|
||||
for i, label in enumerate(labels):
|
||||
shopping_lists_labels_data.append(
|
||||
{"id": uuid4(), "shopping_list_id": shopping_list.id, "label_id": label.id, "position": i}
|
||||
)
|
||||
|
||||
op.bulk_insert(shopping_lists_multi_purpose_labels_table, shopping_lists_labels_data)
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
shopping_lists_multi_purpose_labels_table = op.create_table(
|
||||
"shopping_lists_multi_purpose_labels",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("shopping_list_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("label_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("position", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["label_id"],
|
||||
["multi_purpose_labels.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["shopping_list_id"],
|
||||
["shopping_lists.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", "shopping_list_id", "label_id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
session = Session(bind=op.get_bind())
|
||||
populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table, session)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("shopping_lists_multi_purpose_labels")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,43 @@
|
||||
"""add auth_method to user table
|
||||
|
||||
Revision ID: 38514b39a824
|
||||
Revises: b04a08da2108
|
||||
Create Date: 2023-02-22 21:45:52.900964
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "38514b39a824"
|
||||
down_revision = "b04a08da2108"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
return op.get_context().dialect.name == "postgresql"
|
||||
|
||||
|
||||
authMethod = sa.Enum("MEALIE", "LDAP", name="authmethod")
|
||||
|
||||
|
||||
def upgrade():
|
||||
if is_postgres():
|
||||
authMethod.create(op.get_bind())
|
||||
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("auth_method", authMethod, nullable=False, server_default="MEALIE"),
|
||||
)
|
||||
op.execute("UPDATE users SET auth_method = 'LDAP' WHERE password = 'LDAP'")
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.drop_column("auth_method")
|
||||
|
||||
if is_postgres():
|
||||
authMethod.drop(op.get_bind())
|
||||
@@ -0,0 +1,89 @@
|
||||
"""postgres fuzzy search
|
||||
|
||||
Revision ID: b3dbb554ba53
|
||||
Revises: 38514b39a824
|
||||
Create Date: 2023-04-13 06:47:04.617131
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
import alembic.context as context
|
||||
from mealie.core.config import get_app_settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b3dbb554ba53"
|
||||
down_revision = "38514b39a824"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def get_db_type():
|
||||
return op.get_context().dialect.name
|
||||
|
||||
|
||||
def setup_postgres_trigrams():
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;")
|
||||
op.create_index(
|
||||
"ix_recipes_name_normalized_gin",
|
||||
table_name="recipes",
|
||||
columns=["name_normalized"],
|
||||
unique=False,
|
||||
postgresql_using="gin",
|
||||
postgresql_ops={
|
||||
"name_normalized": "gin_trgm_ops",
|
||||
},
|
||||
)
|
||||
op.create_index(
|
||||
"ix_recipes_description_normalized_gin",
|
||||
table_name="recipes",
|
||||
columns=["description_normalized"],
|
||||
unique=False,
|
||||
postgresql_using="gin",
|
||||
postgresql_ops={
|
||||
"description_normalized": "gin_trgm_ops",
|
||||
},
|
||||
)
|
||||
op.create_index(
|
||||
"ix_recipes_ingredients_note_normalized_gin",
|
||||
table_name="recipes_ingredients",
|
||||
columns=["note_normalized"],
|
||||
unique=False,
|
||||
postgresql_using="gin",
|
||||
postgresql_ops={
|
||||
"note_normalized": "gin_trgm_ops",
|
||||
},
|
||||
)
|
||||
op.create_index(
|
||||
"ix_recipes_ingredients_original_text_normalized_gin",
|
||||
table_name="recipes_ingredients",
|
||||
columns=["original_text_normalized"],
|
||||
unique=False,
|
||||
postgresql_using="gin",
|
||||
postgresql_ops={
|
||||
"original_text_normalized": "gin_trgm_ops",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def remove_postgres_trigrams():
|
||||
op.execute("DROP EXTENSION IF EXISTS pg_trgm;")
|
||||
op.drop_index("ix_recipes_name_normalized_gin", table_name="recipe")
|
||||
op.drop_index("ix_recipes_description_normalized_gin", table_name="recipe")
|
||||
op.drop_index("ix_recipes_ingredients_note_normalized_gin", table_name="recipes_ingredients")
|
||||
op.drop_index("ix_recipes_ingredients_original_text_normalized_gin", table_name="recipes_ingredients")
|
||||
|
||||
|
||||
def upgrade():
|
||||
if get_db_type() == "postgresql":
|
||||
setup_postgres_trigrams()
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
if get_db_type() == "postgres":
|
||||
remove_postgres_trigrams()
|
||||
else:
|
||||
pass
|
||||
@@ -0,0 +1,56 @@
|
||||
"""added group slug
|
||||
|
||||
Revision ID: 04ac51cbe9a4
|
||||
Revises: b3dbb554ba53
|
||||
Create Date: 2023-08-06 21:00:34.582905
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from alembic import op
|
||||
from mealie.db.models.group.group import Group
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "04ac51cbe9a4"
|
||||
down_revision = "b3dbb554ba53"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def populate_group_slugs(session: Session):
|
||||
groups: list[Group] = session.query(Group).all()
|
||||
seen_slugs: set[str] = set()
|
||||
for group in groups:
|
||||
original_name = group.name
|
||||
attempts = 0
|
||||
while True:
|
||||
slug = slugify(group.name)
|
||||
if slug not in seen_slugs:
|
||||
break
|
||||
|
||||
attempts += 1
|
||||
group.name = f"{original_name} ({attempts})"
|
||||
|
||||
seen_slugs.add(slug)
|
||||
group.slug = slug
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("groups", sa.Column("slug", sa.String(), nullable=True))
|
||||
op.create_index(op.f("ix_groups_slug"), "groups", ["slug"], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
session = Session(bind=op.get_bind())
|
||||
populate_group_slugs(session)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_groups_slug"), table_name="groups")
|
||||
op.drop_column("groups", "slug")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,28 @@
|
||||
"""added recipe note to shopping list recipe ref
|
||||
|
||||
Revision ID: 1825b5225403
|
||||
Revises: 04ac51cbe9a4
|
||||
Create Date: 2023-08-14 19:30:49.103185
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1825b5225403"
|
||||
down_revision = "04ac51cbe9a4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("shopping_list_item_recipe_reference", sa.Column("recipe_note", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("shopping_list_item_recipe_reference", "recipe_note")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""remove tool name and slug unique contraints
|
||||
|
||||
Revision ID: bcfdad6b7355
|
||||
Revises: 1825b5225403
|
||||
Create Date: 2023-08-15 16:25:07.058929
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "bcfdad6b7355"
|
||||
down_revision = "1825b5225403"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index("ix_tools_name", table_name="tools")
|
||||
op.create_index(op.f("ix_tools_name"), "tools", ["name"], unique=False)
|
||||
op.drop_index("ix_tools_slug", table_name="tools")
|
||||
op.create_index(op.f("ix_tools_slug"), "tools", ["slug"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_tools_slug"), table_name="tools")
|
||||
op.create_index("ix_tools_slug", "tools", ["slug"], unique=True)
|
||||
op.drop_index(op.f("ix_tools_name"), table_name="tools")
|
||||
op.create_index("ix_tools_name", "tools", ["name"], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,71 @@
|
||||
"""added normalized unit and food names
|
||||
|
||||
Revision ID: 0341b154f79a
|
||||
Revises: bcfdad6b7355
|
||||
Create Date: 2023-09-01 14:55:42.166766
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm, select
|
||||
|
||||
from alembic import op
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0341b154f79a"
|
||||
down_revision = "bcfdad6b7355"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def populate_normalized_fields():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
units = session.execute(select(IngredientUnitModel)).scalars().all()
|
||||
for unit in units:
|
||||
if unit.name is not None:
|
||||
unit.name_normalized = IngredientUnitModel.normalize(unit.name)
|
||||
|
||||
if unit.abbreviation is not None:
|
||||
unit.abbreviation_normalized = IngredientUnitModel.normalize(unit.abbreviation)
|
||||
|
||||
session.add(unit)
|
||||
|
||||
foods = session.execute(select(IngredientFoodModel)).scalars().all()
|
||||
for food in foods:
|
||||
if food.name is not None:
|
||||
food.name_normalized = IngredientFoodModel.normalize(food.name)
|
||||
|
||||
session.add(food)
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("ingredient_foods", sa.Column("name_normalized", sa.String(), nullable=True))
|
||||
op.create_index(op.f("ix_ingredient_foods_name_normalized"), "ingredient_foods", ["name_normalized"], unique=False)
|
||||
op.add_column("ingredient_units", sa.Column("name_normalized", sa.String(), nullable=True))
|
||||
op.add_column("ingredient_units", sa.Column("abbreviation_normalized", sa.String(), nullable=True))
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_units_abbreviation_normalized"),
|
||||
"ingredient_units",
|
||||
["abbreviation_normalized"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_ingredient_units_name_normalized"), "ingredient_units", ["name_normalized"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
populate_normalized_fields()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_ingredient_units_name_normalized"), table_name="ingredient_units")
|
||||
op.drop_index(op.f("ix_ingredient_units_abbreviation_normalized"), table_name="ingredient_units")
|
||||
op.drop_column("ingredient_units", "abbreviation_normalized")
|
||||
op.drop_column("ingredient_units", "name_normalized")
|
||||
op.drop_index(op.f("ix_ingredient_foods_name_normalized"), table_name="ingredient_foods")
|
||||
op.drop_column("ingredient_foods", "name_normalized")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,172 @@
|
||||
"""added unique constraints
|
||||
|
||||
Revision ID: dded3119c1fe
|
||||
Revises: 0341b154f79a
|
||||
Create Date: 2023-10-04 14:29:26.688065
|
||||
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "dded3119c1fe"
|
||||
down_revision = "0341b154f79a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TableMeta:
|
||||
tablename: str
|
||||
pk_1: str
|
||||
pk_2: str
|
||||
|
||||
@classmethod
|
||||
def composite_pk(self, pk_1_val: Any, pk_2_val: Any) -> str:
|
||||
return "$$".join([pk_1_val, pk_2_val])
|
||||
|
||||
|
||||
def _is_postgres():
|
||||
return op.get_context().dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _remove_duplicates_from_m2m_table(session: Session, table_meta: TableMeta):
|
||||
if _is_postgres():
|
||||
default_pk = "CTID"
|
||||
else:
|
||||
default_pk = "ROWID"
|
||||
|
||||
# some of these tables are missing defined unique pks, so we have to rely on the database default pk
|
||||
query = sa.text(
|
||||
f"""
|
||||
DELETE FROM {table_meta.tablename}
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM {table_meta.tablename} t2
|
||||
WHERE {table_meta.tablename}.{table_meta.pk_1} = t2.{table_meta.pk_1}
|
||||
AND {table_meta.tablename}.{table_meta.pk_2} = t2.{table_meta.pk_2}
|
||||
AND {table_meta.tablename}.{default_pk} > t2.{default_pk}
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
session.execute(query)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _remove_duplicates_from_m2m_tables(table_metas: list[TableMeta]):
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
for table_meta in table_metas:
|
||||
_remove_duplicates_from_m2m_table(session, table_meta)
|
||||
|
||||
|
||||
def upgrade():
|
||||
_remove_duplicates_from_m2m_tables(
|
||||
[
|
||||
# M2M
|
||||
TableMeta("cookbooks_to_categories", "cookbook_id", "category_id"),
|
||||
TableMeta("cookbooks_to_tags", "cookbook_id", "tag_id"),
|
||||
TableMeta("cookbooks_to_tools", "cookbook_id", "tool_id"),
|
||||
TableMeta("group_to_categories", "group_id", "category_id"),
|
||||
TableMeta("plan_rules_to_categories", "group_plan_rule_id", "category_id"),
|
||||
TableMeta("plan_rules_to_tags", "plan_rule_id", "tag_id"),
|
||||
TableMeta("recipes_to_categories", "recipe_id", "category_id"),
|
||||
TableMeta("recipes_to_tags", "recipe_id", "tag_id"),
|
||||
TableMeta("recipes_to_tools", "recipe_id", "tool_id"),
|
||||
TableMeta("users_to_favorites", "user_id", "recipe_id"),
|
||||
TableMeta("shopping_lists_multi_purpose_labels", "shopping_list_id", "label_id"),
|
||||
# Foods/Units/Labels
|
||||
TableMeta("ingredient_foods", "name", "group_id"),
|
||||
TableMeta("ingredient_units", "name", "group_id"),
|
||||
TableMeta("multi_purpose_labels", "name", "group_id"),
|
||||
]
|
||||
)
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# we use batch_alter_table here because otherwise this fails on sqlite
|
||||
|
||||
# M2M
|
||||
with op.batch_alter_table("cookbooks_to_categories") as batch_op:
|
||||
batch_op.create_unique_constraint("cookbook_id_category_id_key", ["cookbook_id", "category_id"])
|
||||
|
||||
with op.batch_alter_table("cookbooks_to_tags") as batch_op:
|
||||
batch_op.create_unique_constraint("cookbook_id_tag_id_key", ["cookbook_id", "tag_id"])
|
||||
|
||||
with op.batch_alter_table("cookbooks_to_tools") as batch_op:
|
||||
batch_op.create_unique_constraint("cookbook_id_tool_id_key", ["cookbook_id", "tool_id"])
|
||||
|
||||
with op.batch_alter_table("group_to_categories") as batch_op:
|
||||
batch_op.create_unique_constraint("group_id_category_id_key", ["group_id", "category_id"])
|
||||
|
||||
with op.batch_alter_table("plan_rules_to_categories") as batch_op:
|
||||
batch_op.create_unique_constraint("group_plan_rule_id_category_id_key", ["group_plan_rule_id", "category_id"])
|
||||
|
||||
with op.batch_alter_table("plan_rules_to_tags") as batch_op:
|
||||
batch_op.create_unique_constraint("plan_rule_id_tag_id_key", ["plan_rule_id", "tag_id"])
|
||||
|
||||
with op.batch_alter_table("recipes_to_categories") as batch_op:
|
||||
batch_op.create_unique_constraint("recipe_id_category_id_key", ["recipe_id", "category_id"])
|
||||
|
||||
with op.batch_alter_table("recipes_to_tags") as batch_op:
|
||||
batch_op.create_unique_constraint("recipe_id_tag_id_key", ["recipe_id", "tag_id"])
|
||||
|
||||
with op.batch_alter_table("recipes_to_tools") as batch_op:
|
||||
batch_op.create_unique_constraint("recipe_id_tool_id_key", ["recipe_id", "tool_id"])
|
||||
|
||||
with op.batch_alter_table("users_to_favorites") as batch_op:
|
||||
batch_op.create_unique_constraint("user_id_recipe_id_key", ["user_id", "recipe_id"])
|
||||
|
||||
with op.batch_alter_table("shopping_lists_multi_purpose_labels") as batch_op:
|
||||
batch_op.create_unique_constraint("shopping_list_id_label_id_key", ["shopping_list_id", "label_id"])
|
||||
|
||||
# Foods/Units/Labels
|
||||
with op.batch_alter_table("ingredient_foods") as batch_op:
|
||||
batch_op.create_unique_constraint("ingredient_foods_name_group_id_key", ["name", "group_id"])
|
||||
|
||||
with op.batch_alter_table("ingredient_units") as batch_op:
|
||||
batch_op.create_unique_constraint("ingredient_units_name_group_id_key", ["name", "group_id"])
|
||||
|
||||
with op.batch_alter_table("multi_purpose_labels") as batch_op:
|
||||
batch_op.create_unique_constraint("multi_purpose_labels_name_group_id_key", ["name", "group_id"])
|
||||
|
||||
op.create_index(
|
||||
op.f("ix_shopping_lists_multi_purpose_labels_created_at"),
|
||||
"shopping_lists_multi_purpose_labels",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# M2M
|
||||
op.drop_constraint("user_id_recipe_id_key", "users_to_favorites", type_="unique")
|
||||
op.drop_index(
|
||||
op.f("ix_shopping_lists_multi_purpose_labels_created_at"), table_name="shopping_lists_multi_purpose_labels"
|
||||
)
|
||||
op.drop_constraint("recipe_id_tool_id_key", "recipes_to_tools", type_="unique")
|
||||
op.drop_constraint("recipe_id_tag_id_key", "recipes_to_tags", type_="unique")
|
||||
op.drop_constraint("recipe_id_category_id_key", "recipes_to_categories", type_="unique")
|
||||
op.drop_constraint("plan_rule_id_tag_id_key", "plan_rules_to_tags", type_="unique")
|
||||
op.drop_constraint("group_plan_rule_id_category_id_key", "plan_rules_to_categories", type_="unique")
|
||||
op.drop_constraint("group_id_category_id_key", "group_to_categories", type_="unique")
|
||||
op.drop_constraint("cookbook_id_tool_id_key", "cookbooks_to_tools", type_="unique")
|
||||
op.drop_constraint("cookbook_id_tag_id_key", "cookbooks_to_tags", type_="unique")
|
||||
op.drop_constraint("cookbook_id_category_id_key", "cookbooks_to_categories", type_="unique")
|
||||
op.drop_constraint("shopping_list_id_label_id_key", "shopping_lists_multi_purpose_labels", type_="unique")
|
||||
|
||||
# Foods/Units/Labels
|
||||
op.drop_constraint("multi_purpose_labels_name_group_id_key", "multi_purpose_labels", type_="unique")
|
||||
op.drop_constraint("ingredient_units_name_group_id_key", "ingredient_units", type_="unique")
|
||||
op.drop_constraint("ingredient_foods_name_group_id_key", "ingredient_foods", type_="unique")
|
||||
# ### end Alembic commands ###
|
||||
@@ -25,7 +25,6 @@ def get_path_objects(app: FastAPI):
|
||||
for key, value in app.openapi().items():
|
||||
if key == "paths":
|
||||
for key, value in value.items():
|
||||
|
||||
paths.append(
|
||||
PathObject(
|
||||
route_object=RouteObject(key),
|
||||
@@ -50,7 +49,6 @@ def read_template(file: Path):
|
||||
|
||||
|
||||
def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]):
|
||||
|
||||
template = Template(read_template(CodeTemplates.pytest_routes))
|
||||
content = template.render(
|
||||
paths={
|
||||
|
||||
@@ -79,14 +79,12 @@ def find_modules(root: pathlib.Path) -> list[Modules]:
|
||||
modules: list[Modules] = []
|
||||
for file in root.iterdir():
|
||||
if file.is_dir() and file.name not in SKIP:
|
||||
|
||||
modules.append(Modules(directory=file))
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
modules = find_modules(SCHEMA_PATH)
|
||||
|
||||
for module in modules:
|
||||
|
||||
@@ -231,8 +231,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
}
|
||||
|
||||
|
||||
def login(username="changeme@email.com", password="MyPassword"):
|
||||
|
||||
def login(username="changeme@example.com", password="MyPassword"):
|
||||
payload = {"username": username, "password": password}
|
||||
r = requests.post("http://localhost:9000/api/auth/token", payload)
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# WARNING: currently not functional, see #756, #1072
|
||||
# Use root/example as user/password credentials
|
||||
version: "3.4"
|
||||
services:
|
||||
# Vue Frontend
|
||||
mealie-frontend:
|
||||
container_name: mealie-frontend
|
||||
image: mealie-frontend:dev
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.frontend
|
||||
restart: always
|
||||
ports:
|
||||
- 9920:8080
|
||||
environment:
|
||||
- GLOBAL_MIDDLEWARE=null
|
||||
- BASE_URL=""
|
||||
- ALLOW_SIGNUP=true
|
||||
volumes:
|
||||
- ./frontend/:/app
|
||||
- /app/node_modules
|
||||
|
||||
# Fast API
|
||||
mealie-api:
|
||||
container_name: mealie-api
|
||||
image: mealie-api:dev
|
||||
build:
|
||||
context: ./
|
||||
target: development
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- 9921:9000
|
||||
environment:
|
||||
TZ: America/Anchorage # Specify Correct Timezone for Date/Time to line up correctly.
|
||||
volumes:
|
||||
- ./dev/data:/app/dev/data
|
||||
- ./mealie:/app/mealie
|
||||
|
||||
# Mkdocs
|
||||
mealie-docs:
|
||||
container_name: mealie-docs
|
||||
image: squidfunk/mkdocs-material
|
||||
restart: always
|
||||
ports:
|
||||
- 9922:8000
|
||||
volumes:
|
||||
- ./docs:/docs
|
||||
@@ -1,93 +0,0 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mealie-frontend:
|
||||
container_name: mealie-frontend
|
||||
image: mealie-frontend:dev
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 500M
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
ports:
|
||||
- 9091:3000
|
||||
environment:
|
||||
- API_URL=http://mealie-api:9000
|
||||
|
||||
# =====================================
|
||||
# Light Mode Config
|
||||
- THEME_LIGHT_PRIMARY=#E58325
|
||||
- THEME_LIGHT_ACCENT=#007A99
|
||||
- THEME_LIGHT_SECONDARY=#973542
|
||||
- THEME_LIGHT_SUCCESS=#43A047
|
||||
- THEME_LIGHT_INFO=#1976D2
|
||||
- THEME_LIGHT_WARNING=#FF6D00
|
||||
- THEME_LIGHT_ERROR=#EF5350
|
||||
# =====================================
|
||||
# Dark Mode Config
|
||||
- THEME_DARK_PRIMARY=#E58325
|
||||
- THEME_DARK_ACCENT=#007A99
|
||||
- THEME_DARK_SECONDARY=#973542
|
||||
- THEME_DARK_SUCCESS=#43A047
|
||||
- THEME_DARK_INFO=#1976D2
|
||||
- THEME_DARK_WARNING=#FF6D00
|
||||
- THEME_DARK_ERROR=#EF5350
|
||||
mealie:
|
||||
container_name: mealie-api
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M
|
||||
build:
|
||||
context: ./
|
||||
target: production
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
ports:
|
||||
- 9092:9000
|
||||
environment:
|
||||
ALLOW_SIGNUP: "false"
|
||||
|
||||
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
|
||||
# =====================================
|
||||
# Postgres Config
|
||||
POSTGRES_USER: mealie
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_SERVER: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: mealie
|
||||
|
||||
# =====================================
|
||||
# Web Concurrency
|
||||
WEB_GUNICORN: true
|
||||
WORKERS_PER_CORE: 0.5
|
||||
MAX_WORKERS: 1
|
||||
WEB_CONCURRENCY: 1
|
||||
|
||||
# =====================================
|
||||
# Email Configuration
|
||||
# SMTP_HOST=
|
||||
# SMTP_PORT=587
|
||||
# SMTP_FROM_NAME=Mealie
|
||||
# SMTP_AUTH_STRATEGY=TLS # Options: 'TLS', 'SSL', 'NONE'
|
||||
# SMTP_FROM_EMAIL=
|
||||
# SMTP_USER=
|
||||
# SMTP_PASSWORD=
|
||||
|
||||
# postgres:
|
||||
# container_name: postgres
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD: mealie
|
||||
# POSTGRES_USER: mealie
|
||||
|
||||
volumes:
|
||||
mealie-data:
|
||||
driver: local
|
||||
@@ -1,5 +1,21 @@
|
||||
FROM node:16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./frontend .
|
||||
|
||||
RUN yarn install \
|
||||
--prefer-offline \
|
||||
--frozen-lockfile \
|
||||
--non-interactive \
|
||||
--production=false \
|
||||
# https://github.com/docker/build-push-action/issues/471
|
||||
--network-timeout 1000000
|
||||
|
||||
RUN yarn generate
|
||||
|
||||
###############################################
|
||||
# Base Image
|
||||
# Base Image - Python
|
||||
###############################################
|
||||
FROM python:3.10-slim as python-base
|
||||
|
||||
@@ -38,10 +54,11 @@ RUN apt-get update \
|
||||
# LDAP Dependencies
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install -U --no-cache-dir pip
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
ENV POETRY_VERSION=1.2.1
|
||||
ENV POETRY_VERSION=1.3.1
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# copy project requirement files here to ensure they will be cached.
|
||||
@@ -49,34 +66,7 @@ WORKDIR $PYSETUP_PATH
|
||||
COPY ./poetry.lock ./pyproject.toml ./
|
||||
|
||||
# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
|
||||
RUN poetry install -E pgsql --no-dev
|
||||
|
||||
###############################################
|
||||
# Development Image
|
||||
###############################################
|
||||
FROM python-base as development
|
||||
ENV PRODUCTION=false
|
||||
ENV TESTING=false
|
||||
|
||||
# copying poetry and venv into image
|
||||
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
|
||||
# copy backend
|
||||
COPY ./mealie $MEALIE_HOME/mealie
|
||||
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
||||
|
||||
# Alembic
|
||||
COPY ./alembic $MEALIE_HOME/alembic
|
||||
COPY ./alembic.ini $MEALIE_HOME/
|
||||
|
||||
# venv already has runtime deps installed we get a quicker install
|
||||
WORKDIR $MEALIE_HOME
|
||||
RUN . $VENV_PATH/bin/activate && poetry install
|
||||
WORKDIR /
|
||||
|
||||
RUN chmod +x $MEALIE_HOME/mealie/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/mealie/run.sh "reload"
|
||||
RUN poetry install -E pgsql --only main
|
||||
|
||||
###############################################
|
||||
# CRFPP Image
|
||||
@@ -98,8 +88,9 @@ ENV GIT_COMMIT_HASH=$COMMIT
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
gosu \
|
||||
iproute2 \
|
||||
tesseract-ocr-all \
|
||||
&& apt-get autoremove \
|
||||
libldap-common \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# copying poetry and venv into image
|
||||
@@ -122,7 +113,7 @@ COPY ./alembic.ini $MEALIE_HOME/
|
||||
|
||||
# venv already has runtime deps installed we get a quicker install
|
||||
WORKDIR $MEALIE_HOME
|
||||
RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --no-dev
|
||||
RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --only main
|
||||
WORKDIR /
|
||||
|
||||
# Grab CRF++ Model Release
|
||||
@@ -135,5 +126,17 @@ EXPOSE ${APP_PORT}
|
||||
|
||||
HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1
|
||||
|
||||
RUN chmod +x $MEALIE_HOME/mealie/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/mealie/run.sh
|
||||
# ----------------------------------
|
||||
# Copy Frontend
|
||||
|
||||
# copying caddy into image
|
||||
ENV STATIC_FILES=/spa/static
|
||||
COPY --from=builder /app/dist ${STATIC_FILES}
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
|
||||
EXPOSE ${APP_PORT}
|
||||
COPY ./docker/entry.sh $MEALIE_HOME/run.sh
|
||||
|
||||
RUN chmod +x $MEALIE_HOME/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/run.sh
|
||||
45
docker/docker-compose.yml
Normal file
45
docker/docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mealie:
|
||||
container_name: mealie
|
||||
image: mealie:dev
|
||||
build:
|
||||
context: ../
|
||||
target: production
|
||||
dockerfile: ./docker/Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
ports:
|
||||
- 9091:9000
|
||||
environment:
|
||||
ALLOW_SIGNUP: "false"
|
||||
|
||||
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
|
||||
# =====================================
|
||||
# Postgres Config
|
||||
POSTGRES_USER: mealie
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_SERVER: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: mealie
|
||||
|
||||
# =====================================
|
||||
# Web Concurrency
|
||||
WEB_GUNICORN: "false"
|
||||
WORKERS_PER_CORE: 0.5
|
||||
MAX_WORKERS: 1
|
||||
WEB_CONCURRENCY: 1
|
||||
# =====================================
|
||||
# Email Configuration
|
||||
# SMTP_HOST=
|
||||
# SMTP_PORT=587
|
||||
# SMTP_FROM_NAME=Mealie
|
||||
# SMTP_AUTH_STRATEGY=TLS # Options: 'TLS', 'SSL', 'NONE'
|
||||
# SMTP_FROM_EMAIL=
|
||||
# SMTP_USER=
|
||||
# SMTP_PASSWORD=
|
||||
|
||||
volumes:
|
||||
mealie-data:
|
||||
driver: local
|
||||
41
mealie/run.sh → docker/entry.sh
Executable file → Normal file
41
mealie/run.sh → docker/entry.sh
Executable file → Normal file
@@ -1,9 +1,9 @@
|
||||
# Start Backend API
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Get Reload Arg `run.sh reload` for dev server
|
||||
ARG1=${1:-production}
|
||||
# Strict Mode
|
||||
# set -e
|
||||
# IFS=$'\n\t'
|
||||
|
||||
# Get PUID/PGID
|
||||
PUID=${PUID:-911}
|
||||
@@ -22,7 +22,7 @@ change_user() {
|
||||
|
||||
echo "Switching to dedicated user"
|
||||
exec gosu $PUID "$BASH_SOURCE" "$@"
|
||||
elif [ "$(id -u)" = $PUID ]; then
|
||||
elif [ "$(id -u)" = $PUID ]; then
|
||||
echo "
|
||||
User uid: $PUID
|
||||
User gid: $PGID
|
||||
@@ -41,28 +41,15 @@ init() {
|
||||
poetry run python /app/mealie/db/init_db.py
|
||||
}
|
||||
|
||||
if [ "$ARG1" == "reload" ]; then
|
||||
echo "Hot Reload!"
|
||||
# change_user
|
||||
init
|
||||
GUNICORN_PORT=${API_PORT:-9000}
|
||||
|
||||
init
|
||||
|
||||
# Start API
|
||||
python /app/mealie/app.py
|
||||
# Start API
|
||||
hostip=`/sbin/ip route|awk '/default/ { print $3 }'`
|
||||
if [ "$WEB_GUNICORN" = 'true' ]; then
|
||||
echo "Starting Gunicorn"
|
||||
gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT --forwarded-allow-ips=$hostip -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload
|
||||
else
|
||||
echo "Production"
|
||||
|
||||
change_user
|
||||
|
||||
init
|
||||
|
||||
GUNICORN_PORT=${API_PORT:-9000}
|
||||
|
||||
# Start API
|
||||
|
||||
if [ $WEB_GUNICORN == 'true' ]; then
|
||||
echo "Starting Gunicorn"
|
||||
gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload
|
||||
else
|
||||
uvicorn mealie.app:app --host 0.0.0.0 --port $GUNICORN_PORT
|
||||
fi
|
||||
uvicorn mealie.app:app --host 0.0.0.0 --forwarded-allow-ips=$hostip --port $GUNICORN_PORT
|
||||
fi
|
||||
BIN
docs/docs/assets/img/sc1.jpg
Normal file
BIN
docs/docs/assets/img/sc1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/docs/assets/img/sc1half.png
Normal file
BIN
docs/docs/assets/img/sc1half.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/docs/assets/img/sc2.jpg
Normal file
BIN
docs/docs/assets/img/sc2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/docs/assets/img/sc2half.png
Normal file
BIN
docs/docs/assets/img/sc2half.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/docs/assets/img/sc3.jpg
Normal file
BIN
docs/docs/assets/img/sc3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/docs/assets/img/sc3half.png
Normal file
BIN
docs/docs/assets/img/sc3half.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -1,3 +1,3 @@
|
||||
## NOTICE:
|
||||
### NOTICE:
|
||||
|
||||
Release changelogs are now published on github releases. This file is kept for historical purposes.
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests
|
||||
Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests:
|
||||
Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `dev`.
|
||||
1. Fork the repo and create your branch from `mealie-next`.
|
||||
2. Checkout the Discord, the PRs page, or the Projects page to get an idea of what's already being worked on.
|
||||
3. If you're interested on working on major changes please get in touch on discord and coordinate with other developers. No sense in doubling up on work if someones already on it.
|
||||
3. If you're interested on working on major changes please get in touch on discord and coordinate with other developers. No sense in doubling up on work if someones already on it.
|
||||
4. Once you've got an idea of what changes you want to make, create a draft PR as soon as you can to let us know what you're working on and how we can help!
|
||||
5. If you've changed APIs, update the documentation.
|
||||
6. Issue that pull request!
|
||||
7. If you make changes to the dev branch reflect those changes in the active changelog to keep track of changes. Don't forget to add your name/handle/identifier!
|
||||
6. Run tests, including `make backend-all`. Note that the tests do not clean up after themselves and leave things in the database. So be sure to also run `make clean-data` and/or `make backend-clean` inbetween major testing rounds to be sure that you aren't testing on old data.
|
||||
6. Issue that pull request! First make a draft PR, make sure that the automated github tests all pass, then mark as ready for review.
|
||||
7. Be sure to add release notes to the pull request.
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
## Any contributions you make will be under the AGPL Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [AGPL License](https://choosealicense.com/licenses/agpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/hay-kot/mealie/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/hay-kot/mealie/issues/new); it's that easy!
|
||||
@@ -37,7 +38,7 @@ People *love* thorough bug reports. I'm not even kidding.
|
||||
|
||||
|
||||
## License
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
By contributing, you agree that your contributions will be licensed under its AGPL License.
|
||||
|
||||
## References
|
||||
This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md)
|
||||
|
||||
@@ -13,9 +13,13 @@ Prerequisites
|
||||
- Docker
|
||||
- Visual Studio Code
|
||||
|
||||
### Linux and MacOS
|
||||
First ensure that docker is running. Then when you clone the repo and open with VS Code you should see a popup asking you to reopen the project inside a development container. Click yes and it will build the development container and run the setup required to run both the backend API and the frontend webserver. This also pre-configures pre-commit hooks to ensure that the code is up to date before committing.
|
||||
|
||||
Checkout the makefile for all of the available commands.
|
||||
### Windows
|
||||
Make sure the VSCode Dev Containers extension is installed, then select "Dev Containers: Clone Repository in Container Volume..." in the command pallete (F1). Select your forked repo and choose the `mealie-next` branch, which contains the latest changes. This mounts your repository directly in WSL2, which [greatly improves the performance of the container](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-a-git-repository-or-github-pr-in-an-isolated-container-volume), and enables hot-reloading for the frontend. Running the container on a mounted volume may not work correctly on Windows due to WSL permission mapping issues.
|
||||
|
||||
[Checkout the makefile reference](#make-file-reference) for all of the available commands.
|
||||
|
||||
!!! tip
|
||||
For slow terminal checkout the solution in this [GitHub Issue](https://github.com/microsoft/vscode/issues/133215)
|
||||
@@ -62,9 +66,13 @@ Once the prerequisites are installed you can cd into the project base directory
|
||||
|
||||
Before you start the server you MUST copy the `template.env` and `frontend/template.env` files to their respective locations with the name `.env` and `frontend/.env` respectively. The application will-not run without these files.
|
||||
|
||||
### Starting The Server
|
||||
## Postgres
|
||||
- Whether using a container or manual install, you need to set up your own postgres dev server. The database, username, password, etc should match the `POSTGRES_*` options located in the `.env` file.
|
||||
- Install psycog2 with `poetry install -E pgsql` (in the main `mealie` directory, *not* `frontend`)
|
||||
|
||||
Once that is complete you're ready to start the servers. You'll need two shells open, One for the server and one for the frontend.
|
||||
## Starting The Server
|
||||
|
||||
Now you're ready to start the servers. You'll need two shells open, One for the server and one for the frontend.
|
||||
|
||||
=== "Linux / macOS"
|
||||
|
||||
@@ -114,3 +122,24 @@ docker-dev 🐳 Build and Start Docker Development Stack (currently no
|
||||
docker-prod 🐳 Build and Start Docker Production Stack
|
||||
|
||||
```
|
||||
## Internationalization
|
||||
### Frontend
|
||||
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/lang/messages](https://github.com/hay-kot/mealie/tree/mealie-next/frontend/lang/messages).
|
||||
### Backend
|
||||
Translations are stored in json format located in [mealie/lang/messages](https://github.com/hay-kot/mealie/tree/mealie-next/mealie/lang/messages).
|
||||
|
||||
### Quick frontend localization with VS Code
|
||||
[i18n Ally for VScode](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) is helpful for generating new strings to translate using Code Actions. It also has a nice feature, which shows translations in-place when editing code.
|
||||
|
||||
A few settings must be tweaked to make the most of its features. Some settings are stored on project level, but most of them have to be set manually in your workspace or user settings.\
|
||||
We've found that the following settings work best:
|
||||
|
||||
```
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.extract.autoDetect": true,
|
||||
"i18n-ally.dirStructure": "auto",
|
||||
"i18n-ally.extract.targetPickingStrategy": "global-previous",
|
||||
"i18n-ally.displayLanguage": "en-US",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing with Translations
|
||||
Translations can be a great way **for non-coders** to contribute to project.
|
||||
We use **[Crowdin](https://crowdin.com/project/mealie)** to allow several contributors to work on translating Mealie.
|
||||
We use **[Crowdin](https://crowdin.com/project/mealie)** to allow several contributors to work on translating Mealie.
|
||||
You can simply help by voting for your preferred translations, or even by completely translating Mealie into a new language.
|
||||
|
||||
Translations are regularly pulled from Crowdin and included in each new release.
|
||||
@@ -8,14 +8,9 @@ Translations are regularly pulled from Crowdin and included in each new release.
|
||||
Please use Crowdin as much as possible if you have any question/issue regarding a particular string. You can take a look at [Crowdin Knowledge base](https://support.crowdin.com/for-volunteer-translators/) if you want to know more about how to use this tool.
|
||||
|
||||
## My language is missing in Mealie
|
||||
Once your language is translated on Crowdin, we need to manually add it in Mealie. If you believe your language is ready for use, please create an issue on GitHub.
|
||||
Once your language is translated on Crowdin, we need to manually add it in Mealie. If you believe your language is ready for use, please create an issue on GitHub.
|
||||
|
||||
## I can't find a particular text in Crowdin
|
||||
There can be several reasons:
|
||||
- The text you're looking for is outdated: someone has already changed it or it will be removed/changed in the next release.
|
||||
- It is possible some texts are not translatable (yet) for technical reasons. If you spot one, please reach out to us on [Discord](https://discord.gg/QuStdQGSGK) or raise an issue on GitHub.
|
||||
|
||||
## Technical information
|
||||
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/src/locales/messages](https://github.com/hay-kot/mealie/tree/master/frontend/src/locales/messages).
|
||||
|
||||
[i18n Ally for VScode](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) is helpful for generating new strings to translate. It also has a nice feature, which shows translations in-place when editing code.
|
||||
@@ -33,7 +33,7 @@ function import_from_file () {
|
||||
}
|
||||
|
||||
input="list"
|
||||
mail="changeme@email.com"
|
||||
mail="changeme@example.com"
|
||||
password="MyPassword"
|
||||
mealie_url=http://localhost:9000
|
||||
|
||||
@@ -85,7 +85,7 @@ def import_from_file(input_file, token, mealie_url):
|
||||
print(response.text)
|
||||
|
||||
input_file="list"
|
||||
mail="changeme@email.com"
|
||||
mail="changeme@example.com"
|
||||
password="MyPassword"
|
||||
mealie_url="http://localhost:9000"
|
||||
|
||||
|
||||
@@ -1,25 +1,77 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
In a lot of ways, Home Assistant is why this project exists! Since Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard.
|
||||
|
||||
### Get Todays Meal in Lovelace
|
||||
Starting in v0.4.1 you are now able to use the uri `/api/meal-plans/today/image?group_name=Home` to directly access the image to todays meal. This makes it incredibly easy to include the image into your Home Assistant Dashboard using the picture entity.
|
||||
### Display Today's Meal in Lovelace
|
||||
|
||||
Here's an example where `sensor.mealie_todays_meal` is pulling in the meal-plan name and I'm using the url to get the image.
|
||||
You can use the Mealie API to get access to meal plans in Home Assistant like in the image below.
|
||||
|
||||

|
||||
|
||||
Steps:
|
||||
|
||||
#### 1. Get your API Token
|
||||
|
||||
Create an API token from Mealie's User Settings page (https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation)
|
||||
|
||||
#### 2. Create Home Assistant Sensors
|
||||
|
||||
Create REST sensors in home assistant to get the details of today's meal.
|
||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retreive the image for the meal.
|
||||
|
||||
Make sure the url and port (`http://mealie:9000` ) matches your installation's address and _API_ port.
|
||||
|
||||
```yaml
|
||||
- platform: rest
|
||||
resource: "http://mealie:9000/api/groups/mealplans/today"
|
||||
method: GET
|
||||
name: Mealie todays meal
|
||||
headers:
|
||||
Authorization: Bearer <<API_TOKEN>>
|
||||
value_template: "{{ value_json[0].recipe.name }}"
|
||||
force_update: true
|
||||
scan_interval: 30
|
||||
```
|
||||
|
||||
```yaml
|
||||
- platform: rest
|
||||
resource: "http://mealie:9000/api/groups/mealplans/today"
|
||||
method: GET
|
||||
name: Mealie todays meal ID
|
||||
headers:
|
||||
Authorization: Bearer <<API_TOKEN>>
|
||||
value_template: "{{ value_json[0].recipe.id }}"
|
||||
force_update: true
|
||||
scan_interval: 30
|
||||
```
|
||||
|
||||
#### 3. Create a Camera Entity
|
||||
|
||||
We will create a camera entity to display the image of today's meal in Lovelace.
|
||||
|
||||
In Home Assistant's `Integrations` page, create a new `generic camera` entity.
|
||||
|
||||
In the still image url field put in:
|
||||
`http://mealie:9000/api/media/recipes/{{states('sensor.mealie_todays_meal_id')}}/images/min-original.webp`
|
||||
|
||||
Under the entity page for the new camera, rename it.
|
||||
e.g. `camera.mealie_todays_meal_image`
|
||||
|
||||
#### 4. Create a Lovelace Card
|
||||
|
||||
Create a picture entity card and set the entity to `mealie_todays_meal` and the camera entity to `camera.mealie_todays_meal_image` or set in the yaml directly.
|
||||
|
||||
```yaml
|
||||
show_state: true
|
||||
show_name: true
|
||||
camera_view: auto
|
||||
type: picture-entity
|
||||
entity: sensor.mealie_todays_meal
|
||||
name: Dinner Tonight
|
||||
show_state: true
|
||||
show_name: true
|
||||
image: 'http://localhost:9000/api/meal-plans/today/image?group_name=Home'
|
||||
style:
|
||||
.: |
|
||||
camera_image: camera.mealie_todays_meal_image
|
||||
card_mod:
|
||||
style: |
|
||||
ha-card {
|
||||
max-height: 300px !important;
|
||||
overflow: hidden;
|
||||
@@ -29,19 +81,5 @@ style:
|
||||
}
|
||||
```
|
||||
|
||||
The sensor that gets the name of the meal can be achieved using the following REST sensor in Home Assistant
|
||||
```yaml
|
||||
sensor:
|
||||
- platform: rest
|
||||
resource: 'http://localhost:9000/api/meal-plans/today'
|
||||
method: GET
|
||||
name: Mealie todays meal
|
||||
headers:
|
||||
Authorization: Bearer MySuperSecretBearerCode
|
||||
value_template: "{{ value_json.name }}"
|
||||
```
|
||||
The Bearer token can be created from the User Settings page (https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation)
|
||||
|
||||
|
||||
!!! tip
|
||||
Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This includes and [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS.
|
||||
Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This requires an [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!-- prettier-ignore -->
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
You can use bookmarklets to generate a bookmark that will take your current location, and open a new tab that will try to import that URL into Mealie.
|
||||
|
||||
You can use a [bookmarklet generator site](https://caiorss.github.io/bookmarklet-maker/) and the code below to generate a bookmark for your site. Just change the `http://localhost:8080` to your sites web address and follow the instructions.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! note
|
||||
There is no trailing `/` at the end of the url!
|
||||
|
||||
```js
|
||||
var url = document.URL;
|
||||
var mealie = "http://localhost:8080";
|
||||
var dest = mealie + "/recipe/create/url?recipe_import_url=" + url;
|
||||
window.open(dest, "_blank");
|
||||
```
|
||||
@@ -5,20 +5,35 @@
|
||||
|
||||

|
||||
|
||||
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/hay-kot/mealie/issues/103) for interested users. This is a useful utility for iOS users who browse for recipes in their web browser from their devices. Recent updates to Mealie has made this original shortcut break. Reddit user [BooNooBooNooB](https://www.reddit.com/user/BooNooBooNooB/) has helped to create a new working version.
|
||||
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/hay-kot/mealie/issues/103) for interested users.
|
||||
This original method broke after the transition to version 1.X and an issue was raised on [Github](https://github.com/hay-kot/mealie/issues/2092) GitHub user [Zippyy](https://github.com/zippyy) has helped to create a working shortcut for version 1.X.
|
||||
|
||||
This is a useful utility for iOS users who browse for recipes in their web browser from their devices.
|
||||
|
||||
Don't know what an iOS shortcut is? Neither did I! Experienced iOS users may already be familiar with this utility but for the uninitiated, here is the official Apple explanation:
|
||||
|
||||
> A shortcut is a quick way to get one or more tasks done with your apps. The Shortcuts app lets you create your own shortcuts with multiple steps. For example, build a “Surf Time” shortcut that grabs the surf report, gives an ETA to the beach, and launches your surf music playlist.
|
||||
|
||||
Basically it is a visual scripting language that lets a user build an automation in a guided fashion. The automation can be [shared with anyone](https://www.icloud.com/shortcuts/4c40fcc6f39549f9a189995a449cd44f) but if it is a user creation, you'll have to jump through a few hoops to make an untrusted automation work on your device.
|
||||
Basically it is a visual scripting language that lets a user build an automation in a guided fashion. The automation can be [shared with anyone](https://www.icloud.com/shortcuts/cc568d1615bc4f998789f85d1ef74846) but if it is a user creation, you'll have to jump through a few hoops to make an untrusted automation work on your device.
|
||||
|
||||
You need to replace `username` and `password` with the login information for your mealie instance.
|
||||
This guide assumes that you already know how to [generate API tokens](https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation) for your user that intends to use an iOS shortcut.
|
||||
|
||||

|
||||
First, click the [link](https://www.icloud.com/shortcuts/cc568d1615bc4f998789f85d1ef74846) and begin the setup of the shortcut.
|
||||
|
||||
Then, you need to put in your mealie domain. The API port of `:9000` is not needed when putting your domain in the text field.
|
||||
|
||||

|
||||

|
||||
|
||||
Next, you need to replace `url` and `port` with the information for your mealie instance.
|
||||
|
||||
If you have a TLD that you use, put that here with no port. If you just run local, Then, you need to put in your mealie instance IP and the port of `9926`.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
Finally, you need to replace the word `keyhere` with your API token. Keep the word `Bearer`!!!
|
||||
|
||||
|
||||

|
||||
|
||||
You should now be able to share a website to the shortcut and have mealie grab all the necessary information!
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
## Getting a Token
|
||||
|
||||
Mealie supports long-live api tokens in the user frontend. See [user settings page](../users-groups/user-settings.md)
|
||||
|
||||
Mealie supports long-live api tokens in the user frontend. These can be created on the `/user/profile/api-tokens` page.
|
||||
|
||||
## Key Components
|
||||
|
||||
### Exploring Your Local API
|
||||
On your local installation you can access interactive API documentation that provides `curl` examples and expected results. This allows you to easily test and interact with your API to identify places to include your own functionality. You can visit the documentation at `http://mealie.yourdomain.com/docs` or see the example at the [Demo Site](https://demo.mealie.io/docs).
|
||||
On your local installation you can access interactive API documentation that provides `curl` examples and expected results. This allows you to easily test and interact with your API to identify places to include your own functionality. You can visit the documentation at `http://<your-mealie-site>/docs` or see the example at the [Demo Site](https://demo.mealie.io/docs).
|
||||
|
||||
### Extras
|
||||
#### Recipe Extras
|
||||
@@ -73,6 +72,33 @@ This filter will find all recipes created on or after a particular date: <br>
|
||||
This filter will find all units that have `useAbbreviation` disabled: <br>
|
||||
`useAbbreviation = false`
|
||||
|
||||
This filter will find all foods that are not named "carrot": <br>
|
||||
`name <> "carrot"`
|
||||
|
||||
##### 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`).
|
||||
|
||||
Here is an example of a filter that returns all recipes where the "last made" value is not null: <br>
|
||||
`lastMade IS NOT NULL`
|
||||
|
||||
This filter will find all recipes that don't start with the word "Test": <br>
|
||||
`name NOT LIKE "Test%"`
|
||||
|
||||
> **_NOTE:_** for more information on this, [check out the SQL "LIKE" operator](https://www.w3schools.com/sql/sql_like.asp)
|
||||
|
||||
This filter will find all recipes that have particular slugs: <br>
|
||||
`slug IN ["pasta-fagioli", "delicious-ramen"]`
|
||||
|
||||
##### 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>
|
||||
`user.username = "SousChef20220320"`
|
||||
|
||||
This timeline event filter will return all timeline events for recipes that were created after a particular date: <br>
|
||||
`recipe.createdAt >= "2023-02-25"`
|
||||
|
||||
This recipe filter will return all recipes that contains a particular set of tags: <br>
|
||||
`tags.name CONTAINS ALL ["Easy", "Cajun"]`
|
||||
|
||||
##### Compound Filters
|
||||
You can combine multiple filter statements using logical operators (`AND`, `OR`).
|
||||
|
||||
@@ -90,3 +116,31 @@ You can have multiple filter groups combined by logical operators. You can defin
|
||||
|
||||
Here's a filter that will find all recipes updated between two particular times, but exclude the "Pasta Fagioli" recipe: <br>
|
||||
`(updatedAt > "2022-07-17T15:47:00Z" AND updatedAt < "2022-07-17T15:50:00Z") AND name <> "Pasta Fagioli"`
|
||||
|
||||
#### Advanced Ordering
|
||||
Pagination supports `orderBy`, `orderByNullPosition`, and `orderDirection` params to change how you want your query results to be ordered. These can be fine-tuned for more advanced use-cases.
|
||||
|
||||
##### Order By
|
||||
The pagination `orderBy` attribute allows you to sort your query results by a particular attribute. Sometimes, however, [you may want to sort by more than one attribute](https://www.w3schools.com/sql/sql_orderby.asp). This can be achieved by passing a comma-separated string to the `orderBy` parameter. For instance, if you want to sort recipes by their last made datetime, then by their created datetime, you can pass the following `orderBy` string: <br>
|
||||
`lastMade, createdAt`
|
||||
|
||||
Similar to the standard SQL `ORDER BY` logic, your attribute orders will be applied sequentially. In the above example, *first* recipes will be sorted by `lastMade`, *then* any recipes with an identical `lastMade` value are sorted by `createdAt`. In addition, standard SQL rules apply when handling results with null values (such as when joining related tables). You can apply the `NULLS FIRST` and `NULLS LAST` SQL expressions by setting the `orderByNullPosition` to "first" or "last". If left empty, the default SQL behavior is applied, [which is different depending on which database you're using](https://learnsql.com/blog/how-to-order-rows-with-nulls/).
|
||||
|
||||
##### Order Direction
|
||||
The query will be ordered in ascending or descending order, depending on what you pass to the pagination `orderDirection` param. You can either specify "asc" or "desc".
|
||||
|
||||
When sorting by multiple attributes, if you *also* want one or more of those sorts to be different directions, you can specify them with a colon. For instance, if, like our previous example, say you want to sort by `lastMade` and `createdAt`. However, this time, you want to sort by `lastMade` ascending, but `createdAt` descending. You could pass this `orderBy` string: <br>
|
||||
`lastMade:asc, createdAt:desc`
|
||||
|
||||
In the above example, whatever you pass to `orderDirection` will be ignored. If, however, you only specify the direction on one attribute, all other attributes will use the `orderDirection` value.
|
||||
|
||||
Consider this `orderBy` string: <br>
|
||||
`lastMade:asc, createdAt, slug`
|
||||
|
||||
And this `orderDirection` value: <br>
|
||||
`desc`
|
||||
|
||||
This will result in a recipe query where all recipes are sorted by `lastMade` ascending, then `createdAt` descending, and finally `slug` descending.
|
||||
|
||||
Similar to query filters, when querying tables with relationships, you can order by properties on related tables. For instance, if you want to query all foods with labels, sorted by label name, you could use this `orderBy` value: <br>
|
||||
`label.name`
|
||||
|
||||
@@ -1,35 +1,62 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## How do I enable "smart" ingredient handling?
|
||||
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And making shopping lists from reciepes that have shared ingredients can yield multiple lines of the same ingredient. **But** mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
### Set up your Foods and Units
|
||||
Do the following just **once**. Doing this applies to your whole group, so be careful.
|
||||
|
||||
1. Click on your name in the upper left corner to get to your settings
|
||||
2. In the bottom right, select `Manage Data`
|
||||
3. In the Management page, make sure that a little orange button says `Foods`
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
5. Click the little orange `Foods` button and now choose `Units`.
|
||||
6. Click `Seed` and choose your language. You should end up with a list of units (e.g. `tablespoon`)
|
||||
|
||||
Initial seeding of Units is pretty complete, but there are many Foods in the world. You'll probably find that you need to add Foods to the database during parsing for the first several recipes. Once you have a well-populated Food database, there are API routes to parse ingredients automatically in bulk. But this is not a good idea without a very complete set of Foods.
|
||||
|
||||
### Set up Recipes to use Foods and Units
|
||||
Do the following for each recipe you want to intelligently handle ingredients.
|
||||
|
||||
1. Go to a recipe
|
||||
2. Click the Edit button/icon
|
||||
3. Click the Recipe Settings gear and deselect `Disable Ingredient Amounts`
|
||||
4. Save
|
||||
5. The ingredients should now look a little weird (`1 1 cup broth` and so on)
|
||||
6. Click the Edit button/icon again
|
||||
7. Scroll to the ingredients and you should see new fields for Amount, Unit, Food, and Note. The Note in particular will contain the original text of the Recipe.
|
||||
8. Click `Parse` and you will be taken to the ingredient parsing page.
|
||||
9. Choose your parser. the `Natural Language Parser` works very well, but you can also use the `Brute Parser`.
|
||||
10. Click `Parse All` and your ingredients should be separated out into Units and Foods based on your seeding in Step 1 above.
|
||||
11. For ingredients where the Unit or Food were not found, you can click a button to accept an automatically suggested Food to add to the database. Or manually enter the Unit/Food and hit `Enter` (or click `Create`) to add it to the database
|
||||
12. When done, click `Save All` and you will be taken back to the recipe. Now the Unit and Food fields of the recipe should be filled out.
|
||||
|
||||
Scaling up this recipe or adding it to a Shopping List will now smartly take care of ingredient amounts and duplicate combinations.
|
||||
|
||||
## Is it Safe to Upgrade Mealie?
|
||||
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Not that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
|
||||
**Links**
|
||||
|
||||
- [Migration From v0.5.x](./migrating-to-mealie-v1.md)
|
||||
|
||||
## How can I change the theme?
|
||||
|
||||
You can change the theme by settings the environment variables on the frontend container.
|
||||
|
||||
Links:
|
||||
|
||||
- [Frontend Theme](./installation/frontend-config#themeing)
|
||||
- [Frontend Theme](../installation/frontend-config#themeing)
|
||||
|
||||
## How can I change the language?
|
||||
|
||||
Languages need to be set on the frontend and backend containers as ENV variables.
|
||||
|
||||
Links
|
||||
|
||||
- [Frontend Config](./installation/frontend-config/)
|
||||
- [Backend Config](./installation/backend-config/)
|
||||
- [Frontend Config](../installation/frontend-config/)
|
||||
- [Backend Config](../installation/backend-config/)
|
||||
|
||||
## How can I change the Login Session Timeout?
|
||||
|
||||
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
|
||||
|
||||
- [Backend Config](./installation/backend-config/)
|
||||
- [Backend Config](../installation/backend-config/)
|
||||
|
||||
## Can I serve Mealie on a subpath?
|
||||
|
||||
@@ -39,12 +66,88 @@ No. Due to limitations from the Javascript Framework, mealie doesn't support ser
|
||||
|
||||
Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover updating and upgrading your system with this configuration is unsupported and will likely require manual interventions. If you insist on installing Mealie on your local machine, you can use the links below to help guide your path.
|
||||
|
||||
- [Advanced Installation](./installation/advanced/)
|
||||
- [Advanced Installation](../installation/advanced/)
|
||||
|
||||
## How I can attach an Image or Video to a Recipe?
|
||||
## What is fuzzy search and how do I use it?
|
||||
Mealie can use fuzzy search, which is robust to minor typos. For example, searching for "brocolli" will still find your recipe for "broccoli soup". But fuzzy search is only functional on a Postgres database backend. To enable fuzzy search you will need to migrate to Postgres:
|
||||
|
||||
1. Backup your database and download the .zip file (same as when [migrating](./migrating-to-mealie-v1.md))
|
||||
2. Set up a [Postgres](./installation/postgres.md) instance of Mealie
|
||||
3. Upload the backup .zip and click to apply it (as as migration)
|
||||
|
||||
## How i can attach an image or video to a Recipe?
|
||||
|
||||
Yes. Mealie's Recipe Steps and other fields support the markdown syntax and therefor supports images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, youtube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
|
||||
|
||||
```html
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/nAUwKeO93bY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
```
|
||||
|
||||
## How can I unlock my account?
|
||||
|
||||
If your account has been locked by bad password attempts, you can use an administrator account to unlock another account. Alternatively you can unlock all account via a scripts within the container.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie-next bash
|
||||
|
||||
python /app/mealie/scripts/reset_locked_users.py
|
||||
```
|
||||
|
||||
## How can I change my password
|
||||
|
||||
You can change your password by going to the user profile page and clicking the "Change Password" button. Alternatively you can use the following script to change your password via the CLI if you are locked out of your account.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie-next bash
|
||||
|
||||
python /app/mealie/scripts/change_password.py
|
||||
```
|
||||
|
||||
## How do private groups and recipes work?
|
||||
|
||||
Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly.
|
||||
|
||||
- Private links that are generated using th`Share` button bypass all group and recipe permissions.
|
||||
- Private groups block all access to recipes, including those that are public. Expect as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
r1: Request Access
|
||||
p1: Using Private Link?
|
||||
p2: Is Group Private?
|
||||
p3: Is Recipe Private?
|
||||
s1: Deny Access
|
||||
n1: Allow Access
|
||||
|
||||
|
||||
r1 --> p1
|
||||
p1 --> p2: No
|
||||
p1 --> n1: Yes
|
||||
|
||||
p2 --> s1: Yes
|
||||
p2 --> p3: No
|
||||
|
||||
p3 --> s1: Yes
|
||||
p3 --> n1: No
|
||||
```
|
||||
|
||||
## Can I use fail2ban with mealie?
|
||||
Yes, mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that, due to restrictions in docker, IP address forwarding only works on linux.
|
||||
|
||||
Your fail2ban usage should look like the following:
|
||||
```
|
||||
Use datepattern : %d-%b-%y %H:%M:%S : Day-MON-Year2 24hour:Minute:Second
|
||||
Use failregex line : ^ERROR:\s+Incorrect username or password from <HOST>
|
||||
```
|
||||
|
||||
## Why An API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
|
||||
## Why a Database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in controls of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
|
||||
- **Developer Experience:** Without a database a lot of the work to maintain your data is taken on by the developer instead of a battle tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor.
|
||||
|
||||
[Demo](https://demo.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||
[Creation Demo](https://demo.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||
|
||||
### Importing Recipes
|
||||
|
||||
@@ -22,7 +22,7 @@ Mealie supports importing recipes from a few other sources besides websites. Cur
|
||||
You can access these options on your installation at the `/group/migrations` page on your installation. If you'd like to see another source added, feel free to request so on Github.
|
||||
|
||||
|
||||
[Demo](https://demo.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
[Import Demo](https://demo.mealie.io/group/migrations){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
### Organizing Recipes
|
||||
@@ -34,13 +34,13 @@ Mealie has a robust and flexible recipe organization system with a few different
|
||||
|
||||
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
|
||||
|
||||
[Demo](https://demo.mealie.io/recipes/categories){ .md-button .md-button--primary }
|
||||
[Categories Demo](https://demo.mealie.io/recipes/categories){ .md-button .md-button--primary }
|
||||
|
||||
#### Tags
|
||||
|
||||
Tags, are nearly identical to categories in function but play a secondary role in some cases. As such, we recommend that you use tags freely to help you organize your recipes by more specific topics. For example, if a recipe can be frozen or is a great left-over meal, you could assign the tags **frozen** and **left-over** and easily filter for those at a later time.
|
||||
|
||||
[Demo](https://demo.mealie.io/recipes/tags){ .md-button .md-button--primary }
|
||||
[Tags Demo](https://demo.mealie.io/recipes/tags){ .md-button .md-button--primary }
|
||||
|
||||
#### Tools
|
||||
|
||||
@@ -48,7 +48,7 @@ Tools, are another way that some users like to organize their recipes. If a reci
|
||||
|
||||
Each of the above organizers can be filtered in searches, and have their own pages where you can view all the recipes that are associated with those organizers.
|
||||
|
||||
[Demo](https://demo.mealie.io/recipes/tools){ .md-button .md-button--primary }
|
||||
[Tools Demo](https://demo.mealie.io/recipes/tools){ .md-button .md-button--primary }
|
||||
|
||||
#### Cookbooks
|
||||
|
||||
@@ -60,7 +60,7 @@ Mealie also has the concept of cookbooks. These can be created inside of a group
|
||||
- Pasta Sides: Recipes that have both the **Side** category and the **Pasta** tag
|
||||
- Dessert Breads: Recipes that have both the **Bread** category and the **Dessert** tag
|
||||
|
||||
[Demo](https://demo.mealie.io/group/cookbooks){ .md-button .md-button--primary }
|
||||
[Cookbooks Demo](https://demo.mealie.io/group/cookbooks){ .md-button .md-button--primary }
|
||||
|
||||
## Meal Planning
|
||||
|
||||
@@ -69,13 +69,13 @@ Mealie uses a calendar like view to help you plan your meals. It shows you the p
|
||||
!!! tip
|
||||
You can also add a "Note" type entry to your meal-plan when you want to include something that might not have a specific recipes. This is great for leftovers, or for ordering out.
|
||||
|
||||
[Demo](https://demo.mealie.io/group/mealplan/planner){ .md-button .md-button--primary }
|
||||
[Mealplanner Demo](https://demo.mealie.io/group/mealplan/planner/view){ .md-button .md-button--primary }
|
||||
|
||||
### Planner Rules
|
||||
|
||||
The meal planner has the concept of plan rules. These offer a flexible way to use your organizers to customize how a random recipe is inserted into your meal plan. You can set rules to restrict the pool of recipes based on the Tags and/or Categories of a recipe. Additionally, since meal plans have a Breakfast, Lunch, Dinner, and Snack labels you can specifically set a rule to be active for a **specific meal type** or even a **specific day of the week.**
|
||||
|
||||
[Demo](https://demo.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
|
||||
[Planner Settings Demo](https://demo.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
|
||||
|
||||
## Shopping Lists
|
||||
|
||||
@@ -85,7 +85,7 @@ The shopping lists feature is a great way to keep track of what you need to buy
|
||||
At this time there isn't a tight integration between meal-plans and shopping lists, however it's something we have planned for the future.
|
||||
|
||||
|
||||
[Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
|
||||
[Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
## Data Management
|
||||
@@ -107,7 +107,7 @@ Managing a robust collection of recipes inevitable requires a lot of data. Meali
|
||||
- Merge Units into a single unit entry
|
||||
- Export as JSON
|
||||
|
||||
[Demo](https://demo.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
[Data Management Demo](https://demo.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
|
||||
## Server Administration
|
||||
|
||||
@@ -115,20 +115,20 @@ Managing a robust collection of recipes inevitable requires a lot of data. Meali
|
||||
|
||||
The site settings page contains general information about your installation like the application version, some configuration details, and some utilities to help you confirm your installation is working as expected. For example, you can use the Email Configuration section to validate that your email credentials are setup correctly and that the email service is working as expected. Additionally, there is a docker-volume utility that will confirm your volumes are configured and shared correctly between the front and backend of the application.
|
||||
|
||||
[Demo](https://demo.mealie.io/admin/site-settings){ .md-button .md-button--primary }
|
||||
[Settings Demo](https://demo.mealie.io/admin/site-settings){ .md-button .md-button--primary }
|
||||
|
||||
### Users and Group
|
||||
|
||||
There is a small management area for users and groups that allows you to create, edit, and delete users and groups.
|
||||
|
||||
[Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
[Users Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
|
||||
### Backups
|
||||
|
||||
The backups page provides a full system backup of your installation including all assets and images related to recipes. These are archived into a zip file and stored on the server but can also be downloaded through the UI. Due to some issues in the past Mealie no longer performs automatic backups, **it is advised that during setup you also setup a backup strategy to ensure your data is not lost.**
|
||||
|
||||
|
||||
[Demo](https://demo.mealie.io/admin/backups){ .md-button .md-button--primary }
|
||||
[Backups Demo](https://demo.mealie.io/admin/backups){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| DEFAULT_EMAIL | changeme@email.com | The default username for the superuser |
|
||||
| DEFAULT_EMAIL | changeme@example.com | The default username for the superuser |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
@@ -17,7 +17,6 @@
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
| ALLOW_SIGNUP | true | Allow user sign-up without token (should match frontend env) |
|
||||
|
||||
|
||||
### Security
|
||||
|
||||
| Variables | Default | Description |
|
||||
@@ -36,7 +35,6 @@
|
||||
| POSTGRES_PORT | 5432 | Postgres database port |
|
||||
| POSTGRES_DB | mealie | Postgres database name |
|
||||
|
||||
|
||||
### Email
|
||||
|
||||
| Variables | Default | Description |
|
||||
@@ -50,6 +48,7 @@
|
||||
| SMTP_PASSWORD | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
|
||||
### Webworker
|
||||
|
||||
Changing the webworker settings may cause unforeseen memory leak issues with Mealie. It's best to leave these at the defaults unless you begin to experience issues with multiple users. Exercise caution when changing these settings
|
||||
|
||||
| Variables | Default | Description |
|
||||
@@ -59,15 +58,20 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
| MAX_WORKERS | 1 | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
||||
| WEB_CONCURRENCY | 1 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
||||
|
||||
|
||||
### LDAP
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
|
||||
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| LDAP_BIND_TEMPLATE | None | Templated DN for users, `{}` will be replaced with the username (e.g. `cn={},dc=example,dc=com`, `{}@example.com`) |
|
||||
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
||||
| Variables | Default | Description |
|
||||
| -------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
|
||||
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server |
|
||||
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
|
||||
| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used |
|
||||
| LDAP_QUERY_PASSWORD | None | Optional password for the bind user used in LDAP_QUERY_BIND |
|
||||
| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
||||
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
|
||||
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
|
||||
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Frontend Configuration
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### General
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------- | :--------------------: | ------------------------- |
|
||||
| API_URL | http://mealie-api:9000 | URL to proxy API requests |
|
||||
|
||||
### Themeing
|
||||
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------------- | :-----: | --------------------------- |
|
||||
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
||||
| THEME_DARK_PRIMARY | #E58325 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ACCENT | #007A99 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SECONDARY | #973542 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SUCCESS | #43A047 | Dark Theme Config Variable |
|
||||
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
|
||||
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
|
||||
@@ -7,6 +7,7 @@ To install Mealie on your server there are a few steps for proper configuration.
|
||||
|
||||
- [SQLite docker-compose](./sqlite.md)
|
||||
- [Postgres docker-compose](./postgres.md)
|
||||
- [Single container docker-compose](./single-container.md)
|
||||
|
||||
## Pre-work
|
||||
|
||||
@@ -25,9 +26,21 @@ To deploy mealie on your local network it is highly recommended to use docker to
|
||||
!!! warning "32bit Support"
|
||||
Due to a build dependency limitation, Mealie is not supported on 32bit ARM systems. If you're running into this limitation on a newer Raspberry Pi, please consider upgrading to a 64bit operating system on the Raspberry Pi.
|
||||
|
||||
## Migrating From over V1 Versions
|
||||
|
||||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers *or* the omni image) to the new nightly, there are a few things you need to do:
|
||||
|
||||
1. Take a backup just in case!
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:nightly`
|
||||
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
|
||||
|
||||
For an example of what these changes look like, see the new [SQLite](./sqlite.md) or [PostgreSQL](./postgres.md) docker-compose examples. The container swap should be seemless, at least that's our hope!
|
||||
|
||||
## Step 1: Deployment Type
|
||||
SQLite is a popular, open source, self-contained, zero-configuration database that is the ideal choice for Mealie when you have 1-20 Users and your concurrent write operations will be some-what limited. If you need to support many concurrent users, you may want to consider a more robust database such as PostgreSQL.
|
||||
SQLite is a popular, open source, self-contained, zero-configuration database that is the ideal choice for Mealie when you have 1-20 Users and your concurrent write operations will be some-what limited.
|
||||
|
||||
PostgreSQL might be considered if you need to support many concurrent users. In addition, some features are only enabled on PostgreSQL, such as fuzzy search.
|
||||
|
||||
You can find the relevant ready to use docker-compose files for supported installations at the links below.
|
||||
|
||||
@@ -40,21 +53,19 @@ The following steps were tested on a Ubuntu 20.04 server, but should work for mo
|
||||
|
||||
|
||||
1. SSH into your server and navigate to the home directory of the user you want to run Mealie as. If that is your current user, you can use `cd ~` to ensure you're in the right directory.
|
||||
2. Create a directory called `docker` and navigate into it. `mkdir docker && cd docker`
|
||||
3. Do the same for mealie `mkdir mealie && cd mealie`
|
||||
4. Create a docker-compose.yaml file in the mealie directory. `touch docker-compose.yaml`
|
||||
5. Use the text editor or your choice to edit the file and copy the contents of the docker-compose template for the deployment type you want to use. `nano docker-compose.yaml` or `vi docker-compose.yaml`
|
||||
2. Create a directory called `docker` and navigate into it: `mkdir docker && cd docker` (this is optional, if you organizer your docker installs separate from everything else)
|
||||
3. Do the same for mealie: `mkdir mealie && cd mealie`
|
||||
4. Create a docker-compose.yaml file in the mealie directory: `touch docker-compose.yaml`
|
||||
5. Use the text editor or your choice to edit the file and copy the contents of the docker-compose template for the deployment type you want to use: `nano docker-compose.yaml` or `vi docker-compose.yaml`
|
||||
|
||||
|
||||
## Step 2: Customizing The `docker-compose.yaml` files.
|
||||
After you've decided setup the files it's important to set a few ENV variables to ensure that you can use all the features of Mealie. I recommend that you verify and check that:
|
||||
|
||||
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
||||
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc)
|
||||
- [x] Verified the port mapped on the `mealie-frontend` container is an open port on your server (Default: 9925)
|
||||
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc). You can setup a [google app password](https://support.google.com/accounts/answer/185833?hl=en) if you want to send email via gmail.
|
||||
- [x] You've set the [`BASE_URL`](./backend-config.md#general) variable.
|
||||
- [x] You've set the `DEFAULT_EMAIL` and `DEFAULT_GROUP` variable.
|
||||
- [x] Make any theme changes on the frontend container. [See Frontend Config](./frontend-config.md#themeing)
|
||||
|
||||
## Step 3: Startup
|
||||
After you've configured your database, and updated the `docker-compose.yaml` files, you can start Mealie by running the following command in the directory where you've added your `docker-compose.yaml`.
|
||||
@@ -65,9 +76,13 @@ $ docker-compose up -d
|
||||
|
||||
You should see the containers start up without error. You should now be able to access the Mealie frontend at [http://localhost:9925](http://localhost:9925).
|
||||
|
||||
!!! warning "Default Username"
|
||||
|
||||
Note that the default username (below) has been changed from previous versions
|
||||
|
||||
!!! tip "Default Credentials"
|
||||
|
||||
**Username:** changeme@email.com
|
||||
**Username:** changeme@example.com
|
||||
|
||||
**Password:** MyPassword
|
||||
|
||||
@@ -75,9 +90,6 @@ You should see the containers start up without error. You should now be able to
|
||||
|
||||
After the startup is complete you should see a login screen. Use the default credentials above to login and navigate to `/admin/site-settings`. Here you'll find a summary of your configuration details and their respective status. Before proceeding you should validate that the configuration is correct. For any warnings or errors the page will display an error and notify you of what you need to verify.
|
||||
|
||||
!!! tip "Docker Volume"
|
||||
Mealie uses a shared data-volume between the Backend and Frontend containers for images and assets. Ensure that this is configured correctly by using the "Docker Volume Test" section in the settings page. Running this validation will ensure that you have configured your volumes correctly. Mealie will not work correctly without this configured correctly.
|
||||
|
||||
## Step 5: Backup
|
||||
While v1.0.0 is a great step to data-stability and security, it's not a backup. Mealie provides a full site data backup mechanism through the UI.
|
||||
|
||||
@@ -87,6 +99,14 @@ These backups are just plain .zip files that you can download from the UI or acc
|
||||
|
||||
### Docker Tags
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:nightly`
|
||||
|
||||
The nightly build are the latest and greatest builds that are built directly off of every commit to the `mealie-next` branch and as such may contain bugs. These are great to help the community catch bugs before they hit the stable release or if you like living on the edge.
|
||||
|
||||
---
|
||||
|
||||
**These tags no are long updated**
|
||||
|
||||
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
|
||||
|
||||
These are the tags for the latest beta release of the frontend docker-container. These are currently considered the latest and most stable releases and the recommended way of using Mealie.
|
||||
@@ -94,16 +114,3 @@ These are the tags for the latest beta release of the frontend docker-container.
|
||||
`mealie:frontend-nightly`**and** `mealie:api-nightly`
|
||||
|
||||
The nightly build are the latest and greatest builds that are built directly off of every commit to the `mealie-next` branch and as such may contain bugs. These are great to help the community catch bugs before they hit the stable release or if you like living on the edge.
|
||||
|
||||
|
||||
### Docker Diagram
|
||||
|
||||
While the docker-compose file should work without modification, some users want to tailor it to their installation. This diagram shows network and volume architecture for the default setup. You can use this to help you customize your configuration.
|
||||
|
||||

|
||||
|
||||
In the diagram above there's a few crucial things to note.
|
||||
|
||||
1. Port 9925 is the host port, this can be anything you want. The important part is that it's mapped to the mealie-frontend container at port 3000.
|
||||
2. The mealie-frontend container communicated with the mealie-api container through the INTERNAL docker network. This requires that the two containers are on the same network and that the network supports name resolution (anything but the default bridge network). The resolution URL can be specified in the docker-compose as the `API_URL` environment variable.
|
||||
3. The mealie-data volume is mounted to BOTH the mealie-frontend and mealie-api containers. This is REQUIRED to ensure that images and assets are served up correctly. While the default configuration is a docker-volume, that same can be accomplished by using a local directory mounted to the containers.
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
# Installing with PostgreSQL
|
||||
|
||||
PostgreSQL might be considered if you need to support many concurrent users. In addition, some features are only enabled on PostgreSQL, such as fuzzy search.
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
- [Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-4
|
||||
container_name: mealie-frontend
|
||||
depends_on:
|
||||
- mealie-api
|
||||
environment:
|
||||
# Set Frontend ENV Variables Here
|
||||
- API_URL=http://mealie-api:9000 # (1)
|
||||
restart: always
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:nightly
|
||||
container_name: mealie
|
||||
ports:
|
||||
- "9925:3000" # (2)
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-v1.0.0beta-4
|
||||
container_name: mealie-api
|
||||
- "9925:9000"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (4)
|
||||
memory: 1000M # (1)
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
@@ -53,8 +43,10 @@ services:
|
||||
restart: always
|
||||
postgres:
|
||||
container_name: postgres
|
||||
image: postgres
|
||||
image: postgres:15
|
||||
restart: always
|
||||
volumes:
|
||||
- ./mealie-pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
@@ -62,12 +54,11 @@ services:
|
||||
volumes:
|
||||
mealie-data:
|
||||
driver: local
|
||||
mealie-pgdata:
|
||||
driver: local
|
||||
```
|
||||
|
||||
<!-- Updating This? Be Sure to also update the SQLite Annotations -->
|
||||
|
||||
1. Whoa whoa whoa, what is this nonsense? The API_URL is the URL the frontend container uses to proxy api requests to the backend server. In this example, the name `mealie-api` resolves to the `mealie-api` container which runs the API server on port 9000. This allows you to access the API without exposing an additional port on the host.
|
||||
<br/> <br/> **Note** that both containers must be on the same docker-network for this to work.
|
||||
2. To access the mealie interface you only need to expose port 3000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
3. Mounting the data directory to the frontend is now required to access the images/assets directory. This can be mounted read-only. Internally the frontend containers runs a Caddy proxy server that serves the assets requested to reduce load on the backend API.
|
||||
4. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
1. To access the mealie interface you only need to expose port 9000 on the mealie container. Here we expose port 9925 on the host, but feel free to change this to any port you like.
|
||||
2. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
|
||||
@@ -4,31 +4,21 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
- [Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-4
|
||||
container_name: mealie-frontend
|
||||
environment:
|
||||
# Set Frontend ENV Variables Here
|
||||
- API_URL=http://mealie-api:9000 # (1)
|
||||
restart: always
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:nightly
|
||||
container_name: mealie
|
||||
ports:
|
||||
- "9925:3000" # (2)
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-v1.0.0beta-4
|
||||
container_name: mealie-api
|
||||
- "9925:9000" # (1)
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (4)
|
||||
memory: 1000M # (2)
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
@@ -49,8 +39,5 @@ volumes:
|
||||
|
||||
<!-- Updating This? Be Sure to also update the Postgres Annotations -->
|
||||
|
||||
1. Whoa whoa whoa, what is this nonsense? The API_URL is the URL the frontend container uses to proxy api requests to the backend server. In this example, the name `mealie-api` resolves to the `mealie-api` container which runs the API server on port 9000. This allows you to access the API without exposing an additional port on the host.
|
||||
<br/> <br/> **Note** that both containers must be on the same docker-network for this to work.
|
||||
2. To access the mealie interface you only need to expose port 3000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
3. Mounting the data directory to the frontend is now required to access the images/assets directory. This can be mounted read-only. Internally the frontend containers runs a Caddy proxy server that serves the assets requested to reduce load on the backend API.
|
||||
4. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
1. To access the mealie interface you only need to expose port 9000 on the container. Here we expose port 9925 on the host, but feel free to change this to any port you like.
|
||||
2. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
|
||||
@@ -12,14 +12,14 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
|
||||
|
||||
## Key Features
|
||||
- 🔍 Fuzzy search
|
||||
- 🏷️ Tag recipes with categories or tags to flexible sorting
|
||||
- 🔍 Smart search, mix & match of "quoted literal searches" and keyword search. Fuzzy search ("is it brocolli or broccoli?") is also available when using a Postgres database.
|
||||
- 🏷️ Tag recipes with categories or tags for flexible sorting
|
||||
- 🕸 Import recipes from around the web by URL
|
||||
- 📱 Progressive Web App
|
||||
- 📆 Create Meal Plans
|
||||
- 🛒 Generate shopping lists
|
||||
- 🐳 Easy setup with Docker
|
||||
- 🎨 Customize your interface with color themes layouts
|
||||
- 🎨 Customize your interface with color themed layouts
|
||||
- 💾 Export all your data in any format with Jinja2 Templates, with easy data restoration from the user interface.
|
||||
- 🌍 localized in many languages
|
||||
- ➕ Plus tons more!
|
||||
@@ -34,17 +34,8 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
- Random meal plan generation
|
||||
|
||||
## FAQ
|
||||
See the [Frequently Asked Questions page](./faq.md)
|
||||
|
||||
### Why An API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
|
||||
### Why a Database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in controls of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
|
||||
- **Developer Experience:** Without a database a lot of the work to maintain your data is taken on by the developer instead of a battle tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
|
||||
## Built With
|
||||
|
||||
@@ -53,11 +44,6 @@ As to why we need a database?
|
||||
* [FastAPI](https://fastapi.tiangolo.com/)
|
||||
* [Docker](https://www.docker.com/)
|
||||
|
||||
<!-- ROADMAP -->
|
||||
## Road Map
|
||||
|
||||
[See Roadmap](../../roadmap.md)
|
||||
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
## Contributing
|
||||
|
||||
@@ -54,4 +54,4 @@ In most cases, it's faster to manually migrate the recipes that didn't take inst
|
||||
|
||||
v1 Comes with a whole host of new features and improvements. Checkout the changelog to get a sense for what's new.
|
||||
|
||||
- [v1 Changelog](../../changelog/v1.0.0.md)
|
||||
- [Github releases changelog](https://github.com/hay-kot/mealie/releases)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Development Road Map
|
||||
|
||||
## Feature Requests
|
||||
See the [Github META issue for tracking feature requests](https://github.com/hay-kot/mealie/issues/122)
|
||||
[Please request new features on Github](https://github.com/hay-kot/mealie/issues/317)
|
||||
|
||||
## Progress
|
||||
See the [Github Projects](https://github.com/hay-kot/mealie/projects) to see what is currently being worked on
|
||||
## Progress
|
||||
See the [Github Projects page](https://github.com/users/hay-kot/projects/2) to see what is currently being worked on
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Updating Mealie
|
||||
|
||||
!!! warning "Read The Release Notes"
|
||||
You MUST read the release notes prior to upgrading your container. Currently Mealie provides no database migrations as doing so would slow down development and hinder major changes that may need to happen prior to v1.0.0. Mealie has a robust backup and restore system for managing your data.
|
||||
You MUST read the release notes prior to upgrading your container. Mealie has a robust backup and restore system for managing your data. Pre-v1.0.0 versions of Mealie use a different database structure, so if you are upgrading from pre-v1.0.0 to v1.0.0, you MUST backup your data and then re-import it. Even if you are already on v1.0.0, it is strongly recommended to backup all data before updating.
|
||||
|
||||
### Before Upgrading
|
||||
- Read The Release Notes
|
||||
@@ -9,13 +9,16 @@
|
||||
- Create a Backup and Download from the UI
|
||||
- Upgrade
|
||||
|
||||
## Upgrading to Mealie v1
|
||||
If you are upgrading from pre-v1.0.0 to v1.0.0, make sure you read [Migrating to Mealie v1](./migrating-to-mealie-v1.md)!
|
||||
|
||||
## Backing Up Your Data
|
||||
|
||||
[See Backups and Restore Section](../admin/backups-and-exports.md) for details on backing up your data
|
||||
[See Backups and Restore Section](../getting-started/usage/backups-and-restoring.md) for details on backing up your data
|
||||
|
||||
## Docker
|
||||
For all setups using Docker the updating process looks something like this
|
||||
|
||||
- Stop the container using docker-compose down
|
||||
- Pull the latest image using docker-compose pull
|
||||
- Start the container again using docker-compose up -d
|
||||
- Pull the latest image using `docker-compose pull`
|
||||
- Start the container again using `docker-compose up -d`
|
||||
|
||||
8
docs/docs/documentation/getting-started/usage/ldap.md
Normal file
8
docs/docs/documentation/getting-started/usage/ldap.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# LDAP Authentication
|
||||
|
||||
If LDAP is enabled and [configured properly](../installation/backend-config.md), users will be able to log in with their LDAP credentials. If the user does not already have an account in Mealie, then one will be created.
|
||||
|
||||
If the user already has an account in Mealie and wants to use their LDAP credentials instead, then you can go to the **User Management** page in the admin panel and change the "Authentication Backend" from `Mealie` to `LDAP`. If for whatever reason, the user no longer wants to use LDAP authentication, then you can switch this back to `Mealie`.
|
||||
|
||||
!!! warning "Head's Up"
|
||||
If you switch a user from `LDAP` to `Mealie` who was initially created by LDAP, then the user will have to reset their password through the password reset flow.
|
||||
File diff suppressed because one or more lines are too long
@@ -224,19 +224,11 @@
|
||||
and a reactive frontend application built in Vue for a pleasant user
|
||||
experience for the whole family.
|
||||
</p>
|
||||
<a
|
||||
href="{{ page.next_page.url | url }}"
|
||||
title="{{ page.next_page.title | striptags }}"
|
||||
class="md-button md-button--primary"
|
||||
>
|
||||
<a href="{{ page.next_page.url | url }}" title="{{ page.next_page.title | striptags }}"
|
||||
class="md-button md-button--primary">
|
||||
Get started
|
||||
</a>
|
||||
<a
|
||||
href="{{ config.demo_url }}"
|
||||
title="{{ lang.t('source.link.title') }}"
|
||||
target="_blank"
|
||||
class="md-button"
|
||||
>
|
||||
<a href="{{ config.demo_url }}" title="{{ lang.t('source.link.title') }}" target="_blank" class="md-button">
|
||||
View the Demo
|
||||
</a>
|
||||
</div>
|
||||
@@ -251,10 +243,8 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,4.5L8.95,4.53C8.71,5.05 8.34,5.93 8.07,7H10.93C10.66,5.93 10.29,5.05 10.05,4.53C9.87,4.5 9.69,4.5 9.5,4.5M13.83,7C13.24,5.97 12.29,5.17 11.15,4.78C11.39,5.31 11.7,6.08 11.93,7H13.83M5.17,7H7.07C7.3,6.08 7.61,5.31 7.85,4.78C6.71,5.17 5.76,5.97 5.17,7M4.5,9.5C4.5,10 4.58,10.53 4.73,11H6.87L6.75,9.5L6.87,8H4.73C4.58,8.47 4.5,9 4.5,9.5M14.27,11C14.42,10.53 14.5,10 14.5,9.5C14.5,9 14.42,8.47 14.27,8H12.13C12.21,8.5 12.25,9 12.25,9.5C12.25,10 12.21,10.5 12.13,11H14.27M7.87,8L7.75,9.5L7.87,11H11.13C11.21,10.5 11.25,10 11.25,9.5C11.25,9 11.21,8.5 11.13,8H7.87M9.5,14.5C9.68,14.5 9.86,14.5 10.03,14.47C10.28,13.95 10.66,13.07 10.93,12H8.07C8.34,13.07 8.72,13.95 8.97,14.47L9.5,14.5M13.83,12H11.93C11.7,12.92 11.39,13.69 11.15,14.22C12.29,13.83 13.24,13.03 13.83,12M5.17,12C5.76,13.03 6.71,13.83 7.85,14.22C7.61,13.69 7.3,12.92 7.07,12H5.17Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,4.5L8.95,4.53C8.71,5.05 8.34,5.93 8.07,7H10.93C10.66,5.93 10.29,5.05 10.05,4.53C9.87,4.5 9.69,4.5 9.5,4.5M13.83,7C13.24,5.97 12.29,5.17 11.15,4.78C11.39,5.31 11.7,6.08 11.93,7H13.83M5.17,7H7.07C7.3,6.08 7.61,5.31 7.85,4.78C6.71,5.17 5.76,5.97 5.17,7M4.5,9.5C4.5,10 4.58,10.53 4.73,11H6.87L6.75,9.5L6.87,8H4.73C4.58,8.47 4.5,9 4.5,9.5M14.27,11C14.42,10.53 14.5,10 14.5,9.5C14.5,9 14.42,8.47 14.27,8H12.13C12.21,8.5 12.25,9 12.25,9.5C12.25,10 12.21,10.5 12.13,11H14.27M7.87,8L7.75,9.5L7.87,11H11.13C11.21,10.5 11.25,10 11.25,9.5C11.25,9 11.21,8.5 11.13,8H7.87M9.5,14.5C9.68,14.5 9.86,14.5 10.03,14.47C10.28,13.95 10.66,13.07 10.93,12H8.07C8.34,13.07 8.72,13.95 8.97,14.47L9.5,14.5M13.83,12H11.93C11.7,12.92 11.39,13.69 11.15,14.22C12.29,13.83 13.24,13.03 13.83,12M5.17,12C5.76,13.03 6.71,13.83 7.85,14.22C7.61,13.69 7.3,12.92 7.07,12H5.17Z" />
|
||||
</svg>
|
||||
Import Recipes
|
||||
</h2>
|
||||
@@ -266,10 +256,8 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z" />
|
||||
</svg>
|
||||
Automatic Backups
|
||||
</h2>
|
||||
@@ -281,10 +269,8 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,4C20.11,4 21,4.9 21,6V18A2,2 0 0,1 19,20H5C3.89,20 3,19.1 3,18V6A2,2 0 0,1 5,4H19M19,18V8H5V18H19Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M19,4C20.11,4 21,4.9 21,6V18A2,2 0 0,1 19,20H5C3.89,20 3,19.1 3,18V6A2,2 0 0,1 5,4H19M19,18V8H5V18H19Z" />
|
||||
</svg>
|
||||
Rich User Interface
|
||||
</h2>
|
||||
@@ -296,10 +282,8 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9,10V12H7V10H9M13,10V12H11V10H13M17,10V12H15V10H17M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H6V1H8V3H16V1H18V3H19M19,19V8H5V19H19M9,14V16H7V14H9M13,14V16H11V14H13M17,14V16H15V14H17Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M9,10V12H7V10H9M13,10V12H11V10H13M17,10V12H15V10H17M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H6V1H8V3H16V1H18V3H19M19,19V8H5V19H19M9,14V16H7V14H9M13,14V16H11V14H13M17,14V16H15V14H17Z" />
|
||||
</svg>
|
||||
Meal Planner
|
||||
</h2>
|
||||
@@ -312,10 +296,8 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" />
|
||||
</svg>
|
||||
Users
|
||||
</h2>
|
||||
@@ -327,25 +309,21 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" />
|
||||
</svg>
|
||||
Groups
|
||||
</h2>
|
||||
<p>
|
||||
Sort users into groups to share recipes with the whole family, but keep
|
||||
your Meal Plans separate.
|
||||
Full multi-user support with groups for sharing recipes and meal plans
|
||||
with family.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10.46,19C9,21.07 6.15,21.59 4.09,20.15C2.04,18.71 1.56,15.84 3,13.75C3.87,12.5 5.21,11.83 6.58,11.77L6.63,13.2C5.72,13.27 4.84,13.74 4.27,14.56C3.27,16 3.58,17.94 4.95,18.91C6.33,19.87 8.26,19.5 9.26,18.07C9.57,17.62 9.75,17.13 9.82,16.63V15.62L15.4,15.58L15.47,15.47C16,14.55 17.15,14.23 18.05,14.75C18.95,15.27 19.26,16.43 18.73,17.35C18.2,18.26 17.04,18.58 16.14,18.06C15.73,17.83 15.44,17.46 15.31,17.04L11.24,17.06C11.13,17.73 10.87,18.38 10.46,19M17.74,11.86C20.27,12.17 22.07,14.44 21.76,16.93C21.45,19.43 19.15,21.2 16.62,20.89C15.13,20.71 13.9,19.86 13.19,18.68L14.43,17.96C14.92,18.73 15.75,19.28 16.75,19.41C18.5,19.62 20.05,18.43 20.26,16.76C20.47,15.09 19.23,13.56 17.5,13.35C16.96,13.29 16.44,13.36 15.97,13.53L15.12,13.97L12.54,9.2H12.32C11.26,9.16 10.44,8.29 10.47,7.25C10.5,6.21 11.4,5.4 12.45,5.44C13.5,5.5 14.33,6.35 14.3,7.39C14.28,7.83 14.11,8.23 13.84,8.54L15.74,12.05C16.36,11.85 17.04,11.78 17.74,11.86M8.25,9.14C7.25,6.79 8.31,4.1 10.62,3.12C12.94,2.14 15.62,3.25 16.62,5.6C17.21,6.97 17.09,8.47 16.42,9.67L15.18,8.95C15.6,8.14 15.67,7.15 15.27,6.22C14.59,4.62 12.78,3.85 11.23,4.5C9.67,5.16 8.97,7 9.65,8.6C9.93,9.26 10.4,9.77 10.97,10.11L11.36,10.32L8.29,15.31C8.32,15.36 8.36,15.42 8.39,15.5C8.88,16.41 8.54,17.56 7.62,18.05C6.71,18.54 5.56,18.18 5.06,17.24C4.57,16.31 4.91,15.16 5.83,14.67C6.22,14.46 6.65,14.41 7.06,14.5L9.37,10.73C8.9,10.3 8.5,9.76 8.25,9.14Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M10.46,19C9,21.07 6.15,21.59 4.09,20.15C2.04,18.71 1.56,15.84 3,13.75C3.87,12.5 5.21,11.83 6.58,11.77L6.63,13.2C5.72,13.27 4.84,13.74 4.27,14.56C3.27,16 3.58,17.94 4.95,18.91C6.33,19.87 8.26,19.5 9.26,18.07C9.57,17.62 9.75,17.13 9.82,16.63V15.62L15.4,15.58L15.47,15.47C16,14.55 17.15,14.23 18.05,14.75C18.95,15.27 19.26,16.43 18.73,17.35C18.2,18.26 17.04,18.58 16.14,18.06C15.73,17.83 15.44,17.46 15.31,17.04L11.24,17.06C11.13,17.73 10.87,18.38 10.46,19M17.74,11.86C20.27,12.17 22.07,14.44 21.76,16.93C21.45,19.43 19.15,21.2 16.62,20.89C15.13,20.71 13.9,19.86 13.19,18.68L14.43,17.96C14.92,18.73 15.75,19.28 16.75,19.41C18.5,19.62 20.05,18.43 20.26,16.76C20.47,15.09 19.23,13.56 17.5,13.35C16.96,13.29 16.44,13.36 15.97,13.53L15.12,13.97L12.54,9.2H12.32C11.26,9.16 10.44,8.29 10.47,7.25C10.5,6.21 11.4,5.4 12.45,5.44C13.5,5.5 14.33,6.35 14.3,7.39C14.28,7.83 14.11,8.23 13.84,8.54L15.74,12.05C16.36,11.85 17.04,11.78 17.74,11.86M8.25,9.14C7.25,6.79 8.31,4.1 10.62,3.12C12.94,2.14 15.62,3.25 16.62,5.6C17.21,6.97 17.09,8.47 16.42,9.67L15.18,8.95C15.6,8.14 15.67,7.15 15.27,6.22C14.59,4.62 12.78,3.85 11.23,4.5C9.67,5.16 8.97,7 9.65,8.6C9.93,9.26 10.4,9.77 10.97,10.11L11.36,10.32L8.29,15.31C8.32,15.36 8.36,15.42 8.39,15.5C8.88,16.41 8.54,17.56 7.62,18.05C6.71,18.54 5.56,18.18 5.06,17.24C4.57,16.31 4.91,15.16 5.83,14.67C6.22,14.46 6.65,14.41 7.06,14.5L9.37,10.73C8.9,10.3 8.5,9.76 8.25,9.14Z" />
|
||||
</svg>
|
||||
Webhooks
|
||||
</h2>
|
||||
@@ -357,10 +335,8 @@
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 7H5A2 2 0 0 0 3 9V17H5V13H7V17H9V9A2 2 0 0 0 7 7M7 11H5V9H7M14 7H10V17H12V13H14A2 2 0 0 0 16 11V9A2 2 0 0 0 14 7M14 11H12V9H14M20 9V15H21V17H17V15H18V9H17V7H21V9Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M7 7H5A2 2 0 0 0 3 9V17H5V13H7V17H9V9A2 2 0 0 0 7 7M7 11H5V9H7M14 7H10V17H12V13H14A2 2 0 0 0 16 11V9A2 2 0 0 0 14 7M14 11H12V9H14M20 9V15H21V17H17V15H18V9H17V7H21V9Z" />
|
||||
</svg>
|
||||
Open API
|
||||
</h2>
|
||||
@@ -375,43 +351,28 @@
|
||||
<!-- Custom narrow footer -->
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-footer-social">
|
||||
<a
|
||||
class="md-footer-social__link"
|
||||
href="https://github.com/hay-kot/mealie"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="github.com"
|
||||
>
|
||||
<a class="md-footer-social__link" href="https://github.com/hay-kot/mealie" rel="noopener" target="_blank"
|
||||
title="github.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z"
|
||||
></path>
|
||||
d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z">
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="md-footer-social__link"
|
||||
href="https://twitter.com/kot_hay"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="twitter.com"
|
||||
>
|
||||
<a class="md-footer-social__link" href="https://twitter.com/kot_hay" rel="noopener" target="_blank"
|
||||
title="twitter.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
|
||||
></path>
|
||||
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z">
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="md-footer-social__link"
|
||||
href="https://www.linkedin.com/in/hay-kot"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="www.linkedin.com"
|
||||
>
|
||||
<a class="md-footer-social__link" href="https://www.linkedin.com/in/hay-kot" rel="noopener" target="_blank"
|
||||
title="www.linkedin.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
|
||||
></path>
|
||||
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z">
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +42,11 @@ markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- pymdownx.tabbed
|
||||
- pymdownx.superfences
|
||||
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
extra_css:
|
||||
- assets/stylesheets/custom.css
|
||||
extra_javascript:
|
||||
@@ -62,21 +65,23 @@ nav:
|
||||
- FAQ: "documentation/getting-started/faq.md"
|
||||
- API: "documentation/getting-started/api-usage.md"
|
||||
- Road Map: "documentation/getting-started/roadmap.md"
|
||||
- Advanced: "documentation/getting-started/installation/advanced.md"
|
||||
|
||||
- Installation:
|
||||
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
|
||||
- SQLite (Recommended): "documentation/getting-started/installation/sqlite.md"
|
||||
- PostgreSQL: "documentation/getting-started/installation/postgres.md"
|
||||
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
|
||||
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
||||
- Usage:
|
||||
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
|
||||
- LDAP Authentication: "documentation/getting-started/usage/ldap.md"
|
||||
|
||||
- Community Guides:
|
||||
- iOS Shortcuts: "documentation/community-guide/ios.md"
|
||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
||||
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
||||
- Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md"
|
||||
|
||||
- API Reference: "api/redoc.md"
|
||||
|
||||
@@ -90,6 +95,7 @@ nav:
|
||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||
|
||||
- Change Log:
|
||||
- v1.0.0beta-5: "changelog/v1.0.0beta-5.md"
|
||||
- v1.0.0beta-4: "changelog/v1.0.0beta-4.md"
|
||||
- v1.0.0beta-3: "changelog/v1.0.0beta-3.md"
|
||||
- v1.0.0beta-2: "changelog/v1.0.0beta-2.md"
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
settings: {
|
||||
"import/ignore": ["@vueuse*"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -26,7 +23,7 @@ module.exports = {
|
||||
],
|
||||
// Re-add once we use nuxt bridge
|
||||
// See https://v3.nuxtjs.org/getting-started/bridge#update-nuxtconfig
|
||||
ignorePatterns: ["nuxt.config.js"],
|
||||
ignorePatterns: ["nuxt.config.js", "lib/api/types/**/*.ts"],
|
||||
plugins: ["prettier"],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
@@ -58,6 +55,15 @@ module.exports = {
|
||||
],
|
||||
|
||||
// TODO Gradually activate all rules
|
||||
// Allow Promise in onMounted
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
"error",
|
||||
{
|
||||
checksVoidReturn: {
|
||||
arguments: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
|
||||
1
frontend/.nuxtignore
Normal file
1
frontend/.nuxtignore
Normal file
@@ -0,0 +1 @@
|
||||
pages/**/*.ts
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
|
||||
:3000 {
|
||||
@apidocs path /docs /openapi.json
|
||||
|
||||
@static {
|
||||
file
|
||||
path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.webp
|
||||
}
|
||||
|
||||
encode gzip zstd
|
||||
|
||||
# Handles Recipe Images / Assets
|
||||
handle_path /api/media/recipes/* {
|
||||
header @static Cache-Control max-age=31536000
|
||||
root * /app/data/recipes/
|
||||
file_server
|
||||
}
|
||||
|
||||
# Handles User Images
|
||||
handle_path /api/media/users/* {
|
||||
header @static Cache-Control max-age=31536000
|
||||
root * /app/data/users/
|
||||
file_server
|
||||
}
|
||||
|
||||
# Handle Docker Volume Validation File
|
||||
handle_path /api/media/docker/* {
|
||||
root * /app/data/docker-validation/
|
||||
file_server
|
||||
}
|
||||
|
||||
|
||||
handle @apidocs {
|
||||
uri strip_suffix /
|
||||
reverse_proxy {$API_URL}
|
||||
}
|
||||
|
||||
handle {
|
||||
uri strip_suffix /
|
||||
reverse_proxy http://127.0.0.1:3001
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
FROM node:16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install \
|
||||
--prefer-offline \
|
||||
--frozen-lockfile \
|
||||
--non-interactive \
|
||||
--production=false \
|
||||
# https://github.com/docker/build-push-action/issues/471
|
||||
--network-timeout 1000000
|
||||
|
||||
RUN yarn build
|
||||
|
||||
RUN rm -rf node_modules && \
|
||||
NODE_ENV=production yarn install \
|
||||
--prefer-offline \
|
||||
--pure-lockfile \
|
||||
--non-interactive \
|
||||
--production=true
|
||||
|
||||
FROM node:16-alpine
|
||||
|
||||
RUN apk add caddy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# copying caddy into image
|
||||
COPY --from=builder /app .
|
||||
COPY ./Caddyfile /app/
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
EXPOSE 3000
|
||||
|
||||
RUN chmod +x /app/run.sh
|
||||
ENTRYPOINT /app/run.sh
|
||||
BIN
frontend/assets/audio/kitchen_alarm.mp3
Normal file
BIN
frontend/assets/audio/kitchen_alarm.mp3
Normal file
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-cyrillic-ext1.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-cyrillic-ext1.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@@ -13,7 +13,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-cyrillic2.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-cyrillic2.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@@ -22,7 +22,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-greek-ext3.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-greek-ext3.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@@ -31,7 +31,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-greek4.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-greek4.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@@ -40,7 +40,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-vietnamese5.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-vietnamese5.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@@ -49,7 +49,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-latin-ext6.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-latin-ext6.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@@ -58,7 +58,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-100-latin7.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-100-latin7.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@@ -67,7 +67,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-cyrillic-ext8.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-cyrillic-ext8.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@@ -76,7 +76,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-cyrillic9.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-cyrillic9.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@@ -85,7 +85,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-greek-ext10.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-greek-ext10.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@@ -94,7 +94,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-greek11.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-greek11.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@@ -103,7 +103,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-vietnamese12.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-vietnamese12.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@@ -112,7 +112,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-latin-ext13.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-latin-ext13.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@@ -121,7 +121,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-300-latin14.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-300-latin14.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@@ -130,7 +130,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-cyrillic-ext15.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-cyrillic-ext15.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@@ -139,7 +139,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-cyrillic16.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-cyrillic16.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@@ -148,7 +148,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-greek-ext17.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-greek-ext17.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@@ -157,7 +157,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-greek18.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-greek18.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@@ -166,7 +166,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-vietnamese19.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-vietnamese19.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@@ -175,7 +175,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-latin-ext20.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-latin-ext20.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@@ -184,7 +184,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-400-latin21.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-400-latin21.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@@ -193,7 +193,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-cyrillic-ext22.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-cyrillic-ext22.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@@ -202,7 +202,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-cyrillic23.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-cyrillic23.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@@ -211,7 +211,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-greek-ext24.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-greek-ext24.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@@ -220,7 +220,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-greek25.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-greek25.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@@ -229,7 +229,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-vietnamese26.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-vietnamese26.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@@ -238,7 +238,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-latin-ext27.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-latin-ext27.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@@ -247,7 +247,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-500-latin28.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-500-latin28.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@@ -256,7 +256,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-cyrillic-ext29.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-cyrillic-ext29.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@@ -265,7 +265,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-cyrillic30.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-cyrillic30.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@@ -274,7 +274,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-greek-ext31.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-greek-ext31.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@@ -283,7 +283,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-greek32.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-greek32.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@@ -292,7 +292,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-vietnamese33.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-vietnamese33.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@@ -301,7 +301,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-latin-ext34.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-latin-ext34.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@@ -310,7 +310,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-700-latin35.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-700-latin35.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@@ -319,7 +319,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-cyrillic-ext36.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-cyrillic-ext36.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@@ -328,7 +328,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-cyrillic37.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-cyrillic37.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@@ -337,7 +337,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-greek-ext38.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-greek-ext38.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@@ -346,7 +346,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-greek39.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-greek39.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@@ -355,7 +355,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-vietnamese40.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-vietnamese40.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@@ -364,7 +364,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-latin-ext41.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-latin-ext41.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@@ -373,6 +373,6 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Roboto-900-latin42.woff2') format('woff2');
|
||||
src: url('~assets/fonts/Roboto-900-latin42.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
74
frontend/components/Domain/Cookbook/CookbookPage.vue
Normal file
74
frontend/components/Domain/Cookbook/CookbookPage.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<v-container v-if="book" fluid>
|
||||
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
||||
<v-icon large left> {{ $globals.icons.pages }} </v-icon>
|
||||
<v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title>
|
||||
</v-app-bar>
|
||||
<v-card flat>
|
||||
<v-card-text class="py-0">
|
||||
{{ book.description }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-container class="pa-0">
|
||||
<RecipeCardSection
|
||||
class="mb-5 mx-1"
|
||||
:recipes="recipes"
|
||||
:query="{ cookbook: slug }"
|
||||
:group-slug="groupSlug"
|
||||
@sortRecipes="assignSorted"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
@delete="removeRecipe"
|
||||
/>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useRoute, ref, useContext, useMeta } from "@nuxtjs/composition-api";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
props: {
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => $auth.loggedIn);
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
|
||||
const route = useRoute();
|
||||
const slug = route.value.params.slug;
|
||||
const { getOne } = useCookbook(loggedIn.value ? null : props.groupSlug);
|
||||
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
|
||||
useMeta(() => {
|
||||
return {
|
||||
title: book?.value?.name || "Cookbook",
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
};
|
||||
},
|
||||
head: {}, // Must include for useMeta
|
||||
});
|
||||
</script>
|
||||
@@ -8,9 +8,11 @@
|
||||
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
||||
|
||||
<!-- TODO Make this localizable -->
|
||||
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }}
|
||||
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }}
|
||||
<!-- TODO: proper pluralization of inputDay -->
|
||||
{{ $t('meal-plan.this-rule-will-apply', {
|
||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType])
|
||||
}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle title="General Preferences"></BaseCardSectionTitle>
|
||||
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" label="Private Group"></v-checkbox>
|
||||
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
|
||||
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" :label="$t('group.private-group')"></v-checkbox>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
@@ -11,7 +11,7 @@
|
||||
:label="$t('settings.first-day-of-week')"
|
||||
/>
|
||||
|
||||
<BaseCardSectionTitle class="mt-5" title="Group Recipe Preferences"></BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle class="mt-5" :title="$tc('group.group-recipe-preferences')"></BaseCardSectionTitle>
|
||||
<template v-for="(_, key) in preferences">
|
||||
<v-checkbox
|
||||
v-if="labels[key]"
|
||||
@@ -38,12 +38,12 @@ export default defineComponent({
|
||||
const { i18n } = useContext();
|
||||
|
||||
const labels = {
|
||||
recipePublic: "Allow users outside of your group to see your recipes",
|
||||
recipeShowNutrition: "Show nutrition information",
|
||||
recipeShowAssets: "Show recipe assets",
|
||||
recipeLandscapeView: "Default to landscape view",
|
||||
recipeDisableComments: "Disable recipe comments from users in your group",
|
||||
recipeDisableAmount: "Disable organizing recipe ingredients by units and food",
|
||||
recipePublic: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
recipeShowNutrition: i18n.tc("group.show-nutrition-information"),
|
||||
recipeShowAssets: i18n.tc("group.show-recipe-assets"),
|
||||
recipeLandscapeView: i18n.tc("group.default-to-landscape-view"),
|
||||
recipeDisableComments: i18n.tc("group.disable-users-from-commenting-on-recipes"),
|
||||
recipeDisableAmount: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
};
|
||||
|
||||
const allDays = [
|
||||
@@ -96,4 +96,4 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
icon: $globals.icons.testTube,
|
||||
text: $tc('general.test'),
|
||||
event: 'test',
|
||||
// TODO: There is no functionality hooked up to this. Enable it when there is
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
>
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
:title="$t('recipe.delete-recipe')"
|
||||
:title="$tc('recipe.delete-recipe')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
@confirm="emitDelete()"
|
||||
@@ -22,54 +22,65 @@
|
||||
<v-spacer></v-spacer>
|
||||
<div v-if="!open" class="custom-btn-group ma-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="recipe.slug" show-always />
|
||||
<v-tooltip v-if="!locked" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-else bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> {{ $t("recipe.locked-by-owner") }} </span>
|
||||
</v-tooltip>
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="!locked" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-else bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> {{ $t("recipe.locked-by-owner") }} </span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<RecipeTimerMenu
|
||||
fab
|
||||
color="info"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
||||
<RecipeContextMenu
|
||||
show-print
|
||||
:menu-top="false"
|
||||
:name="recipe.name"
|
||||
:group-id="recipe.groupId"
|
||||
:slug="recipe.slug"
|
||||
:menu-icon="$globals.icons.dotsVertical"
|
||||
fab
|
||||
color="info"
|
||||
:card-menu="false"
|
||||
:recipe="recipe"
|
||||
:recipe-id="recipe.id"
|
||||
:recipe-scale="recipeScale"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
download: loggedIn,
|
||||
duplicate: loggedIn,
|
||||
mealplanner: loggedIn,
|
||||
shoppingList: loggedIn,
|
||||
print: true,
|
||||
share: true,
|
||||
publicUrl: recipe.settings ? recipe.settings.public : false,
|
||||
printPreferences: true,
|
||||
share: loggedIn,
|
||||
publicUrl: recipe.settings && loggedIn ? recipe.settings.public : false,
|
||||
}"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="open" class="custom-btn-group mb-">
|
||||
<div v-if="open" class="custom-btn-group gapped">
|
||||
<v-btn
|
||||
v-for="(btn, index) in editorButtons"
|
||||
:key="index"
|
||||
:fab="$vuetify.breakpoint.xs"
|
||||
:small="$vuetify.breakpoint.xs"
|
||||
class="mx-1"
|
||||
:color="btn.color"
|
||||
@click="emitHandler(btn.event)"
|
||||
>
|
||||
@@ -84,6 +95,8 @@
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeTimerMenu from "./RecipeTimerMenu.vue";
|
||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
const SAVE_EVENT = "save";
|
||||
@@ -93,7 +106,7 @@ const JSON_EVENT = "json";
|
||||
const OCR_EVENT = "ocr";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge },
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimerMenu, RecipeTimelineBadge },
|
||||
props: {
|
||||
recipe: {
|
||||
required: true,
|
||||
@@ -103,6 +116,10 @@ export default defineComponent({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
open: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
@@ -204,6 +221,10 @@ export default defineComponent({
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.gapped {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<v-card
|
||||
:class="{ 'on-hover': hover }"
|
||||
:elevation="hover ? 12 : 2"
|
||||
:to="route ? `/recipe/${slug}` : ''"
|
||||
:to="route ? recipeRoute : ''"
|
||||
:min-height="imageHeight + 75"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
@@ -19,7 +19,9 @@
|
||||
<v-expand-transition v-if="description">
|
||||
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
{{ description }}
|
||||
<div class="descriptionWrapper">
|
||||
<SafeMarkdown :source="description" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
@@ -37,7 +39,10 @@
|
||||
<RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="loggedIn"
|
||||
color="grey darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
@@ -49,6 +54,7 @@
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}"
|
||||
@@ -77,6 +83,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -112,14 +122,19 @@ export default defineComponent({
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`;
|
||||
});
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
recipeRoute,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -143,4 +158,10 @@ export default defineComponent({
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.descriptionWrapper{
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
class="mx-auto"
|
||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
||||
hover
|
||||
:to="$listeners.selected ? undefined : `/recipe/${slug}`"
|
||||
:to="$listeners.selected ? undefined : recipeRoute"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-list-item three-line>
|
||||
<slot name="avatar">
|
||||
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
|
||||
<v-img v-if="vertical" class="rounded-sm">
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="150"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
/>
|
||||
</v-img>
|
||||
<v-list-item three-line :class="vertical ? 'px-2' : 'px-0'">
|
||||
<slot v-if="!vertical" name="avatar">
|
||||
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0">
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="125"
|
||||
@@ -17,18 +27,20 @@
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
></RecipeCardImage>
|
||||
/>
|
||||
</v-list-item-avatar>
|
||||
</slot>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="mb-1">{{ name }} </v-list-item-title>
|
||||
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
|
||||
<div class="d-flex justify-center align-center">
|
||||
<v-list-item-content class="py-0">
|
||||
<v-list-item-title class="mt-3 mb-1">{{ name }} </v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<SafeMarkdown :source="description" />
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<v-rating
|
||||
color="secondary"
|
||||
class="ml-auto"
|
||||
:class="loggedIn ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
background-color="secondary lighten-3"
|
||||
dense
|
||||
length="5"
|
||||
@@ -36,7 +48,11 @@
|
||||
:value="rating"
|
||||
></v-rating>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<!-- We also add padding to the v-rating above to compensate -->
|
||||
<RecipeContextMenu
|
||||
v-if="loggedIn"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
@@ -48,6 +64,7 @@
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}"
|
||||
@@ -79,6 +96,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -104,15 +125,28 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isFlat: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`;
|
||||
});
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
recipeRoute,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -145,4 +179,9 @@ export default defineComponent({
|
||||
.text-top {
|
||||
align-self: start !important;
|
||||
}
|
||||
|
||||
.flat, .theme--dark .flat {
|
||||
box-shadow: none!important;
|
||||
background-color: transparent!important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,13 +49,19 @@
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.lastMade)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.chefHat }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.last-made") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<ContextMenu
|
||||
v-if="!$vuetify.breakpoint.xsOnly"
|
||||
:items="[
|
||||
{
|
||||
title: $t('general.toggle-view'),
|
||||
title: $tc('general.toggle-view'),
|
||||
icon: $globals.icons.eye,
|
||||
event: 'toggle-dense-view',
|
||||
},
|
||||
@@ -70,12 +76,12 @@
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:group-slug="groupSlug"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
@delete="$emit('delete', recipe.slug)"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
@@ -94,12 +100,12 @@
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:group-slug="groupSlug"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
@delete="$emit('delete', recipe.slug)"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
@@ -123,6 +129,7 @@ import {
|
||||
useAsync,
|
||||
useContext,
|
||||
useRouter,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import RecipeCard from "./RecipeCard.vue";
|
||||
@@ -131,6 +138,7 @@ import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
||||
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||
|
||||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||
@@ -157,24 +165,16 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
cookbookSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
categorySlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
tagSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
toolSlug: {
|
||||
type: String,
|
||||
query: {
|
||||
type: Object as () => RecipeSearchQuery,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
@@ -186,10 +186,14 @@ export default defineComponent({
|
||||
rating: "rating",
|
||||
created: "created",
|
||||
updated: "updated",
|
||||
lastMade: "lastMade",
|
||||
shuffle: "shuffle",
|
||||
};
|
||||
|
||||
const { $globals, $vuetify } = useContext();
|
||||
const { $auth, $globals, $vuetify } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const useMobileCards = computed(() => {
|
||||
return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
|
||||
});
|
||||
@@ -207,44 +211,69 @@ export default defineComponent({
|
||||
if (props.recipes.length > 0) {
|
||||
const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)];
|
||||
if (recipe.slug !== undefined) {
|
||||
router.push(`/recipe/${recipe.slug}`);
|
||||
router.push(loggedIn.value ? `/recipe/${recipe.slug}` : `/explore/recipes/${props.groupSlug}/${recipe.slug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const perPage = ref(32);
|
||||
const perPage = 32;
|
||||
const hasMore = ref(true);
|
||||
const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const cookbook = ref<string>(props.cookbookSlug);
|
||||
const category = ref<string>(props.categorySlug);
|
||||
const tag = ref<string>(props.tagSlug);
|
||||
const tool = ref<string>(props.toolSlug);
|
||||
const { fetchMore } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
|
||||
const { fetchMore } = useLazyRecipes();
|
||||
const queryFilter = computed(() => {
|
||||
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
||||
return preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
|
||||
});
|
||||
|
||||
async function fetchRecipes(pageCount = 1) {
|
||||
return await fetchMore(
|
||||
page.value,
|
||||
// we double-up the first call to avoid a bug with large screens that render the entire first page without scrolling, preventing additional loading
|
||||
perPage * pageCount,
|
||||
props.query?.orderBy || preferences.value.orderBy,
|
||||
props.query?.orderDirection || preferences.value.orderDirection,
|
||||
props.query,
|
||||
// filter out recipes that have a null value for the property we're sorting by
|
||||
queryFilter.value
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const newRecipes = await fetchMore(
|
||||
page.value,
|
||||
if (props.query) {
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// we double-up the first call to avoid a bug with large screens that render the entire first page without scrolling, preventing additional loading
|
||||
perPage.value * 2,
|
||||
preferences.value.orderBy,
|
||||
preferences.value.orderDirection,
|
||||
cookbook.value,
|
||||
category.value,
|
||||
tag.value,
|
||||
tool.value
|
||||
);
|
||||
let lastQuery: string | undefined;
|
||||
watch(
|
||||
() => props.query,
|
||||
async (newValue: RecipeSearchQuery | undefined) => {
|
||||
const newValueString = JSON.stringify(newValue)
|
||||
if (newValue && (!ready.value || lastQuery !== newValueString)) {
|
||||
lastQuery = newValueString;
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function initRecipes() {
|
||||
page.value = 1;
|
||||
const newRecipes = await fetchRecipes(2);
|
||||
if (!newRecipes.length) {
|
||||
hasMore.value = false;
|
||||
}
|
||||
|
||||
// since we doubled the first call, we also need to advance the page
|
||||
page.value = page.value + 1;
|
||||
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
ready.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
const infiniteScroll = useThrottleFn(() => {
|
||||
useAsync(async () => {
|
||||
@@ -255,16 +284,7 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
page.value = page.value + 1;
|
||||
|
||||
const newRecipes = await fetchMore(
|
||||
page.value,
|
||||
perPage.value,
|
||||
preferences.value.orderBy,
|
||||
preferences.value.orderDirection,
|
||||
cookbook.value,
|
||||
category.value,
|
||||
tag.value,
|
||||
tool.value
|
||||
);
|
||||
const newRecipes = await fetchRecipes();
|
||||
if (!newRecipes.length) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
@@ -280,10 +300,17 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
function setter(orderBy: string, ascIcon: string, descIcon: string) {
|
||||
function setter(
|
||||
orderBy: string,
|
||||
ascIcon: string,
|
||||
descIcon: string,
|
||||
defaultOrderDirection = "asc",
|
||||
filterNull = false
|
||||
) {
|
||||
if (preferences.value.orderBy !== orderBy) {
|
||||
preferences.value.orderBy = orderBy;
|
||||
preferences.value.orderDirection = "asc";
|
||||
preferences.value.orderDirection = defaultOrderDirection;
|
||||
preferences.value.filterNull = filterNull;
|
||||
} else {
|
||||
preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc";
|
||||
}
|
||||
@@ -292,16 +319,37 @@ export default defineComponent({
|
||||
|
||||
switch (sortType) {
|
||||
case EVENTS.az:
|
||||
setter("name", $globals.icons.sortAlphabeticalAscending, $globals.icons.sortAlphabeticalDescending);
|
||||
setter(
|
||||
"name",
|
||||
$globals.icons.sortAlphabeticalAscending,
|
||||
$globals.icons.sortAlphabeticalDescending,
|
||||
"asc",
|
||||
false
|
||||
);
|
||||
break;
|
||||
case EVENTS.rating:
|
||||
setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending);
|
||||
setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending, "desc", true);
|
||||
break;
|
||||
case EVENTS.created:
|
||||
setter("created_at", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending);
|
||||
setter(
|
||||
"created_at",
|
||||
$globals.icons.sortCalendarAscending,
|
||||
$globals.icons.sortCalendarDescending,
|
||||
"desc",
|
||||
false
|
||||
);
|
||||
break;
|
||||
case EVENTS.updated:
|
||||
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending);
|
||||
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
|
||||
break;
|
||||
case EVENTS.lastMade:
|
||||
setter(
|
||||
"last_made",
|
||||
$globals.icons.sortCalendarAscending,
|
||||
$globals.icons.sortCalendarDescending,
|
||||
"desc",
|
||||
true
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown Event", sortType);
|
||||
@@ -317,16 +365,7 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
|
||||
// fetch new recipes
|
||||
const newRecipes = await fetchMore(
|
||||
page.value,
|
||||
perPage.value,
|
||||
preferences.value.orderBy,
|
||||
preferences.value.orderDirection,
|
||||
cookbook.value,
|
||||
category.value,
|
||||
tag.value,
|
||||
tool.value
|
||||
);
|
||||
const newRecipes = await fetchRecipes();
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
|
||||
state.sortLoading = false;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
color="accent"
|
||||
:small="small"
|
||||
dark
|
||||
:to="`/recipes/${urlPrefix}/${category.slug}`"
|
||||
:to=" loggedIn ? `/?${urlPrefix}=${category.id}` : undefined"
|
||||
>
|
||||
{{ truncateText(category.name) }}
|
||||
</v-chip>
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/user";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
@@ -54,6 +54,11 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn
|
||||
})
|
||||
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
if (!props.truncate) return text;
|
||||
const node = document.createElement("div");
|
||||
@@ -63,6 +68,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="text-center">
|
||||
<!-- Recipe Share Dialog -->
|
||||
<RecipeDialogShare v-model="shareDialog" :recipe-id="recipeId" :name="name" />
|
||||
<RecipeDialogPrintPreferences v-model="printPreferencesDialog" :recipe="recipeRef" />
|
||||
<BaseDialog
|
||||
v-model="recipeDeleteDialog"
|
||||
:title="$t('recipe.delete-recipe')"
|
||||
@@ -13,6 +14,23 @@
|
||||
{{ $t("recipe.delete-confirmation") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
v-model="recipeDuplicateDialog"
|
||||
:title="$t('recipe.duplicate')"
|
||||
color="primary"
|
||||
:icon="$globals.icons.duplicate"
|
||||
@confirm="duplicateRecipe()"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="recipeName"
|
||||
dense
|
||||
:label="$t('recipe.recipe-name')"
|
||||
autofocus
|
||||
@keyup.enter="duplicateRecipe()"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
v-model="mealplannerDialog"
|
||||
:title="$t('recipe.add-recipe-to-mealplan')"
|
||||
@@ -58,7 +76,7 @@
|
||||
:key="list.id"
|
||||
hover
|
||||
class="my-2 left-border"
|
||||
@click="addRecipeToList(list.id)"
|
||||
@click="openShoppingListIngredientDialog(list)"
|
||||
>
|
||||
<v-card-title class="py-2">
|
||||
{{ list.name }}
|
||||
@@ -66,6 +84,62 @@
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
v-model="shoppingListIngredientDialog"
|
||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
||||
:icon="$globals.icons.cartCheck"
|
||||
width="70%"
|
||||
:submit-text="$tc('recipe.add-to-list')"
|
||||
@submit="addRecipeToList()"
|
||||
>
|
||||
<v-card
|
||||
elevation="0"
|
||||
height="fit-content"
|
||||
max-height="60vh"
|
||||
width="100%"
|
||||
:class="$vuetify.breakpoint.smAndDown ? '' : 'ingredient-grid'"
|
||||
:style="$vuetify.breakpoint.smAndDown ? '' : { gridTemplateRows: `repeat(${Math.ceil(recipeIngredients.length / 2)}, min-content)` }"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(ingredientData, i) in recipeIngredients"
|
||||
:key="'ingredient' + i"
|
||||
dense
|
||||
@click="recipeIngredients[i].checked = !recipeIngredients[i].checked"
|
||||
>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
:input-value="ingredientData.checked"
|
||||
class="pt-0 my-auto py-auto"
|
||||
color="secondary"
|
||||
/>
|
||||
<v-list-item-content :key="ingredientData.ingredient.quantity">
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="ingredientData.ingredient"
|
||||
:disable-amount="ingredientData.disableAmount"
|
||||
:scale="recipeScale" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
<div class="d-flex justify-end mb-4 mt-2">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.checkboxBlankOutline,
|
||||
text: $tc('shopping-list.uncheck-all-items'),
|
||||
event: 'uncheck',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.checkboxOutline,
|
||||
text: $tc('shopping-list.check-all-items'),
|
||||
event: 'check',
|
||||
},
|
||||
]"
|
||||
@uncheck="bulkCheckIngredients(false)"
|
||||
@check="bulkCheckIngredients(true)"
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
@@ -75,7 +149,7 @@
|
||||
:nudge-top="menuTop ? '5' : '0'"
|
||||
allow-overflow
|
||||
close-delay="125"
|
||||
open-on-hover
|
||||
:open-on-hover="$vuetify.breakpoint.mdAndUp"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
@@ -96,11 +170,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api";
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { planTypeOptions } from "~/composables/use-group-mealplan";
|
||||
import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
|
||||
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/group";
|
||||
import { PlanEntryType } from "~/lib/api/types/meal-plan";
|
||||
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
|
||||
@@ -113,6 +190,7 @@ export interface ContextMenuIncludes {
|
||||
mealplanner: boolean;
|
||||
shoppingList: boolean;
|
||||
print: boolean;
|
||||
printPreferences: boolean;
|
||||
share: boolean;
|
||||
publicUrl: boolean;
|
||||
}
|
||||
@@ -122,12 +200,15 @@ export interface ContextMenuItem {
|
||||
icon: string;
|
||||
color: string | undefined;
|
||||
event: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeDialogPrintPreferences,
|
||||
RecipeDialogShare,
|
||||
},
|
||||
RecipeIngredientListItem
|
||||
},
|
||||
props: {
|
||||
useItems: {
|
||||
type: Object as () => ContextMenuIncludes,
|
||||
@@ -135,9 +216,11 @@ export default defineComponent({
|
||||
delete: true,
|
||||
edit: true,
|
||||
download: true,
|
||||
duplicate: false,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: true,
|
||||
printPreferences: true,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}),
|
||||
@@ -176,28 +259,31 @@ export default defineComponent({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipe: {
|
||||
type: Object as () => Recipe,
|
||||
default: undefined,
|
||||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* Optional group ID prop that is only _required_ when the
|
||||
* public URL is requested. If the public URL button is pressed
|
||||
* and the groupId is not set, an error will be thrown.
|
||||
*/
|
||||
groupId: {
|
||||
type: String,
|
||||
default: "",
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
printPreferencesDialog: false,
|
||||
shareDialog: false,
|
||||
recipeDeleteDialog: false,
|
||||
mealplannerDialog: false,
|
||||
shoppingListDialog: false,
|
||||
shoppingListIngredientDialog: false,
|
||||
recipeDuplicateDialog: false,
|
||||
recipeName: props.name,
|
||||
loading: false,
|
||||
menuItems: [] as ContextMenuItem[],
|
||||
newMealdate: "",
|
||||
@@ -205,7 +291,10 @@ export default defineComponent({
|
||||
pickerMenu: false,
|
||||
});
|
||||
|
||||
const { i18n, $globals } = useContext();
|
||||
const { $auth, i18n, $globals } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Setup
|
||||
@@ -216,48 +305,70 @@ export default defineComponent({
|
||||
icon: $globals.icons.edit,
|
||||
color: undefined,
|
||||
event: "edit",
|
||||
isPublic: false,
|
||||
},
|
||||
delete: {
|
||||
title: i18n.tc("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
color: "error",
|
||||
event: "delete",
|
||||
isPublic: false,
|
||||
},
|
||||
download: {
|
||||
title: i18n.tc("general.download"),
|
||||
icon: $globals.icons.download,
|
||||
color: undefined,
|
||||
event: "download",
|
||||
isPublic: false,
|
||||
},
|
||||
duplicate: {
|
||||
title: i18n.tc("general.duplicate"),
|
||||
icon: $globals.icons.duplicate,
|
||||
color: undefined,
|
||||
event: "duplicate",
|
||||
isPublic: false,
|
||||
},
|
||||
mealplanner: {
|
||||
title: i18n.tc("recipe.add-to-plan"),
|
||||
icon: $globals.icons.calendar,
|
||||
color: undefined,
|
||||
event: "mealplanner",
|
||||
isPublic: false,
|
||||
},
|
||||
shoppingList: {
|
||||
title: i18n.tc("recipe.add-to-list"),
|
||||
icon: $globals.icons.cartCheck,
|
||||
color: undefined,
|
||||
event: "shoppingList",
|
||||
isPublic: false,
|
||||
},
|
||||
print: {
|
||||
title: i18n.tc("general.print"),
|
||||
icon: $globals.icons.printer,
|
||||
color: undefined,
|
||||
event: "print",
|
||||
isPublic: true,
|
||||
},
|
||||
printPreferences: {
|
||||
title: i18n.tc("general.print-preferences"),
|
||||
icon: $globals.icons.printerSettings,
|
||||
color: undefined,
|
||||
event: "printPreferences",
|
||||
isPublic: true,
|
||||
},
|
||||
share: {
|
||||
title: i18n.tc("general.share"),
|
||||
icon: $globals.icons.shareVariant,
|
||||
color: undefined,
|
||||
event: "share",
|
||||
isPublic: false,
|
||||
},
|
||||
publicUrl: {
|
||||
title: i18n.tc("recipe.public-link"),
|
||||
icon: $globals.icons.contentCopy,
|
||||
color: undefined,
|
||||
event: "publicUrl",
|
||||
isPublic: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -265,7 +376,7 @@ export default defineComponent({
|
||||
for (const [key, value] of Object.entries(props.useItems)) {
|
||||
if (value) {
|
||||
const item = defaultItems[key];
|
||||
if (item) {
|
||||
if (item && (item.isPublic || loggedIn.value)) {
|
||||
state.menuItems.push(item);
|
||||
}
|
||||
}
|
||||
@@ -280,6 +391,9 @@ export default defineComponent({
|
||||
// Context Menu Event Handler
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const selectedShoppingList = ref<ShoppingListSummary>();
|
||||
const recipeRef = ref<Recipe>(props.recipe);
|
||||
const recipeIngredients = ref<{ checked: boolean; ingredient: RecipeIngredient, disableAmount: boolean }[]>([]);
|
||||
|
||||
async function getShoppingLists() {
|
||||
const { data } = await api.shopping.lists.getAll();
|
||||
@@ -288,11 +402,65 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
async function addRecipeToList(listId: string) {
|
||||
const { data } = await api.shopping.lists.addRecipe(listId, props.recipeId);
|
||||
async function refreshRecipe() {
|
||||
const { data } = await api.recipes.getOne(props.slug);
|
||||
if (data) {
|
||||
recipeRef.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||
selectedShoppingList.value = list;
|
||||
if (!recipeRef.value) {
|
||||
await refreshRecipe();
|
||||
}
|
||||
|
||||
if (recipeRef.value?.recipeIngredient) {
|
||||
recipeIngredients.value = recipeRef.value.recipeIngredient.map((ingredient) => {
|
||||
return {
|
||||
checked: true,
|
||||
ingredient,
|
||||
disableAmount: recipeRef.value.settings?.disableAmount || false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = true;
|
||||
}
|
||||
|
||||
function bulkCheckIngredients(value = true) {
|
||||
recipeIngredients.value.forEach((data) => {
|
||||
data.checked = value;
|
||||
});
|
||||
}
|
||||
|
||||
async function addRecipeToList() {
|
||||
if (!selectedShoppingList.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ingredients: RecipeIngredient[] = [];
|
||||
recipeIngredients.value.forEach((data) => {
|
||||
if (data.checked) {
|
||||
ingredients.push(data.ingredient);
|
||||
}
|
||||
});
|
||||
|
||||
if (!ingredients.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.shopping.lists.addRecipe(
|
||||
selectedShoppingList.value.id,
|
||||
props.recipeId,
|
||||
props.recipeScale,
|
||||
ingredients
|
||||
);
|
||||
if (data) {
|
||||
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +497,29 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateRecipe() {
|
||||
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
|
||||
if (data && data.slug) {
|
||||
router.push(`/recipe/${data.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { copyText } = useCopy();
|
||||
const groupSlug = ref<string>("");
|
||||
|
||||
async function setGroupSlug() {
|
||||
if (groupSlug.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.users.getSelfGroup();
|
||||
if (data) {
|
||||
groupSlug.value = data.slug;
|
||||
} else {
|
||||
// @ts-ignore this will either be a string or undefined
|
||||
groupSlug.value = $auth.user?.groupId
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Print is handled as an event in the parent component
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
@@ -338,23 +528,34 @@ export default defineComponent({
|
||||
},
|
||||
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"),
|
||||
download: handleDownloadEvent,
|
||||
duplicate: () => {
|
||||
state.recipeDuplicateDialog = true;
|
||||
},
|
||||
mealplanner: () => {
|
||||
state.mealplannerDialog = true;
|
||||
},
|
||||
printPreferences: async () => {
|
||||
if (!recipeRef.value) {
|
||||
await refreshRecipe();
|
||||
}
|
||||
state.printPreferencesDialog = true;
|
||||
},
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
|
||||
state.shoppingListDialog = true;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
},
|
||||
share: () => {
|
||||
state.shareDialog = true;
|
||||
},
|
||||
publicUrl: () => {
|
||||
if (!props.groupId) {
|
||||
alert.error("Unknown group ID");
|
||||
console.error("prop `groupId` is required when requesting a public URL");
|
||||
publicUrl: async () => {
|
||||
await setGroupSlug();
|
||||
if (!groupSlug.value) {
|
||||
return;
|
||||
}
|
||||
copyText(`${window.location.origin}/explore/recipes/${props.groupId}/${props.slug}`);
|
||||
|
||||
copyText(`${window.location.origin}/explore/recipes/${groupSlug.value}/${props.slug}`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -371,16 +572,33 @@ export default defineComponent({
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
const planTypeOptions = usePlanTypeOptions();
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
recipeRef,
|
||||
shoppingLists,
|
||||
selectedShoppingList,
|
||||
openShoppingListIngredientDialog,
|
||||
addRecipeToList,
|
||||
bulkCheckIngredients,
|
||||
duplicateRecipe,
|
||||
contextMenuEventHandler,
|
||||
deleteRecipe,
|
||||
addRecipeToPlan,
|
||||
icon,
|
||||
planTypeOptions,
|
||||
recipeIngredients,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.ingredient-grid {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
<td colspan="4"></td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #item.name="{ item }">
|
||||
<a :href="`/recipe/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a>
|
||||
</template>
|
||||
<template #item.tags="{ item }">
|
||||
<RecipeChip small :items="item.tags" :is-category="false" url-prefix="tags" />
|
||||
</template>
|
||||
@@ -28,9 +31,7 @@
|
||||
</template>
|
||||
<template #item.userId="{ item }">
|
||||
<v-list-item class="justify-start">
|
||||
<v-list-item-avatar>
|
||||
<img src="https://i.pravatar.cc/300" alt="John" />
|
||||
</v-list-item-avatar>
|
||||
<UserAvatar :user-id="item.userId" size="40" />
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ getMember(item.userId) }}
|
||||
@@ -43,6 +44,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import UserAvatar from "../User/UserAvatar.vue";
|
||||
import RecipeChip from "./RecipeChips.vue";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
@@ -61,7 +63,7 @@ interface ShowHeaders {
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeChip },
|
||||
components: { RecipeChip, UserAvatar },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
:icon="$globals.icons.printerSettings"
|
||||
:title="$tc('general.print-preferences')"
|
||||
width="70%"
|
||||
max-width="816px"
|
||||
>
|
||||
<div class="pa-6">
|
||||
<v-container class="print-config mb-3 pa-0">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center" class="text-center">
|
||||
<div class="text-subtitle-2" style="text-align: center;">{{ $tc('recipe.recipe-image') }}</div>
|
||||
<v-btn-toggle v-model="preferences.imagePosition" mandatory style="width: fit-content;">
|
||||
<v-btn :value="ImagePosition.left">
|
||||
<v-icon>{{ $globals.icons.dockLeft }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn :value="ImagePosition.right">
|
||||
<v-icon>{{ $globals.icons.dockRight }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn :value="ImagePosition.hidden">
|
||||
<v-icon>{{ $globals.icons.windowClose }}</v-icon>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-col cols="auto" align-self="start">
|
||||
<v-row no-gutters>
|
||||
<v-switch v-model="preferences.showDescription" hide-details :label="$tc('recipe.description')" />
|
||||
</v-row>
|
||||
<v-row no-gutters>
|
||||
<v-switch v-model="preferences.showNotes" hide-details :label="$tc('recipe.notes')" />
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-card
|
||||
height="fit-content"
|
||||
max-height="40vh"
|
||||
width="100%"
|
||||
class="print-preview"
|
||||
style="overflow-y: auto;"
|
||||
>
|
||||
<RecipePrintView :recipe="recipe"/>
|
||||
</v-card>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipePrintView,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipe: {
|
||||
type: Object as () => Recipe,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const preferences = useUserPrintPreferences();
|
||||
|
||||
// V-Model Support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
dialog,
|
||||
ImagePosition,
|
||||
preferences,
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -5,7 +5,7 @@
|
||||
<v-app-bar sticky dark color="primary lighten-1" :rounded="!$vuetify.breakpoint.xs">
|
||||
<v-text-field
|
||||
id="arrow-search"
|
||||
v-model="search"
|
||||
v-model="search.query.value"
|
||||
autofocus
|
||||
solo
|
||||
flat
|
||||
@@ -31,11 +31,11 @@
|
||||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
<router-link to="/search?advanced=true"> {{ $t("search.advanced-search") }} </router-link>
|
||||
<router-link to="/"> {{ $t("search.advanced-search") }} </router-link>
|
||||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
v-for="(recipe, index) in results.slice(0, 10)"
|
||||
v-for="(recipe, index) in search.data.value"
|
||||
:key="index"
|
||||
:tabindex="index"
|
||||
class="ma-1 arrow-nav"
|
||||
@@ -56,8 +56,9 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, ref, watch, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useRecipes, allRecipes, useRecipeSearch } from "~/composables/recipes";
|
||||
import { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
||||
const SELECTED_EVENT = "selected";
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -65,12 +66,9 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(_, context) {
|
||||
const { refreshRecipes } = useRecipes(true, false);
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
searchResults: [],
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
@@ -78,14 +76,11 @@ export default defineComponent({
|
||||
const dialog = ref(false);
|
||||
|
||||
// Reset or Grab Recipes on Change
|
||||
watch(dialog, async (val) => {
|
||||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
search.value = "";
|
||||
search.query.value = "";
|
||||
state.selectedIndex = -1;
|
||||
} else if (allRecipes.value && allRecipes.value.length <= 0) {
|
||||
state.loading = true;
|
||||
await refreshRecipes();
|
||||
state.loading = false;
|
||||
search.data.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,9 +140,9 @@ export default defineComponent({
|
||||
|
||||
// ===========================================================================
|
||||
// Basic Search
|
||||
const api = useUserApi();
|
||||
const search = useRecipeSearch(api);
|
||||
|
||||
const { search, results } = useRecipeSearch(allRecipes);
|
||||
// ===========================================================================
|
||||
// Select Handler
|
||||
|
||||
function handleSelect(recipe: RecipeSummary) {
|
||||
@@ -155,13 +150,20 @@ export default defineComponent({
|
||||
context.emit(SELECTED_EVENT, recipe);
|
||||
}
|
||||
|
||||
return { allRecipes, refreshRecipes, ...toRefs(state), dialog, open, close, handleSelect, search, results };
|
||||
return {
|
||||
...toRefs(state),
|
||||
dialog,
|
||||
open,
|
||||
close,
|
||||
handleSelect,
|
||||
search,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.scroll {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,7 +84,6 @@ export default defineComponent({
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
console.log(val);
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
495
frontend/components/Domain/Recipe/RecipeExplorerPage.vue
Normal file
495
frontend/components/Domain/Recipe/RecipeExplorerPage.vue
Normal file
@@ -0,0 +1,495 @@
|
||||
<template>
|
||||
<v-container fluid class="pa-0">
|
||||
<div class="search-container py-8">
|
||||
<form class="search-box pa-2" @submit.prevent="search">
|
||||
<div class="d-flex justify-center my-2">
|
||||
<v-text-field
|
||||
ref="input"
|
||||
v-model="state.search"
|
||||
outlined
|
||||
hide-details
|
||||
clearable
|
||||
color="primary"
|
||||
:placeholder="$tc('search.search-placeholder')"
|
||||
:prepend-inner-icon="$globals.icons.search"
|
||||
@keyup.enter="hideKeyboard"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<!-- Category Filter -->
|
||||
<SearchFilter
|
||||
v-if="categories"
|
||||
v-model="selectedCategories"
|
||||
:require-all.sync="state.requireAllCategories"
|
||||
:items="categories"
|
||||
>
|
||||
<v-icon left>
|
||||
{{ $globals.icons.tags }}
|
||||
</v-icon>
|
||||
{{ $t("category.categories") }}
|
||||
</SearchFilter>
|
||||
|
||||
<!-- Tag Filter -->
|
||||
<SearchFilter v-if="tags" v-model="selectedTags" :require-all.sync="state.requireAllTags" :items="tags">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.tags }}
|
||||
</v-icon>
|
||||
{{ $t("tag.tags") }}
|
||||
</SearchFilter>
|
||||
|
||||
<!-- Tool Filter -->
|
||||
<SearchFilter v-if="tools" v-model="selectedTools" :require-all.sync="state.requireAllTools" :items="tools">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.tools }}
|
||||
</v-icon>
|
||||
{{ $t("tool.tools") }}
|
||||
</SearchFilter>
|
||||
|
||||
<!-- Food Filter -->
|
||||
<SearchFilter v-if="foods" v-model="selectedFoods" :require-all.sync="state.requireAllFoods" :items="foods">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.foods }}
|
||||
</v-icon>
|
||||
{{ $t("general.foods") }}
|
||||
</SearchFilter>
|
||||
|
||||
<!-- Sort Options -->
|
||||
<v-menu offset-y nudge-bottom="3">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="ml-auto" small color="accent" v-bind="attrs" v-on="on">
|
||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||
{{ state.orderDirection === "asc" ? $globals.icons.sortAscending : $globals.icons.sortDescending }}
|
||||
</v-icon>
|
||||
{{ $vuetify.breakpoint.xsOnly ? null : sortText }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item @click="toggleOrderDirection()">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.sort }}
|
||||
</v-icon>
|
||||
<v-list-item-title>
|
||||
{{ state.orderDirection === "asc" ? "Sort Descending" : "Sort Ascending" }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="v in sortable"
|
||||
:key="v.name"
|
||||
:input-value="state.orderBy === v.value"
|
||||
@click="state.orderBy = v.value"
|
||||
>
|
||||
<v-icon left>
|
||||
{{ v.icon }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ v.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- Settings -->
|
||||
<v-menu offset-y bottom left nudge-bottom="3" :close-on-content-click="false">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn small color="accent" dark v-bind="attrs" v-on="on">
|
||||
<v-icon small>
|
||||
{{ $globals.icons.cog }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-switch v-model="state.auto" :label="$t('search.auto-search')" single-line></v-switch>
|
||||
<v-btn block color="primary" @click="reset">
|
||||
{{ $tc("general.reset") }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</div>
|
||||
<div v-if="!state.auto" class="search-button-container">
|
||||
<v-btn x-large color="primary" type="submit" block>
|
||||
<v-icon left>
|
||||
{{ $globals.icons.search }}
|
||||
</v-icon>
|
||||
{{ $tc("search.search") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<v-container class="mt-6 px-md-6">
|
||||
<RecipeCardSection
|
||||
class="mt-n5"
|
||||
:icon="$globals.icons.search"
|
||||
:title="$tc('search.results')"
|
||||
:group-slug="groupSlug"
|
||||
:recipes="recipes"
|
||||
:query="passedQuery"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
/>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref } from "@nuxtjs/composition-api";
|
||||
import { watchDebounced } from "@vueuse/shared";
|
||||
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
||||
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||
import { usePublicCategoryStore } from "~/composables/store/use-category-store";
|
||||
import { usePublicFoodStore } from "~/composables/store/use-food-store";
|
||||
import { usePublicTagStore } from "~/composables/store/use-tag-store";
|
||||
import { usePublicToolStore } from "~/composables/store/use-tool-store";
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchFilter, RecipeCardSection },
|
||||
props: {
|
||||
groupSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const router = useRouter();
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const state = ref({
|
||||
auto: true,
|
||||
search: "",
|
||||
orderBy: "created_at",
|
||||
orderDirection: "desc" as "asc" | "desc",
|
||||
|
||||
// and/or
|
||||
requireAllCategories: false,
|
||||
requireAllTags: false,
|
||||
requireAllTools: false,
|
||||
requireAllFoods: false,
|
||||
});
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const categories = loggedIn.value ? useCategoryStore() : usePublicCategoryStore(props.groupSlug);
|
||||
const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]);
|
||||
|
||||
const foods = loggedIn.value ? useFoodStore() : usePublicFoodStore(props.groupSlug);
|
||||
const selectedFoods = ref<IngredientFood[]>([]);
|
||||
|
||||
const tags = loggedIn.value ? useTagStore() : usePublicTagStore(props.groupSlug);
|
||||
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
||||
|
||||
const tools = loggedIn.value ? useToolStore() : usePublicToolStore(props.groupSlug);
|
||||
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
|
||||
|
||||
const passedQuery = ref<RecipeSearchQuery | null>(null);
|
||||
|
||||
function reset() {
|
||||
state.value.search = "";
|
||||
state.value.orderBy = "created_at";
|
||||
state.value.orderDirection = "desc";
|
||||
state.value.requireAllCategories = false;
|
||||
state.value.requireAllTags = false;
|
||||
state.value.requireAllTools = false;
|
||||
state.value.requireAllFoods = false;
|
||||
selectedCategories.value = [];
|
||||
selectedFoods.value = [];
|
||||
selectedTags.value = [];
|
||||
selectedTools.value = [];
|
||||
|
||||
router.push({
|
||||
query: {},
|
||||
});
|
||||
|
||||
search();
|
||||
}
|
||||
|
||||
function toggleOrderDirection() {
|
||||
state.value.orderDirection = state.value.orderDirection === "asc" ? "desc" : "asc";
|
||||
}
|
||||
|
||||
function toIDArray(array: { id: string }[]) {
|
||||
return array.map((item) => item.id);
|
||||
}
|
||||
|
||||
function hideKeyboard() {
|
||||
input.value.blur()
|
||||
}
|
||||
|
||||
const input: Ref<any> = ref(null);
|
||||
|
||||
async function search() {
|
||||
await router.push({
|
||||
query: {
|
||||
categories: toIDArray(selectedCategories.value),
|
||||
foods: toIDArray(selectedFoods.value),
|
||||
tags: toIDArray(selectedTags.value),
|
||||
tools: toIDArray(selectedTools.value),
|
||||
// Only add the query param if it's or not default
|
||||
...{
|
||||
auto: state.value.auto ? undefined : "false",
|
||||
search: state.value.search === "" ? undefined : state.value.search,
|
||||
orderBy: state.value.orderBy === "createdAt" ? undefined : state.value.orderBy,
|
||||
orderDirection: state.value.orderDirection === "desc" ? undefined : state.value.orderDirection,
|
||||
requireAllCategories: state.value.requireAllCategories ? "true" : undefined,
|
||||
requireAllTags: state.value.requireAllTags ? "true" : undefined,
|
||||
requireAllTools: state.value.requireAllTools ? "true" : undefined,
|
||||
requireAllFoods: state.value.requireAllFoods ? "true" : undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
passedQuery.value = {
|
||||
search: state.value.search,
|
||||
categories: toIDArray(selectedCategories.value),
|
||||
foods: toIDArray(selectedFoods.value),
|
||||
tags: toIDArray(selectedTags.value),
|
||||
tools: toIDArray(selectedTools.value),
|
||||
requireAllCategories: state.value.requireAllCategories,
|
||||
requireAllTags: state.value.requireAllTags,
|
||||
requireAllTools: state.value.requireAllTools,
|
||||
requireAllFoods: state.value.requireAllFoods,
|
||||
orderBy: state.value.orderBy,
|
||||
orderDirection: state.value.orderDirection,
|
||||
_searchSeed: Date.now().toString()
|
||||
};
|
||||
}
|
||||
|
||||
function waitUntilAndExecute(
|
||||
condition: () => boolean,
|
||||
callback: () => void,
|
||||
opts = { timeout: 2000, interval: 500 }
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = {
|
||||
timeout: undefined as number | undefined,
|
||||
interval: undefined as number | undefined,
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
if (condition()) {
|
||||
clearInterval(state.interval);
|
||||
clearTimeout(state.timeout);
|
||||
callback();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// For some reason these were returning NodeJS.Timeout
|
||||
state.interval = setInterval(check, opts.interval) as unknown as number;
|
||||
state.timeout = setTimeout(() => {
|
||||
clearInterval(state.interval);
|
||||
reject(new Error("Timeout"));
|
||||
}, opts.timeout) as unknown as number;
|
||||
});
|
||||
}
|
||||
|
||||
const sortText = computed(() => {
|
||||
const sort = sortable.find((s) => s.value === state.value.orderBy);
|
||||
if (!sort) return "";
|
||||
return `${sort.name}`;
|
||||
});
|
||||
|
||||
const sortable = [
|
||||
{
|
||||
icon: $globals.icons.orderAlphabeticalAscending,
|
||||
name: i18n.tc("general.sort-alphabetically"),
|
||||
value: "name",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.newBox,
|
||||
name: i18n.tc("general.created"),
|
||||
value: "created_at",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.chefHat,
|
||||
name: i18n.tc("general.last-made"),
|
||||
value: "last_made",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.star,
|
||||
name: i18n.tc("general.rating"),
|
||||
value: "rating",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.update,
|
||||
name: i18n.tc("general.updated"),
|
||||
value: "update_at",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.diceMultiple,
|
||||
name: i18n.tc("general.random"),
|
||||
value: "random",
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
// Hydrate Search
|
||||
// wait for stores to be hydrated
|
||||
|
||||
// read query params
|
||||
const query = router.currentRoute.query;
|
||||
|
||||
if (query.auto) {
|
||||
state.value.auto = query.auto === "true";
|
||||
}
|
||||
|
||||
if (query.search) {
|
||||
state.value.search = query.search as string;
|
||||
}
|
||||
|
||||
if (query.orderBy) {
|
||||
state.value.orderBy = query.orderBy as string;
|
||||
}
|
||||
|
||||
if (query.orderDirection) {
|
||||
state.value.orderDirection = query.orderDirection as "asc" | "desc";
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (query.categories) {
|
||||
promises.push(
|
||||
waitUntilAndExecute(
|
||||
() => categories.items.value.length > 0,
|
||||
() => {
|
||||
const result = categories.items.value.filter((item) =>
|
||||
(query.categories as string[]).includes(item.id as string)
|
||||
);
|
||||
|
||||
selectedCategories.value = result as NoUndefinedField<RecipeCategory>[];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (query.foods) {
|
||||
promises.push(
|
||||
waitUntilAndExecute(
|
||||
() => {
|
||||
if (foods.foods.value) {
|
||||
return foods.foods.value.length > 0;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
() => {
|
||||
const result = foods.foods.value?.filter((item) => (query.foods as string[]).includes(item.id));
|
||||
selectedFoods.value = result ?? [];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (query.tags) {
|
||||
promises.push(
|
||||
waitUntilAndExecute(
|
||||
() => tags.items.value.length > 0,
|
||||
() => {
|
||||
const result = tags.items.value.filter((item) => (query.tags as string[]).includes(item.id as string));
|
||||
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (query.tools) {
|
||||
promises.push(
|
||||
waitUntilAndExecute(
|
||||
() => tools.items.value.length > 0,
|
||||
() => {
|
||||
const result = tools.items.value.filter((item) => (query.tools as string[]).includes(item.id));
|
||||
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Promise.allSettled(promises).then(() => {
|
||||
search();
|
||||
});
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
[
|
||||
() => state.value.search,
|
||||
() => state.value.requireAllCategories,
|
||||
() => state.value.requireAllTags,
|
||||
() => state.value.requireAllTools,
|
||||
() => state.value.requireAllFoods,
|
||||
() => state.value.orderBy,
|
||||
() => state.value.orderDirection,
|
||||
selectedCategories,
|
||||
selectedFoods,
|
||||
selectedTags,
|
||||
selectedTools,
|
||||
],
|
||||
async () => {
|
||||
if (state.value.auto) {
|
||||
await search();
|
||||
}
|
||||
},
|
||||
{
|
||||
debounce: 500,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
sortText,
|
||||
search,
|
||||
reset,
|
||||
state,
|
||||
categories: categories.items as unknown as NoUndefinedField<RecipeCategory>[],
|
||||
tags: tags.items as unknown as NoUndefinedField<RecipeTag>[],
|
||||
foods: foods.foods,
|
||||
tools: tools.items as unknown as NoUndefinedField<RecipeTool>[],
|
||||
|
||||
sortable,
|
||||
toggleOrderDirection,
|
||||
hideKeyboard,
|
||||
input,
|
||||
|
||||
selectedCategories,
|
||||
selectedFoods,
|
||||
selectedTags,
|
||||
selectedTools,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
passedQuery,
|
||||
};
|
||||
},
|
||||
head: {},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.search-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 950px;
|
||||
}
|
||||
|
||||
.search-button-container {
|
||||
margin: 3rem auto 0 auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
@@ -32,6 +32,7 @@
|
||||
<v-autocomplete
|
||||
v-model="value.unit"
|
||||
:search-input.sync="unitSearch"
|
||||
auto-select-first
|
||||
hide-details
|
||||
dense
|
||||
solo
|
||||
@@ -59,6 +60,7 @@
|
||||
<v-autocomplete
|
||||
v-model="value.food"
|
||||
:search-input.sync="foodSearch"
|
||||
auto-select-first
|
||||
hide-details
|
||||
dense
|
||||
solo
|
||||
@@ -99,21 +101,10 @@
|
||||
hover
|
||||
:large="false"
|
||||
class="my-auto"
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $tc('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.dotsVertical,
|
||||
text: $tc('general.menu'),
|
||||
event: 'open',
|
||||
children: contextMenuOptions,
|
||||
},
|
||||
]"
|
||||
:buttons="btns"
|
||||
@toggle-section="toggleTitle"
|
||||
@toggle-original="toggleOriginalText"
|
||||
@insert-ingredient="$emit('insert-ingredient')"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
@@ -143,9 +134,68 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowInsertIngredient: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { i18n } = useContext();
|
||||
setup(props, { listeners }) {
|
||||
const { i18n, $globals } = useContext();
|
||||
|
||||
const contextMenuOptions = computed(() => {
|
||||
const options = [
|
||||
{
|
||||
text: i18n.tc("recipe.toggle-section"),
|
||||
event: "toggle-section",
|
||||
},
|
||||
];
|
||||
|
||||
if (props.allowInsertIngredient) {
|
||||
options.push({
|
||||
text: i18n.tc("recipe.insert-ingredient") ,
|
||||
event: "insert-ingredient",
|
||||
})
|
||||
}
|
||||
|
||||
// FUTURE: add option to parse a single ingredient
|
||||
// if (!value.food && !value.unit && value.note) {
|
||||
// options.push({
|
||||
// text: "Parse Ingredient",
|
||||
// event: "parse-ingredient",
|
||||
// });
|
||||
// }
|
||||
|
||||
if (props.value.originalText) {
|
||||
options.push({
|
||||
text: i18n.tc("recipe.see-original-text"),
|
||||
event: "toggle-original",
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const btns = computed(() => {
|
||||
const out = [
|
||||
{
|
||||
icon: $globals.icons.dotsVertical,
|
||||
text: i18n.tc("general.menu"),
|
||||
event: "open",
|
||||
children: contextMenuOptions.value,
|
||||
},
|
||||
];
|
||||
|
||||
if (listeners && listeners.delete) {
|
||||
// @ts-expect-error - TODO: fix this
|
||||
out.unshift({
|
||||
icon: $globals.icons.delete,
|
||||
text: i18n.tc("general.delete"),
|
||||
event: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
// ==================================================
|
||||
// Foods
|
||||
@@ -155,8 +205,7 @@ export default defineComponent({
|
||||
|
||||
async function createAssignFood() {
|
||||
foodData.data.name = foodSearch.value;
|
||||
await foodStore.actions.createOne(foodData.data);
|
||||
props.value.food = foodStore.foods.value?.find((food) => food.name === foodSearch.value);
|
||||
props.value.food = await foodStore.actions.createOne(foodData.data) || undefined;
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
@@ -168,8 +217,7 @@ export default defineComponent({
|
||||
|
||||
async function createAssignUnit() {
|
||||
unitsData.data.name = unitSearch.value;
|
||||
await unitStore.actions.createOne(unitsData.data);
|
||||
props.value.unit = unitStore.units.value?.find((unit) => unit.name === unitSearch.value);
|
||||
props.value.unit = await unitStore.actions.createOne(unitsData.data) || undefined;
|
||||
unitsData.reset();
|
||||
}
|
||||
|
||||
@@ -209,32 +257,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
const contextMenuOptions = computed(() => {
|
||||
const options = [
|
||||
{
|
||||
text: i18n.t("recipe.toggle-section") as string,
|
||||
event: "toggle-section",
|
||||
},
|
||||
];
|
||||
|
||||
// FUTURE: add option to parse a single ingredient
|
||||
// if (!value.food && !value.unit && value.note) {
|
||||
// options.push({
|
||||
// text: "Parse Ingredient",
|
||||
// event: "parse-ingredient",
|
||||
// });
|
||||
// }
|
||||
|
||||
if (props.value.originalText) {
|
||||
options.push({
|
||||
text: i18n.t("recipe.see-original-text") as string,
|
||||
event: "toggle-original",
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
function quantityFilter(e: KeyboardEvent) {
|
||||
// if digit is pressed, add to quantity
|
||||
if (e.key === "-" || e.key === "+" || e.key === "e") {
|
||||
@@ -259,6 +281,7 @@ export default defineComponent({
|
||||
unitSearch,
|
||||
validators,
|
||||
workingUnitData: unitsData.data,
|
||||
btns,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div v-html="markup"></div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="safeMarkup"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
markup: {
|
||||
@@ -11,5 +13,11 @@ export default defineComponent({
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
||||
return {
|
||||
safeMarkup,
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="ma-0 pa-0 text-subtitle-1 dense-markdown ingredient-item">
|
||||
<SafeMarkdown v-if="parsedIng.quantity" class="d-inline" :source="parsedIng.quantity" />
|
||||
<template v-if="parsedIng.unit">{{ parsedIng.unit }} </template>
|
||||
<SafeMarkdown v-if="parsedIng.note && !parsedIng.name" class="text-bold d-inline" :source="parsedIng.note" />
|
||||
<template v-else>
|
||||
<SafeMarkdown v-if="parsedIng.name" class="text-bold d-inline" :source="parsedIng.name" />
|
||||
<SafeMarkdown v-if="parsedIng.note" class="note" :source="parsedIng.note" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { RecipeIngredient } from "~/lib/api/types/group";
|
||||
import { useParsedIngredientText } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ingredient: {
|
||||
type: Object as () => RecipeIngredient,
|
||||
required: true,
|
||||
},
|
||||
disableAmount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const parsedIng = computed(() => {
|
||||
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
|
||||
});
|
||||
|
||||
return {
|
||||
parsedIng,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.ingredient-item {
|
||||
.d-inline {
|
||||
& > p {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
line-height: 1.25em;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user