From e34079673c3b1e7515a81e6c82614cf15cc4a538 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 11 Jun 2021 21:57:59 -0800 Subject: [PATCH] Docs/v0.5.0 second pass (#496) * update docs * use auto-gen routes * dumb deps * remove whitespace * github action to build dev docs container * no cache Co-authored-by: hay-kot --- .github/workflows/dockerbuild.dev.yml | 22 +++ .gitignore | 24 +-- Caddyfile.dev | 30 +++- dev/scripts/output/app_routes.py | 1 + dev/scripts/templates/js_index.j2 | 7 + dev/scripts/templates/js_requests.j2 | 19 +++ dev/scripts/templates/js_routes.j2 | 7 + dev/scripts/templates/pytest_routes.j2 | 12 ++ docs/Caddyfile | 15 ++ docs/Dockerfile | 10 ++ docs/docker-compose.yml | 11 ++ docs/docs/assets/img/api-key-image-v1.webp | Bin 0 -> 44240 bytes .../changelog/{template.md => .template.md} | 0 docs/docs/changelog/v0.5.0.md | 45 +++-- .../docs/documentation/admin/site-settings.md | 16 +- .../getting-started/api-usage.md | 23 +-- docs/docs/documentation/recipes/recipes.md | 34 +++- .../{toolbox-intro.md => notifications.md} | 27 +-- .../documentation/toolbox/organize-tools.md | 17 ++ .../users-groups/meal-planner.md | 5 +- .../users-groups/user-settings.md | 16 +- docs/docs/overrides/api.html | 2 +- docs/mkdocs.yml | 8 +- frontend/package-lock.json | 154 ++++++++++++----- frontend/src/api/about.js | 35 +--- frontend/src/api/api-utils.js | 5 +- frontend/src/api/apiRoutes.js | 161 +++++++++--------- frontend/src/api/backup.js | 23 +-- frontend/src/api/category.js | 46 ++--- frontend/src/api/groups.js | 21 +-- frontend/src/api/mealplan.js | 32 +--- frontend/src/api/meta.js | 28 +-- frontend/src/api/migration.js | 17 +- frontend/src/api/recipe.js | 48 ++---- frontend/src/api/settings.js | 16 +- frontend/src/api/signUps.js | 19 +-- frontend/src/api/siteSettings.js | 28 +-- frontend/src/api/themes.js | 22 +-- frontend/src/api/users.js | 42 ++--- .../components/UI/Dialogs/ImportDialog.vue | 4 +- frontend/src/main.js | 2 +- frontend/src/utils/globals.js | 9 - 42 files changed, 555 insertions(+), 508 deletions(-) create mode 100644 dev/scripts/templates/js_index.j2 create mode 100644 dev/scripts/templates/js_requests.j2 create mode 100644 dev/scripts/templates/js_routes.j2 create mode 100644 dev/scripts/templates/pytest_routes.j2 create mode 100644 docs/Caddyfile create mode 100644 docs/Dockerfile create mode 100644 docs/docker-compose.yml create mode 100644 docs/docs/assets/img/api-key-image-v1.webp rename docs/docs/changelog/{template.md => .template.md} (100%) rename docs/docs/documentation/toolbox/{toolbox-intro.md => notifications.md} (63%) create mode 100644 docs/docs/documentation/toolbox/organize-tools.md diff --git a/.github/workflows/dockerbuild.dev.yml b/.github/workflows/dockerbuild.dev.yml index db57872a..8e4949bb 100644 --- a/.github/workflows/dockerbuild.dev.yml +++ b/.github/workflows/dockerbuild.dev.yml @@ -6,6 +6,28 @@ on: - dev jobs: + push_to_registry: + name: Push Docker image to GitHub Packages + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Check out the repo + uses: actions/checkout@v2 + - name: Log in to GitHub Docker Registry + uses: docker/login-action@v1 + with: + registry: docker.pkg.github.com + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build container image + uses: docker/build-push-action@v2 + with: + push: true + tags: | + context: ./docs + docker.pkg.github.com/${{ github.repository }}/dev-docs:latest build: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index da82f89e..506b3f2a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ # frontend/.env.development docs/site/ -*temp* +*temp/* .secret dev/data/backups/* @@ -147,24 +147,4 @@ dev/data/backups/dev_sample_data*.zip !dev/data/backups/test*.zip dev/data/recipes/* dev/scripts/output/app_routes.py -dev/scripts/output/javascriptAPI/apiRoutes.js -dev/scripts/output/javascriptAPI/appEvents.js -dev/scripts/output/javascriptAPI/authentication.js -dev/scripts/output/javascriptAPI/backups.js -dev/scripts/output/javascriptAPI/debug.js -dev/scripts/output/javascriptAPI/groups.js -dev/scripts/output/javascriptAPI/index.js -dev/scripts/output/javascriptAPI/mealPlan.js -dev/scripts/output/javascriptAPI/migration.js -dev/scripts/output/javascriptAPI/queryAllRecipes.js -dev/scripts/output/javascriptAPI/recipeCategories.js -dev/scripts/output/javascriptAPI/recipeCRUD.js -dev/scripts/output/javascriptAPI/recipeTags.js -dev/scripts/output/javascriptAPI/settings.js -dev/scripts/output/javascriptAPI/shoppingLists.js -dev/scripts/output/javascriptAPI/siteMedia.js -dev/scripts/output/javascriptAPI/themes.js -dev/scripts/output/javascriptAPI/userAPITokens.js -dev/scripts/output/javascriptAPI/users.js -dev/scripts/output/javascriptAPI/userSignup.js -dev/scripts/output/javascriptAPI/utils.js +dev/scripts/output/javascriptAPI/* \ No newline at end of file diff --git a/Caddyfile.dev b/Caddyfile.dev index f8d99bb3..a0a7efa6 100644 --- a/Caddyfile.dev +++ b/Caddyfile.dev @@ -2,8 +2,32 @@ admin off } +# Add gzip compression to requests +(webconf) { + encode gzip +} + +# Add forward headers to requests +(theheaders) { + header_up X-Forwarded-Ssl on + header_up Host {host} + header_up X-Real-IP {remote} + header_up X-Forwarded-For {remote} + header_up X-Forwarded-Port {server_port} + header_up X-Forwarded-Proto {scheme} + header_up X-Url-Scheme {scheme} + header_up X-Forwarded-Host {host} +} + localhost { - handle /mealie/* { - reverse_proxy http://127.0.0.1:9090 - } + log + redir /dev-docs /dev-docs/ + route /dev-docs* { + + uri strip_prefix dev-docs + reverse_proxy localhost:8888 { + import theheaders + } + import webconf +} } \ No newline at end of file diff --git a/dev/scripts/output/app_routes.py b/dev/scripts/output/app_routes.py index c1e33059..81ff81cf 100644 --- a/dev/scripts/output/app_routes.py +++ b/dev/scripts/output/app_routes.py @@ -36,6 +36,7 @@ class AppRoutes: self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized" self.recipes_summary_untagged = "/api/recipes/summary/untagged" self.recipes_tag = "/api/recipes/tag" + self.recipes_test_scrape_url = "/api/recipes/test-scrape-url" self.shopping_lists = "/api/shopping-lists" self.site_settings = "/api/site-settings" self.site_settings_custom_pages = "/api/site-settings/custom-pages" diff --git a/dev/scripts/templates/js_index.j2 b/dev/scripts/templates/js_index.j2 new file mode 100644 index 00000000..67cb2f29 --- /dev/null +++ b/dev/scripts/templates/js_index.j2 @@ -0,0 +1,7 @@ +{% for api in files.files %} +import { {{ api }}API } from "./{{api}}.js" {% endfor %} + +export const api = { +{% for api in files.files %} + {{api}}: {{api}}API, {% endfor %} +} \ No newline at end of file diff --git a/dev/scripts/templates/js_requests.j2 b/dev/scripts/templates/js_requests.j2 new file mode 100644 index 00000000..4cdc1c78 --- /dev/null +++ b/dev/scripts/templates/js_requests.j2 @@ -0,0 +1,19 @@ +// This Content is Auto Generated +import { API_ROUTES } from "./apiRoutes" + +export const {{paths.export_name}}API = { {% for path in paths.all_paths %} {% for verb in path.http_verbs %} {% if path.route_object.is_function %} + /** {{ verb.js_docs }} {% for v in path.route_object.var %} + * @param {{ v }} {% endfor %} + */ + {{ verb.summary_camel }}({{path.route_object.var|join(", ")}}) { + const response = await apiReq.{{ verb.request_type.value }}(API_ROUTES.{{ path.route_object.router_camel }}({{path.route_object.var|join(", ")}})) + return response.data + }, {% else %} + /** {{ verb.js_docs }} {% for v in path.route_object.var %} + * @param {{ v }} {% endfor %} + */ + {{ verb.summary_camel }}() { + const response = await apiReq.{{ verb.request_type.value }}(API_ROUTES.{{ path.route_object.router_camel }}) + return response.data + },{% endif %} {% endfor %} {% endfor %} +} \ No newline at end of file diff --git a/dev/scripts/templates/js_routes.j2 b/dev/scripts/templates/js_routes.j2 new file mode 100644 index 00000000..4544357c --- /dev/null +++ b/dev/scripts/templates/js_routes.j2 @@ -0,0 +1,7 @@ +// This Content is Auto Generated +const prefix = '{{paths.prefix}}' +export const API_ROUTES = { {% for path in paths.static_paths %} + {{ path.router_camel }}: `${prefix}{{ path.route }}`,{% endfor %} +{% for path in paths.function_paths %} + {{path.router_camel}}: ({{path.var|join(", ")}}) => `${prefix}{{ path.js_route }}`,{% endfor %} +} \ No newline at end of file diff --git a/dev/scripts/templates/pytest_routes.j2 b/dev/scripts/templates/pytest_routes.j2 new file mode 100644 index 00000000..ecd9e09c --- /dev/null +++ b/dev/scripts/templates/pytest_routes.j2 @@ -0,0 +1,12 @@ +# This Content is Auto Generated for Pytest + + +class AppRoutes: + def __init__(self) -> None: + self.prefix = '{{paths.prefix}}' +{% for path in paths.static_paths %} + self.{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}"{% endfor %} +{% for path in paths.function_paths %} + def {{path.router_slug}}(self, {{path.var|join(", ")}}): + return f"{self.prefix}{{ path.route }}" +{% endfor %} \ No newline at end of file diff --git a/docs/Caddyfile b/docs/Caddyfile new file mode 100644 index 00000000..8c88f454 --- /dev/null +++ b/docs/Caddyfile @@ -0,0 +1,15 @@ +{ + auto_https off +} + +:80 { + root * /srv + encode gzip + uri strip_suffix / + + handle { + try_files {path} {path}/ /index.html + file_server + } + +} \ No newline at end of file diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..28300928 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.8-slim as build-stage +WORKDIR /app +RUN pip install --no-cache-dir mkdocs mkdocs-material +COPY . . +RUN mkdocs build + +FROM caddy:alpine +WORKDIR /app +COPY ./Caddyfile /etc/caddy/Caddyfile +COPY --from=build-stage /app/site /srv \ No newline at end of file diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml new file mode 100644 index 00000000..aec59ada --- /dev/null +++ b/docs/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" +services: + wiki: + container_name: mealie-docs + image: mealie-docs + ports: + - 8888:80 + build: + context: . + dockerfile: Dockerfile + restart: always diff --git a/docs/docs/assets/img/api-key-image-v1.webp b/docs/docs/assets/img/api-key-image-v1.webp new file mode 100644 index 0000000000000000000000000000000000000000..ab00b613e563a0f4ab8c556f090726eb2b7fb3a0 GIT binary patch literal 44240 zcmV)dK&QV_Nk&G_5m}NX*=-YDGuXgP-~Yq_7rW5Q z{)_1U1Q4%=WR$TD=+zLYENq)x9`koVNV087%c9S-CNZ`Rr)HNmvL>xtPf8VNwcGBG zM)YIO_KinRk4)OeS8T9&R83wv<~obsO_gt~$JTBN(sn}5chS4|7-Mg`uGxFhtz*G% zid_3$S(1tat#$)mgKfh>W?abmZhE~X35pchCLZLWrPgE=xwg+3W5jdDePmUv${ta* zVT>KwYsrrhqt|6DrKdNn{okcmKgm~q;0u7U9bdw_eWX`=uE(qUyPUX8aYZPQSN}hiiPXZZ!SoEKf+qRL! zUiX<-V0t@tE(z?t+t3%O&_y0q{2AVQCOaB-q^mh{P}O zo2RaeUo;gw5RVr3l^9qAuzA3pMABFT9}P`q;6$ynAFWrl`zIZc&j=qFMzEInx4-)4 zks;CI4<}Z*Bc&y!Xi0IOHirhag9fi0%mT@6BT15E|NqsQofX-$&nY4%0DNbJkiBCt z115ycFn}C{4IqkwOet_e(j)+`08X=dHr<>9Xd(ml@r=&gKH(Xc*d%$x zi(I(e?F}yAbx;cf-^*4-WCXyr9ZC{hyFGK6Z#9B;MAiWomDO2Z4zS?^xj2o8gKweY zh#&VO{R4y@AHbw-{E&(9d3Xz4dGaOWHUDqdwyfV-yWaPD@4dIk3VCmtH(V(wfcpq5 z24x}Tpa{H##I9xjob^1l$Czu?IknH(RVDL{r-McYZHWwdO)y9!6TXQIGQgBqC;u=e zEi&abX^TP`>;i*Ku<4Ks!cDr!0xg0YM$rLk$#7$|W-8b={zH1IkuyWoF%b z-^X3Phq#55;2k)WKoh?TLYg3SNS(;Z-C;&L<2=W*| zoE6k)YYz)u3HJ`e%#NmNe4dpm{%EoP&yl3v>1*V@_ZE3i#e_s9>~|m}q*ZxK9rWIN z@BK3~&pEd{gHeiO^e+xgm4mgyQcUv3@T=?59{2q2v6WG}{+hEFD1_C1~R~Wr-O} ztP+%1CDvc$k;|-ZBLyi%c^k)*E%QPfqH@aki!GHW@Hb?qLQ{&!2pvZ(jklg~sbj5l z%3AQmvY1t3l~W521#H_)H`+IIkfuoLq;9j#{$0Dac7NNpZQHi(wT;?tQzL3@HBC-t zJZsx_Y_@G%*LA;NaS1fHR*h!cc55lyE!Vc8wS3JtY(P6&(%b->)ixrk6^{SC0}>-i zj+8xzCuVf(1POm_?EkHn?d$!^}&5@P;jgc^1qbOQ}FR>g7FQ=450V*%hcjOMbLjo~Xjih6?9cI!D%c zRAEm1u9u+`?Fm<)5{bJFyf8giu1Aw_oyep69&cM1~eQD%-x?&aF zH~wZ$-?_b$SKft3fY@kJxS&pA!?25ej>CaeV^O$3Y#c}xhs4D_@f=-^(;>oQ?hm%Z zftSDl5-yrhuo2c_Zgha#J4cfz$9{EGFtCIRNl6+FXCmaChcBYM--`qF_+;2YxDc>p zwdJw(kGyx7Mt}oxf!MfIt4JQo;EFnz<46q2xDw8F=_m$~qX~!4V2e0zFkHz1ptPN!rvApHsBj8A4zOBc8?rPVk2WyPaeb-^N_1k*p%da$4y3A3 zqNURzbm=O<%Pfx$2EBsO0a$Z2rF=)|f0SI6a zD~q$z;%i`{fHt+&s#qB?iXPfjwkY z$$hf#R}pMdSrBV8MzrC?1+*w6R`GRLjYn2_#P2kb7q97Gbg;k_G&fv|C==s#pd5$` z7{SE+?i^Rg8EluXR;RPrE=8CiqE}I`Bi698z#Fgkc}F{4>|P@W?EX_P)c>jBm_ShZ)93jY-%u1X z5NuoEIeRhJU&GR@Ac`Ft6;oH1`Dzb)`{Q}_$8ybOP7@dP$Xf1ra@_tT-}#OwBZ}u9 zVap^q5aj}wZO!T`p1Fq|YOAZty`sn`|J8kFdGz^SxGL}QJ+1yM&g6z`1w=jVzwTbl zEGe&emq>fq!@h1(vA#Q1)E$qForXSRXAyPt&3Wbfhfz3?g>pX4jXddT?|8!7?sT~# zRV9jLx!!VzPkPcPJ;hBoyC}UbkO7qXFV|gzB6KKY=K%cVt&0qMCYGtGo0}@``EHk$pm`ZXEcX>7Ku zyM3V!1P*zD@4Va4ubgFEuJ92xzUZ_Yd+2w8CK1zE0BPcBVsRi6&rax#H@F}ve|^n4 zn^9>(wEaP zJ%)~Knw-0JLfq-vYfM`>3*^RG&PD08M_8IQOcTf4ECtE)kkS63K5795Bb-vxAnaUcimexJ*> z>acKkYu@=2c5970987hb$7+wOopg_RE|Td2|Fnt8eZ!om)PekwkJ+u)VpC#|Q#mME znmC!Cj6Ps%$1mV3@%-dBLyQx3ESkm3y4%JfiQ=eP!BDCSt{zcT? zE$lJ&QMJ7419^1An4enXaf2vjJT7)GdS!8D#$;)Zhumx&>aDkq`LZrbi%Z$9!+}&o z7r1o8_|rDrGQ~aZIH0KI3Z8hAGoqZI?C)LM9)poZa&NiQ(4o2Y4(BD(Z}bX~si4tYq*G0m{n=pmUISzuE`iplV$_gh}NTOfs98rWIE4>ztUYk>p(a5s~>lf|MaW=Gq2a* za^636w`kOcOF@f62NIK(kV_F1{J+1SCp>O+3S zyMN7;;-JG9oap#D{_*TsP5jGR1I&t3O)36a^=HxSb!3*LCXVCTvEyi%Z>>1>m!A4> z-JL18z5g|nrXYPA>-g$9Qv*o2XhOjX&Va^?(V2Ffvzk-aTuF=9UHY!B99f3<`k@Zv zlyy*ML~-GotU1FBk&mi3e`kv0a|oJ5`y=+DBCOssE3ib0dRE z)T#=uF3qmo=s*0uE`Qn4`ojKQZ{<(@;%QvIlNqs;lZSdd&Y&IYfTo?$9AKtcl83D2 z9EgjE&B%6*7ZmtMKK7w$FmzIO*v3)Y zXZ5K&=RTuW85h+S(wU}idXTj1x2=vEI@4$5QlFJGjScW$Z@4Odey7?`TveWD?dy(t z&Qsq4613s( zd0)G7qyPRV{9D)Zf5&-`uQ5P%m)5s?zv6Ws_WJhkPXi!U)Z4!gu`#^gFBlBfJ^H-g z?@W*K*Zm`pL;bE_Fe93xuRE9 zWUj=6PSq;Om##X~+{}sEkB_iO+o?pwLt|1%cbeBZ-03`q z?zA;XKF6QS=PJ>e4)G|)zzu+JSsitBrcdWmpO-WBbfAb%6vRpVzCX|R{e^X%G<(V4 znc8c;g6OoocP`IxJ|j0;@4|WfJ5+aPngRgPM)@|@@zrytPv=tIooSdViL1n(4pbG= znWng^U7422ZF!dS8M)K^&MyERYC)Z;(!QYGzKwN!^_=O`xm0&&YKV;}5U)=c(wUk# zI8Eo&g|nb;v}@<_?M@5oOo3KeOumJ6RP9WklS_S8&QuX63W=+N>O31Qq%$=@4Mnm) z%jp?*batry1_g*dHb-HdDWvd8 z|MV@haax4!tU-9v#r+oi506ji{=pLlq@DQ7Sga=qS;d zWvdC zU24IOGM%X+N(ul{__zb@3n}C^%umI?99dRQpY;*|2PPiX!>1Gk{F1sJRD$#i2_q;IZ33yAd7EV0~#!$1<3hIp$OJut%qx^ADW_JPw`AAIz&K=6N8n|Qn*9e zQ03`T(^!GgT{+yHDVYoauRy@F9iC*lsbfl*C|9F<9itUV&_b$}U^Gkr4!r_i-@EeK zK*!7ird}uXn(ht9XkZM4;Y{qPU%E=nTfe!%&HF!IGk@uD@~3&@3$+vk-Om6(&`OT@ ztvlH1SRz@nxvuHebce&$nU-01(Z3xVKmyT3X0h&dI4E5>Mm2aPh2J#=>Cx2YIc#$y z;S&R#J8d{Rz?n!6EC2zQhRZ!q;6@6+@t8ce-#&HBPejnbdf3j;D#w3)e}-8*P6sJ{ z%i-!wORT%-$qr#nHW0~Ik|kEA0RY6i*c3J`2>%h7MJe@Nwv+klFI}$A7;9)8ung|z94OIs_ zQYAiVq7pwvbSf&ER`uy%;T4+aSXmiFyS__wrq*y&B_SSA$%x_C1gxg!o~L#@PKO@v z!_MhCXV8#Nd2OE~BB@Byb&ga{oyxOvrWr_x0^-4rMDKW~n4Tg{B$BU-TQPb|!qCNz z#EvF9gOclr{%~KYkffwlQ}Z0!FVZbo8HG`3IvfBJ%#HafS`K;6bdCNnEm*4^%uBq) zbX^Wl2NJJk9ACJ0IEwOr`pr9j1MSMWU!+W$;+o31J}YOcav=j8z<(Xr6of6-@Zg=N zVi-74kj3}UaHu7t_gfdpGVfD5oIA%cqA4bo-V`-DQu4X-4t+ZG<*xl9t+{!QtvB1*&hn0+MThKi7s;7*2tO5k3~(cra6FZp zb#QF$mE#hcehT7n>)T(W~qm4jzI429VfUud;W~)0zD_nD0Z}Ht&NP9CpoN^Bv~x zeLHJ*+=4T!LRys+KCb?(P{x5M0F>xVuSHIZ;JuFIr@+=EAklE00+A#HGRK_CnAZrA zjq7>qRL2x{1Yd#A*T2gCyuuw9ow009$M*`f&a|^wHupTG?z}3(>;08D6{+r!_l{OO z!}4$5v7;R-nz~aFINm!F4hJBL8)Z7v1O?WGD4y#`tCgg>#Bxo`4o57^K7>w@BB?&p zy^c{vk}zd1!?p3eT54~Xb9*XZM!793O-&A!jY`DBxqv_f|Uu$U>GZM4S#Sy{;%h5V&}&<4T&uNQ;_*BMpZn z9}YOgwGQ`LY>?6J;27rxPssJ2?(>x-V}g`o-nzz-9un9aLZP4ps2nC*wX^0q{)Wvn z+{VMW#}L1cKBmpb16SS!o>fPG#^vPI$L`cwia8j&a}AF7?KerhcSLWmp*2 zp2N%GFz5Yw{hZ?QF-pJh8JELc2IY5@qf{i~SuWZ@=mqkx&z*JlHFW%0(=c&8p^Go7 z8$*)OW6g=;O2o1uPPbUC?Qw_6Mo#ZQN^8~r}t zJ6?~PPs!|Lt4hmZJUOnYd)-ApbtIgIL%{S(R!7_x5yX^ivB>FQ-E>`(CYGd^(_xkC zhkTvW@pqyl9Zl7{T7(Ed_IcMW;m*E}j{j2-OZ03q_1!e&c^m4&C)ic2Reod|vU{Gc z+xwfhbFu5_F)mi!F*h82*{+j!)9@R2yxMh3>+3xo%%^lXX?!vrWxH8dXX;*e(N7%` zy{RIz7o_EIkhFC8>Z?mC(&MPd)y}UuhFd(OeOEe08i|TNQNNQ}Ai_mMMR0rwJMI`d zOf=6i_M}PUxaVoPd$MvJ50Xk>`rm+~(JqbC0WbZb zm--z$+A+O`XkRZU(^0OQ9bKJinROSv)8Qft#Un8I=yCTvSvO4GAm*_34%Ho#4^yL^G}~FlkwoT#*_m1$|aL9P*wf5yY zyxu!_nb(rt>@#wv{@sz5w2`xIo7z?XE07c-8qgZFHixwn6;;Sy~7_)%_32~;?;kcRl>S9ILr+$I|2V{m&RX?YC>DFFtvm*Yj$0ujIo0^4Z;)dUr=UZ(BK6Ed@Q-%_7I9W7R`P zv04Z)HT=+_R<-4DO8hQ!Eanh&FN~KrI1^|pJrG1zS8^ReKoFrjUFo;blkNdG|D2Ai zKMI&AMb!JDG;E`KVQ!;w2|ZvUtH%StY0NjTcGuLUX1gEJg$wAhb*%-$XZXXR1=Qv4 z6iIrISBcaDhb>dnF}-&TvdvQrouqOK?Yr|n`?wC_qT|}S7fJ|)zeNdqpYBXcb)+$7 ztCHhzKNl^wV4+#G;XrBN0yV7-hwgA#E-dyt%F)SUNP;)!iC6!k~p7-LbrKPnaEHl2=uK)Zw4P7k+m+&Io9H;?f=IMpfyT ziV8TL!Da{!H)bTPHE}0e{JTfJI~@)xDj3UgX`(0HmrN%&N4M*}=gl|A&0SUhW>Qxr z2787|{gm>$bDZP-GOkRg!}MUZunICa=9_g_yMO-V@YW~-qRYiU5w}w3?MihZ5|Q|! z1&(~hLT^n+*iJ`vbDQ((W9W4}oMN%7oo{CMI%^i6?o2sQq9c_=u|kwk6xBKsIMF#M zP)PwqH`A^SNASDMafJ>V_t{=x&R|c==|O?Px#`bYS@fFuxff)a@e-O{PoH1&L{+#Gt(pbU1pdWK-ApSs7L>+GS@rDOa7Wsxp`v@=)AHD3%2 zzQiq{YPFsZDgFBIzh9;Q!B9Yuc3n=PRgOBh93)iGd-8H}0*3n_C9Qf}KiIq1 zj#!^u^ZsvKQZWBV8|1Ml00JbCX1ggDctCm4haeYy{So7Wlo)fqQk@U1<|_cYCV)sS zN0Eei%Mpz`k_kUwLwZT_cvQ(B($$|kQ+G#d=^S$o%~kK&FokedT4wu7*8;MmXxBJW z!{MedY6pSpMhA1#pZk$A;Wzf{w+}3ConQQ#?uwcgd-{_4w`))WS^{RLZVq+YX@jJ# zsy4(~{nlc!!ell4X!TX{=T*!QtUWdM>Ioc_Cbl~lmc#amsP+(1Z;7S(&9ckVRWEcA z==SjO=Dz9ms0$F3c*bH{p85qL0IDrVbdu0$9fc=u^{$ODFpo zvW*YqT!S?ZD=?Ni$JlAxnE0Wgj_HjAls@X^?WNOG+fk-39bf*v{xbV9rJT80F~&X zDf$2l^DhN*Tnn-{24?w^krczfv@j$hJy$Fco8M3E=^9r$u*nz41Hg4jCIJy65K>T_Zppt~Ka zw%f5}RONS=!< zA8Z$Dxq>WXaL>cS7p>6h{QIR<%$ z*ZSH9M_qpW@>?zq|(E1Nq0=WlBNZw6-NtivxD z6M0;{#o*_`6Q1x8_&TW73sBd(w?~t5GE_h8@`$^HE&hZF3o@7%#cuU9yQgoz;`|RR zUUAuusaqU`AyUfV+vxxNpZ&8BxuHW#+tN`b>hUX`4!`T@Tp4A8cDqM>G>PkrkNj|W ziPPGRm53M4-8-_3Z>KlL;~b1{Uw6m(=kr@LS8A8zT>1QSZs$lZ-(McEeKt2}F1`U$ z1P#W!CwHcfuGHvA&Caobdw#VB$wqvQIf-C!EjBbG_AZW1V#Q$0xJ7o!| zrVts7&EiwazUttqYqy)!Evhevw4XpuV^I!TUKFg0Wc2LJSpEA#u|OBjpifOK1HoHVjJ-Px8|T3MCib(-tVW4dS9tY?8401>`mKplpJ8B31h-VxXD7#?IPlsJX*%*9oP+!*`_4U+^t#+^I*q{3D#5rDcEN?J! z5{&cyWu6XeDyPmmtjo=I#CWiME2sJ{{QT0z%VFGa58?aQ`{fbM?b9zmJ|JH%zmGn> zUxM@L^7LM^4|k>!M^{?tNNt^?T3`eL5P&pF0?I~Dwlz-qpf0yV#I+yR4l8j2FKc%l zJIEpXc>;cIjfpRvp_L9%UPGsv#o*sy)$?F6mwjgd;UM{|8K}% z9i^RWL+eEiej8{JN&Tq1`nxSrx^o&kTAiEX(v>CkcdOGDVzHtbln46Vt47qktQV}h z=)+6SyK-~ye<_ZiJ!@2=8bz1PMH??`TRK46Lr1&&?a{SmHtC;jBjb=EtO_Ybv&Hj(s;SP{L;vk_EiP_ zbougd`y4R=1vjh*AMQ+DT`44Gv2~+{K6Ou&HXISNBepH#uatvKk#rl+K}BoPFh%p2 zX*!N7$IfuveW)eDyTrjKbH#{)ZT&ZVyP48;bB6N@U-g7Xzl>30Hg*2@X!y_=`V5$f z#zDhFHEsjm2DwC=w)U-AMBpnO@T@xD_Cr=~=!^C&?S#5~4+o}%$(sE%dt8z~+rsSA zCQ%&FZ?y*Yeo1<^;dNq=3od>`1DDe3-hJU~Kk2JLWrB(TWrmd0#u0C{+aW=s=g~wc z$DmyUR3fpXHri2oVC;4|e$+j!oaxI0&eTsAPrGuZ^;r*J^5o7`!4~d@r)y!0oXx0g z9v05Dvm|rOt#!A>m9cx3>d8h@os;j!E*P$2x7bCFnQ1miZVvwP2A-*$E#16o`-i8C zPv<)S#@^naUk{KBl7V~IcXP$x!M_#S;gC$N4hQ;aL>kkj)o!tDM}-gjKuaA50GRj8 zd*kE7fA}t+MkN|NE#A{#hj?z8)v>Hm1?0s5-&G!hM|Y-{lmyLO=^-dq6?XQ5)H>)! z3KwnW5W8(2_veGi>$Ga>p3W^A4kf_$IRd2CJC?~I@XL|Q+ocC|Ct-Tbxl2mkx*+{R z0N#lIzr3^iH}>}n4!C!{zjy82;nryP%&N&#Vcgk#Q(zCY%kdQ5ziZb3%tfK}1)k?k zUylr)duZD<%LOKVi0BYck&ie#Q{P=T^eSpqa+I12Y6?$usf73#v#pKXVVo8*PslR* zG<5wkK>`gaB58j}C*$7VISyF_NsYO-G2af-gBt1?oRjjbg;^KRlx}3X!o}i*FH1y=!3JTO5s{v7+>qR5U(Y|2ewpCygM5 z_{A=}(eBRFch?Oi$hBDAARwT^u9J7D-SWDOtKO_@^_2lT1Pb9sOK)wtF_ly{zWO4Es|8(bhRAzJ)T|1+e(J z8;N65>6X0JG1&{iXP2F9S7++I>xR0g44a~qhJ@WcruWxt=E^op=}V7w3Eg?Ep8}u* z9j1`vkN!~}(vR>*lb*MGK5p;?bp-mXb{|$ath_AfDP_q*I~|s9!X4Ww3V^goH0C3~ zra$_8cp%Yte&uM2H+`3QI%n#)>xK&580V@R?7TXNX7NO8T*}va+kHPqB%UC~qh^6D zK&Lt8e-w^x{G$wJa9o(fJzkg5&_KuQ7q;2%r>4u(tgNsQk15Y8S!kofv0ci2FpYwN zs^=cgIxl|O%)-!T+`+gqi_TcF zITAeKQc)U*5`qLx(Y+34w>adzqx9aqoK$=p=FS@^sWs0blKPDmmt~GgEf(g1bHO1= zHs9tx z2XI75@eV_&gFeKDW&stDkKUZAb=}Y;%T#@xfdHN`nqqS#nfHJEZ1@*1Zv08>$2aRz zoYy%LdCwqx^f>;yecQj&FSmBOIuzL5V%lz_8%eK{?H&2l|~`huR8^bA+~9o6UQ!8gwCaa5nTzhr3>7)f+RbyT}?!{ox7G>Us} zXKG(J^e_p+GjgTZZCIDMsR{5WZ;RL+Rkw;6S=p(n-+7qy1-F&o<6*G{_b(q zF>inI;yPG>j&kBS>KQKjCO<0hkN{rWnU-5Obb2CJO26esE;9N91n(%3W$KcZYbuf& zIYsT)dD7^+@JNUL8Lsv_s?XDdZ=BuZs6KCh{+?BdfQVy6@PMoN_g8xeU*=Ac2L!}# zJJS<#5z$@ahMs$D=i>;altmb7IMzdA#H$Z?Uf1ThlX~U;-Z5h&&Hb}n?RQk4rw89S zyT?&|-v0bO%e%T^BS#GL5gq}Z0o>i0I_3s|C^E!Hp5w(>C$cqefjUD1t*Vop~HXU05^80SNb5#8Y&hr@AkI&vKj z_qddE(OjsS&8hEt6<=3;&%7w#bZg6z71Ih-&_X*aCgV|E?s50AV|Kh%=I-Y)G;Q%2 zGwL1WA^7fjgYKp65FJ;&MFN#(X}WBiM<-Itk=WGgQi5W2BYOc`w}rGP}& zrabd#8!}H4ULYh($PBpL+4DT1 z<_DVd$Y>dYF^H!f8OX7FNAz3~<{}9k?g^EDUOb1=gldu{Ai3sosX1NldFWw>r0Pj< zprbKe>jMtxp?B!w$(*VCJT5=t9F}z)|^zLgO)f>*wdje6RAtPPS$WG$-TD} ztM==wWOn3G%|gjB3e6^rNL5&V9*I{CQxzpDE$VXMb>^mX978!rceux~b4Q7B#wfCE z!bpKCEH975eZy4fhbmRNTpJ;d0ezG%4W3D|TrT%K^sys(ztQS^F{2P$dC?p2iD%_Z zhww3Q1K=ahVSXn&tV6WcBlenNbn5$qG@aM+FxJ@|@KLXxriH2-V8t{nOtX<^Bmm3H z)qNM1j)td0EtmR|AflXLnt`#58e@!lj_z=eWA}~>_rYfe{*dG2FJzMT%%*d>tnu6GGrIYTK>qB9KuL>uK}&S5<+*c8n7 zLEIK05|MVedLrQf6Dir^vL}b;inYfXNGY^jmMrhsOvakc@>(+w7(&P$LSy~1TMJPVImAALJh)7MZQa@?f51@b6n4& zJ5r`%gAhzD>U54x=Lj(ti|%lbWA}~>~^O6uusi0 zboB~PJJ_d6WZ4Co#O3nEtna)c7NK5>|3I|PAu`Cy7DP*%zn zs~GS&#~{u|z&YY+N37LCSav!3 z&YGAottA(Wuu24xS*Gs#fhQkAz{%&1D4=V!axP=UXbtB%y2Cw=-8(Xn%g|jf*M|_0 zK9@oorGYf+Hr{zb(_VVq6CUEymf$k5bGk~*a}PU!lJ=Z1XcX1PbgeI@x;Q%1{3Ya{ z3jN28?F$B#=7ZsA*N-xX@A{|(xxuU5 zv=)z*JLhFOcXD3r+8ps&lOU1!wv4Fc8j7G6h$7J<9dWYF+fU2t_#KNoziwiw%slt6 zW4ghKv^c5VE9FE{KMV)OY@MlqtcM%7NDzYn=yX*Yj?^SuBOH!#W%|ky0Dx!xT`Ved zNz!z+98LGM7@JnDl#CIkIi}f}DJ77axGGX68Nxt2nQf6dF*5OG1)x

U3r`H6HAl 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%0DgYc?ilGO77FeJr$Ys9E4)96LZ+=LG6WS3 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


xU# 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 literal 0 HcmV?d00001 diff --git a/docs/docs/changelog/template.md b/docs/docs/changelog/.template.md similarity index 100% rename from docs/docs/changelog/template.md rename to docs/docs/changelog/.template.md diff --git a/docs/docs/changelog/v0.5.0.md b/docs/docs/changelog/v0.5.0.md index fc5256c0..37b2e2f9 100644 --- a/docs/docs/changelog/v0.5.0.md +++ b/docs/docs/changelog/v0.5.0.md @@ -17,11 +17,24 @@ #### API Usage If you have been using the API directly, many of the routes and status codes have changed. You may experience issues with directly consuming the API. + #### Arm/v7 Support + Mealie will no longer build in CI/CD due to a issue with the rust compiler on 32 bit devices. You can reference [this issue on the matrix-org/synapse](https://github.com/matrix-org/synapse/issues/9403) Github page that are facing a similar issue. You may still be able to build the docker image you-self. + ## Bug Fixes +- Fixed #25 - Allow changing rating without going into edit +- Fixed #475 - trim whitespace on login +- Fixes #435 - Better Email Regex +- Fixed #428 - Meal Planner now works on iOS devices +- Fixed #419 - Typos +- Fixed #418 - You can now "export" shopping lists +- Fixed #356 - Shopping List items are now grouped +- Fixed #329 - Fixed profile image not loading +- Fixed #461 - Proper JSON serialization on webhooks - Fixed #332 - Language settings are saved for one browser - Fixes #281 - Slow Handling of Large Sets of Recipes - Fixed #356 - Shopping lists generate duplicate items - Fixed #271 - Slow handling of larger data sets +- Fixed #472, #469, #458, #456 - Improve Recipe Parser ## Features and Improvements @@ -34,9 +47,12 @@ - ⚠️ last_recipe.json is now depreciated - Beta Support for Postgres! 🎉 See the getting started page for details - Recipe Features - - Step Sections - - Recipe Assets - - Additional View Settings. + - New button bar for editors with improved accessibility and performance + - Step Sections now supported + - Recipe Assets + - Add Asset image to recipe step + - Additional View Settings. + - Better print support - New Toolbox Page! - Bulk assign categories and tags by keyword search - Title case all Categories or Tags with 1 click @@ -45,8 +61,8 @@ - Recipe Cards now have a menu button for quick actions! - Edit - Delete - - Download (As Json) - - Copy Link + - Integrated Share with supported OS/Browsers + - Print - New Profile Dashboard! - Edit Your Profile - Create/Edit Themes @@ -58,13 +74,17 @@ - See uncategorized/untagged recipes and organize them! - Backup/Restore right from your dashboard - See server side events. Now you can know who deleted your favorite recipe! +- New Event Notifications through the Apprise Library + - Get notified when specific server side events occur -### Performance -- Images are now served up by the Caddy increase performance and offloading some loads from the API server -- Requesting all recipes from the server has been rewritten to refresh less often and manage client side data better. -- All images are now converted to .webp for better compression +### Meal Planner +- Multiple Recipes per day +- Supports meals without recipes (Enter title and description) +- Generate share-link from created meal-planners +- Shopping lists can be directly generated from the meal plan ### General +- User can now favorite recipes - New 'Dark' Color Theme Packaged with Mealie - Updated Recipe Card Sections Toolbar - New Sort Options (They work this time!) @@ -88,7 +108,12 @@ - Improved styling for search bar in desktop - Improved search layout on mobile - Profile image now shown on all sidebars -- Switched from Flash Messages to Snackbar (Removed dependency +- Switched from Flash Messages to Snackbar (Removed dependency) +- +### Performance +- Images are now served up by the Caddy increase performance and offloading some loads from the API server +- Requesting all recipes from the server has been rewritten to refresh less often and manage client side data better. +- All images are now converted to .webp for better compression ### Behind the Scenes - Black and Flake8 now run as CI/CD checks diff --git a/docs/docs/documentation/admin/site-settings.md b/docs/docs/documentation/admin/site-settings.md index 245ea9a5..7f23bc96 100644 --- a/docs/docs/documentation/admin/site-settings.md +++ b/docs/docs/documentation/admin/site-settings.md @@ -2,14 +2,14 @@ Your sites settings panel can only be accessed by administrators. This where you can customize your site for all users. ## Home Page Settings -| Option | Description | -| ------------------ | -------------------------------------------------------------- | -| Show Recent | To display the recent recipes section on the home page | -| Card Per Section | The amount of cards displayed in each section on the home page | -| Home Page Sections | Category sections to include on the home page | -| Language | The default site language | -| First day of the week | The default start day of the week used in Meal Plans | -| Custom Pages | Create a [custom page](../admin/building-pages.md) which appears in the sidebar with the categories you chose | +| Option | Description | +| --------------------- | ------------------------------------------------------------------------------------------------------------- | +| Show Recent | To display the recent recipes section on the home page | +| Card Per Section | The amount of cards displayed in each section on the home page | +| Home Page Sections | Category sections to include on the home page | +| Language | The default site language | +| First day of the week | The default start day of the week used in Meal Plans | +| Custom Pages | Create a [custom page](../admin/building-pages.md) which appears in the sidebar with the categories you chose | ![Site Settings Image](../../assets/img/site-settings.webp) diff --git a/docs/docs/documentation/getting-started/api-usage.md b/docs/docs/documentation/getting-started/api-usage.md index e4ffb971..045fefbb 100644 --- a/docs/docs/documentation/getting-started/api-usage.md +++ b/docs/docs/documentation/getting-started/api-usage.md @@ -2,29 +2,8 @@ ## Getting a Token -Mealie supports long-live api tokens in the user frontend. In you profile section you can use the +Mealie supports long-live api tokens in the user frontend. See [user settings page](../../users-groups/user-settings/) -### Curl -```bash -curl -X 'POST' \ - 'https://mealie-demo.hay-kot.dev/api/auth/token' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/x-www-form-urlencoded' \ - -d 'grant_type=&username=changeme%40email.com&password=demo&scope=&client_id=&client_secret=' - -``` - -#### Response -```json -{ - "snackbar": { - "text": "User Successfully Logged In", - "type": "success" - }, - "access_token": "your-long-token-string", - "token_type": "bearer" -} -``` ## Key Components diff --git a/docs/docs/documentation/recipes/recipes.md b/docs/docs/documentation/recipes/recipes.md index 164351fa..da455dbc 100644 --- a/docs/docs/documentation/recipes/recipes.md +++ b/docs/docs/documentation/recipes/recipes.md @@ -1,8 +1,11 @@ # Recipes ## URL Import -Adding a recipe can be as easy as clicking in the bottom-right corner, copying the recipe URL into Mealie and letting the web scrapper organize information. Currently this scraper is implemented with [scrape-schema-recipe package](https://pypi.org/project/scrape-schema-recipe/). You may have mixed results on some websites, especially with blogs or non-specific recipe websites. See the bulk import Option below for another a convenient way to add blog style recipes into Mealie. -You can add different sections to make sure your recipe is divided correctly into separate 'chapters' +Adding a recipe can be as easy as clicking in the bottom-right corner, copying the recipe URL into Mealie and letting the web scrapper organize information. Currently this scraper is implemented with [recipe-scrapers](https://github.com/hhursev/recipe-scrapers). You may have mixed results on some websites, especially with blogs or non-specific recipe websites. See the bulk import Option below for another a convenient way to add blog style recipes into Mealie. + +!!! tip + You can find a list of some of the supported sites in the recipe-scrapers repo. If you're site isn't supported, you can work with the recipe-scrapers team to implement it and we can down-stream those changes into Mealie. + ![](../../assets/gifs/URL-import.gif) ## Using Bookmarklets @@ -19,12 +22,33 @@ window.open(dest, '_blank') ``` ## Recipe Editor + +![edit-recipe](../../assets/img/edit-recipe.webp){: align=right style="height:225px;width:275px"} Recipes can be edited and created via the UI. This is done with both a form based approach where you have a UI to work with as well as with a in browser JSON Editor. The JSON editor allows you to easily copy and paste data from other sources. -![edit-recipe](../../assets/img/edit-recipe.webp) -You can also add a custom recipe with the UI editor built into the web view. +You can also add a custom recipe with the UI editor built into the web view. Using the `+` button on the site. + +### Recipe Settings + +Settings for a specific recipe can be adjusted in the settings menu inside the editor. Currently the settings supports + +- Settings a Recipe to Public/Private +- Show Nutrition Values +- Show Assets +- Landscape Mode (Coming Soon) + +!!! note + Recipes set to private will only be displayed when a user is logged in. Currently there is no way to generate a share-link for a private recipe, but it is on the roadmap. + +### Recipe Assets + +While in the editor you also have an opportunity to upload any asset to your recipe. There are several icons that you can choose from or you can choose an arbitrary file icon. Once uploaded you can view or download the asset when viewing the page. + +!!! tip + You can get a link to an asset to embed in a recipe step by select the copy icon in editor mode. + +### Bulk Import -## Bulk Import Mealie also supports bulk import of recipe instructions and ingredients. Select "Bulk Add" in the editor and paste in your plain text data to be parsed. Each line is treated as one entry and will be appended to the existing ingredients or instructions if they exist. Empty lines will be stripped from the text. ![](../../assets/gifs/bulk-add-demo.gif) diff --git a/docs/docs/documentation/toolbox/toolbox-intro.md b/docs/docs/documentation/toolbox/notifications.md similarity index 63% rename from docs/docs/documentation/toolbox/toolbox-intro.md rename to docs/docs/documentation/toolbox/notifications.md index b2c810c4..d543d188 100644 --- a/docs/docs/documentation/toolbox/toolbox-intro.md +++ b/docs/docs/documentation/toolbox/notifications.md @@ -1,21 +1,6 @@ -#Toolbox -The toolbox gives you multiple options to clean-up and organize your recipes. You can get notified through different channels. -You can access it through the 'Settings' menu or through the [dashboard](../admin/dashboard.md). +# External Notifications - -## Category and Tag Editor -The 'Categories' and 'Tags' tab give you the option to bulk assign categories and tags to multiple recipes. You could also remove the unused ones or title case them all. - -![Toolbox-Categories](../../assets/img/Toolbox-Categories.webp) - -## Bulk Organize -The 'Organize' tab can be used to show all of the items that do not have any category or tag assigned. - -![Toolbox-Organize](../../assets/img/Toolbox-Organize.webp) - -## External Notifications - -### Apprise +## Apprise Using the [Apprise](https://github.com/caronc/apprise/) library Mealie is able to provided notification services for nearly every popular service. Some of our favorites are... @@ -28,7 +13,7 @@ Using the [Apprise](https://github.com/caronc/apprise/) library Mealie is able t But there are some many to choose from! Take a look at their wiki for information on how to create their URL formats and that you can use to create a notification integration in Mealie. -### Subscribe Events +## Subscribe Events There are several categories of events that mealie logs that can be broadcast with the notifications feature. You can also see a feed of your events in the Admin Dashboard - General Events @@ -52,15 +37,15 @@ There are several categories of events that mealie logs that can be broadcast wi In most cases the events will also provide details on which user performed the action. Now you'll know when your grandma deletes your favorite recipe! !!! info -This is a new feature and we are still working through all the possibilities of events. if you have an idea for an event let us know! + This is a new feature and we are still working through all the possibilities of events. if you have an idea for an event let us know! -### Creating a New Notification +## Creating a New Notification New events can be created and viewed in admin Toolbox `/admin/toolbox?tab=event-notifications`. Select the "+ Notification" button and you'll be provided with a dialog. Complete the form using the URL for the service you'd like to connect to. Before saving be sure to use the test feature. !!! tip -The feedback provided from the test feature is only an indicated of if the URL you provided is valid, not if the message was successfully sent. Be sure to check the notification feed for the test message. + The feedback provided from the test feature is only an indicated of if the URL you provided is valid, not if the message was successfully sent. Be sure to check the notification feed for the test message. ![Add Notification Image](../../assets/img/add-notification.webp) diff --git a/docs/docs/documentation/toolbox/organize-tools.md b/docs/docs/documentation/toolbox/organize-tools.md new file mode 100644 index 00000000..0036596c --- /dev/null +++ b/docs/docs/documentation/toolbox/organize-tools.md @@ -0,0 +1,17 @@ +#Toolbox +The toolbox gives you multiple options to clean-up and organize your recipes. You can get notified through different channels. +You can access it through the 'Settings' menu or through the [dashboard](../admin/dashboard.md). + + +## Category and Tag Editor + +The 'Categories' and 'Tags' tab give you the option to bulk assign categories and tags to multiple recipes. You could also remove the unused ones or title case them all. + +![Toolbox-Categories](../../assets/img/Toolbox-Categories.webp) + +## Bulk Organize + +The 'Organize' tab can be used to show all of the items that do not have any category or tag assigned. + +![Toolbox-Organize](../../assets/img/Toolbox-Organize.webp) + diff --git a/docs/docs/documentation/users-groups/meal-planner.md b/docs/docs/documentation/users-groups/meal-planner.md index 43938360..53fd26b5 100644 --- a/docs/docs/documentation/users-groups/meal-planner.md +++ b/docs/docs/documentation/users-groups/meal-planner.md @@ -1,8 +1,9 @@ # Meal Planner ## Working with Planner -In Mealie you can create a meal plan based off the calendar inputs on the meal planner page. There is no limit to how long or how short a meal plan is. -You may also create duplicate meal plans for the same date range. After selecting your date range, click on the card for each day and search through recipes to find your choice. Add a side-dish if you prefer to. After selecting a recipe for all meals, save the plan. + +In Mealie you can create a meal plan based off the calendar inputs on the meal planner page. There is no limit to how long or how short a meal plan is. You may also create duplicate meal plans for the same date range. After selecting your date range, click on the card for each day and search through recipes to find your choice. Add a side-dish if you prefer to. After selecting a recipe for all meals, save the plan. Selecting the 'No Recipe' button will allow you to add an entry without a recipe by providing a title and description + You can also randomly generate meal plans with the dice-button at the bottom. To edit the meal in a meal plan simply select the edit button on the card in the timeline. Similarly, to delete a meal plan click the delete button on the card in the timeline. Currently, there is no support to change the date range in a meal plan. diff --git a/docs/docs/documentation/users-groups/user-settings.md b/docs/docs/documentation/users-groups/user-settings.md index 060ca79f..41bd1afd 100644 --- a/docs/docs/documentation/users-groups/user-settings.md +++ b/docs/docs/documentation/users-groups/user-settings.md @@ -1,15 +1,14 @@ # User Settings -A user will be able to access 3 sections in their admin panel. The user profile, themes, group/meal-plan settings section. - ## Profile Settings -In as users profile they are able to +In the users profile they are able to: - Change Display Name - Change Email - Update Password - View Their Group -- Upgrade Profile Picture (Experimental) +- Update Profile Picture (Experimental) +- Create API Keys ## Themes Color themes can be created and set from the UI in the Settings-Profile page. You can select an existing color theme or create a new one. On creation of a new color theme, the default colors will be used, then you can select and save as you'd like. By default, the "default" theme will be loaded for all new users visiting the site. All created color themes are available to all users of the site. Theme Colors will be set for both light and dark modes. @@ -23,3 +22,12 @@ Color themes can be created and set from the UI in the Settings-Profile page. Yo In the meal planner section a user can select categories to be used as a part of the random recipe selector in the meal plan creator. If no categories are selected, all recipes will be used Meal planner webhooks are post requests sent from Mealie to an external endpoint. The body of the message is the Recipe JSON of the scheduled meal. If no meal is schedule, no request is sent. The webhook functionality can be enabled or disabled as well as scheduled. Note that you must "Save" prior to any changes taking affect server side. + +## API Key Generation +Users can quickly and easily generate API keys with the user interface. Provide a name for your token and then you are shown 1 time the generated API key. If you ever loose the API key you are not able to identify or retrieve it from the UI. + +![API Key Image](../../assets/img/api-key-image-v1.webp) + + +!!! warning + API keys are stored in plain text in the database. \ No newline at end of file diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html index f37cd25f..4e9440ab 100644 --- a/docs/docs/overrides/api.html +++ b/docs/docs/overrides/api.html @@ -14,7 +14,7 @@
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index fb7144a8..1149614f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,5 +1,7 @@ site_name: Mealie demo_url: https://mealie-demo.hay-kot.dev/ +site_url: https://hay-kot.github.io/mealie/ +use_directory_urls: true theme: palette: # Light mode @@ -61,6 +63,7 @@ nav: - Users & Groups: - User Settings: "documentation/users-groups/user-settings.md" - Planning Meals: "documentation/users-groups/meal-planner.md" + - Admin: - Dashboard: "documentation/admin/dashboard.md" - Site Settings: "documentation/admin/site-settings.md" @@ -68,15 +71,16 @@ nav: - User Management: "documentation/admin/user-management.md" - Backups and Restore: "documentation/admin/backups-and-exports.md" - Recipe Migration: "documentation/admin/migration-imports.md" + - Toolbox: - - Toolbox: "documentation/toolbox/toolbox-intro.md" + - External Notifications: "documentation/toolbox/notifications.md" + - Organization Tools: "documentation/toolbox/organize-tools.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" - - API Reference: "api/redoc.md" - Contributors Guide: - Non-Code: "contributors/non-coders.md" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e7b19c5e..db8a0bde 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1947,16 +1947,15 @@ } }, "@vue/component-compiler-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz", - "integrity": "sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.1.tgz", + "integrity": "sha512-Mci9WJYLRjyJEBkGHMPxZ1ihJ9l6gOy2Gr6hpYZUNpQoe5+nbpeb3w00aP+PSHJygCF+fxJsqp7Af1zGDITzuw==", "dev": true, "requires": { "consolidate": "^0.15.1", "hash-sum": "^1.0.2", "lru-cache": "^4.1.2", "merge-source-map": "^1.1.0", - "postcss": "^7.0.14", "postcss-selector-parser": "^6.0.2", "prettier": "^1.18.2", "source-map": "~0.6.1", @@ -4641,12 +4640,20 @@ "dev": true }, "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "dev": true, "requires": { - "domelementtype": "1" + "domelementtype": "^2.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + } } }, "domutils": { @@ -6163,34 +6170,43 @@ } }, "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" }, "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", "dev": true, "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" } } } @@ -9680,16 +9696,16 @@ "dev": true }, "renderkid": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", - "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", "dev": true, "requires": { - "css-select": "^2.0.2", - "dom-converter": "^0.2", - "htmlparser2": "^3.10.1", - "lodash": "^4.17.20", - "strip-ansi": "^3.0.0" + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" }, "dependencies": { "ansi-regex": { @@ -9698,6 +9714,62 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", + "dev": true + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -11907,9 +11979,9 @@ } }, "vue-loader": { - "version": "15.9.6", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.6.tgz", - "integrity": "sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg==", + "version": "15.9.7", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.7.tgz", + "integrity": "sha512-qzlsbLV1HKEMf19IqCJqdNvFJRCI58WNbS6XbPqK13MrLz65es75w392MSQ5TsARAfIjUw+ATm3vlCXUJSOH9Q==", "dev": true, "requires": { "@vue/component-compiler-utils": "^3.1.0", diff --git a/frontend/src/api/about.js b/frontend/src/api/about.js index 1cfa13f2..92434fad 100644 --- a/frontend/src/api/about.js +++ b/frontend/src/api/about.js @@ -1,55 +1,38 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import i18n from "@/i18n.js"; - -const prefix = baseURL + "about"; - -const aboutURLs = { - version: `${prefix}/version`, - debug: `${prefix}`, - lastRecipe: `${prefix}/last-recipe-json`, - demo: `${prefix}/is-demo`, - log: num => `${prefix}/log/${num}`, - statistics: `${prefix}/statistics`, - events: `${prefix}/events`, - event: id => `${prefix}/events/${id}`, - - allNotifications: `${prefix}/events/notifications`, - testNotifications: `${prefix}/events/notifications/test`, - notification: id => `${prefix}/events/notifications/${id}`, -}; +import { API_ROUTES } from "./apiRoutes"; export const aboutAPI = { async getEvents() { - const resposne = await apiReq.get(aboutURLs.events); + const resposne = await apiReq.get(API_ROUTES.aboutEvents); return resposne.data; }, async deleteEvent(id) { - const resposne = await apiReq.delete(aboutURLs.event(id)); + const resposne = await apiReq.delete(API_ROUTES.aboutEventsId(id)); return resposne.data; }, async deleteAllEvents() { - const resposne = await apiReq.delete(aboutURLs.events); + const resposne = await apiReq.delete(API_ROUTES.aboutEvents); return resposne.data; }, async allEventNotifications() { - const response = await apiReq.get(aboutURLs.allNotifications); + const response = await apiReq.get(API_ROUTES.aboutEventsNotifications); return response.data; }, async createNotification(data) { - const response = await apiReq.post(aboutURLs.allNotifications, data); + const response = await apiReq.post(API_ROUTES.aboutEventsNotifications, data); return response.data; }, async deleteNotification(id) { - const response = await apiReq.delete(aboutURLs.notification(id)); + const response = await apiReq.delete(API_ROUTES.aboutEventsNotificationsId(id)); return response.data; }, async testNotificationByID(id) { const response = await apiReq.post( - aboutURLs.testNotifications, + API_ROUTES.aboutEventsNotificationsTest, { id: id }, () => i18n.t("events.something-went-wrong"), () => i18n.t("events.test-message-sent") @@ -58,7 +41,7 @@ export const aboutAPI = { }, async testNotificationByURL(url) { const response = await apiReq.post( - aboutURLs.testNotifications, + API_ROUTES.aboutEventsNotificationsTest, { test_url: url }, () => i18n.t("events.something-went-wrong"), () => i18n.t("events.test-message-sent") diff --git a/frontend/src/api/api-utils.js b/frontend/src/api/api-utils.js index c492052c..8f83a409 100644 --- a/frontend/src/api/api-utils.js +++ b/frontend/src/api/api-utils.js @@ -1,4 +1,4 @@ -const baseURL = "/api/"; +import { prefix } from "./apiRoutes"; import axios from "axios"; import { store } from "../store"; import { utils } from "@/utils"; @@ -66,11 +66,10 @@ const apiReq = { const response = await this.get(url); const token = response.data.fileToken; - const tokenURL = baseURL + "utils/download?token=" + token; + const tokenURL = prefix + "utils/download?token=" + token; window.open(tokenURL, "_blank"); return response.data; }, }; export { apiReq }; -export { baseURL }; diff --git a/frontend/src/api/apiRoutes.js b/frontend/src/api/apiRoutes.js index 0102f4f0..9b0c7f85 100644 --- a/frontend/src/api/apiRoutes.js +++ b/frontend/src/api/apiRoutes.js @@ -1,82 +1,87 @@ // This Content is Auto Generated -const prefix = '/api' -export const API_ROUTES = { - aboutEvents: "/api/about/events", - aboutEventsNotifications: "/api/about/events/notifications", - aboutEventsNotificationsTest: "/api/about/events/notifications/test", - authRefresh: "/api/auth/refresh", - authToken: "/api/auth/token", - authTokenLong: "/api/auth/token/long", - backupsAvailable: "/api/backups/available", - backupsExportDatabase: "/api/backups/export/database", - backupsUpload: "/api/backups/upload", - categories: "/api/categories", - categoriesEmpty: "/api/categories/empty", - debug: "/api/debug", - debugLastRecipeJson: "/api/debug/last-recipe-json", - debugLog: "/api/debug/log", - debugStatistics: "/api/debug/statistics", - debugVersion: "/api/debug/version", - groups: "/api/groups", - groupsSelf: "/api/groups/self", - mealPlansAll: "/api/meal-plans/all", - mealPlansCreate: "/api/meal-plans/create", - mealPlansThisWeek: "/api/meal-plans/this-week", - mealPlansToday: "/api/meal-plans/today", - mealPlansTodayImage: "/api/meal-plans/today/image", - migrations: "/api/migrations", - recipesCategory: "/api/recipes/category", - recipesCreate: "/api/recipes/create", - recipesCreateUrl: "/api/recipes/create-url", - recipesSummary: "/api/recipes/summary", - recipesSummaryUncategorized: "/api/recipes/summary/uncategorized", - recipesSummaryUntagged: "/api/recipes/summary/untagged", - recipesTag: "/api/recipes/tag", - shoppingLists: "/api/shopping-lists", - siteSettings: "/api/site-settings", - siteSettingsCustomPages: "/api/site-settings/custom-pages", - siteSettingsWebhooksTest: "/api/site-settings/webhooks/test", - tags: "/api/tags", - tagsEmpty: "/api/tags/empty", - themes: "/api/themes", - themesCreate: "/api/themes/create", - users: "/api/users", - usersApiTokens: "/api/users-tokens", - usersSelf: "/api/users/self", - usersSignUps: "/api/users/sign-ups", - utilsDownload: "/api/utils/download", +export const prefix = "/api"; +export const API_ROUTES = { + aboutEvents: `${prefix}/about/events`, + aboutEventsNotifications: `${prefix}/about/events/notifications`, + aboutEventsNotificationsTest: `${prefix}/about/events/notifications/test`, + authRefresh: `${prefix}/auth/refresh`, + authToken: `${prefix}/auth/token`, + authTokenLong: `${prefix}/auth/token/long`, + backupsAvailable: `${prefix}/backups/available`, + backupsExportDatabase: `${prefix}/backups/export/database`, + backupsUpload: `${prefix}/backups/upload`, + categories: `${prefix}/categories`, + categoriesEmpty: `${prefix}/categories/empty`, + debug: `${prefix}/debug`, + debugLastRecipeJson: `${prefix}/debug/last-recipe-json`, + debugLog: `${prefix}/debug/log`, + debugStatistics: `${prefix}/debug/statistics`, + debugVersion: `${prefix}/debug/version`, + groups: `${prefix}/groups`, + groupsSelf: `${prefix}/groups/self`, + mealPlansAll: `${prefix}/meal-plans/all`, + mealPlansCreate: `${prefix}/meal-plans/create`, + mealPlansThisWeek: `${prefix}/meal-plans/this-week`, + mealPlansToday: `${prefix}/meal-plans/today`, + mealPlansTodayImage: `${prefix}/meal-plans/today/image`, + migrations: `${prefix}/migrations`, + recipesCategory: `${prefix}/recipes/category`, + recipesCreate: `${prefix}/recipes/create`, + recipesCreateUrl: `${prefix}/recipes/create-url`, + recipesSummary: `${prefix}/recipes/summary`, + recipesSummaryUncategorized: `${prefix}/recipes/summary/uncategorized`, + recipesSummaryUntagged: `${prefix}/recipes/summary/untagged`, + recipesTag: `${prefix}/recipes/tag`, + recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`, + shoppingLists: `${prefix}/shopping-lists`, + siteSettings: `${prefix}/site-settings`, + siteSettingsCustomPages: `${prefix}/site-settings/custom-pages`, + siteSettingsWebhooksTest: `${prefix}/site-settings/webhooks/test`, + tags: `${prefix}/tags`, + tagsEmpty: `${prefix}/tags/empty`, + themes: `${prefix}/themes`, + themesCreate: `${prefix}/themes/create`, + users: `${prefix}/users`, + usersApiTokens: `${prefix}/users-tokens`, + usersSelf: `${prefix}/users/self`, + usersSignUps: `${prefix}/users/sign-ups`, + utilsDownload: `${prefix}/utils/download`, - aboutEventsId: (id) => `${prefix}/about/events/${id}`, - aboutEventsNotificationsId: (id) => `${prefix}/about/events/notifications/${id}`, - backupsFileNameDelete: (file_name) => `${prefix}/backups/${file_name}/delete`, - backupsFileNameDownload: (file_name) => `${prefix}/backups/${file_name}/download`, - backupsFileNameImport: (file_name) => `${prefix}/backups/${file_name}/import`, - categoriesCategory: (category) => `${prefix}/categories/${category}`, - debugLogNum: (num) => `${prefix}/debug/log/${num}`, - groupsId: (id) => `${prefix}/groups/${id}`, - mealPlansId: (id) => `${prefix}/meal-plans/${id}`, - mealPlansIdShoppingList: (id) => `${prefix}/meal-plans/${id}/shopping-list`, - mealPlansPlanId: (plan_id) => `${prefix}/meal-plans/${plan_id}`, - mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) => `${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`, - mediaRecipesRecipeSlugImagesFileName: (recipe_slug, file_name) => `${prefix}/media/recipes/${recipe_slug}/images/${file_name}`, - migrationsImportTypeFileNameDelete: (import_type, file_name) => `${prefix}/migrations/${import_type}/${file_name}/delete`, - migrationsImportTypeFileNameImport: (import_type, file_name) => `${prefix}/migrations/${import_type}/${file_name}/import`, - migrationsImportTypeUpload: (import_type) => `${prefix}/migrations/${import_type}/upload`, - recipesRecipeSlug: (recipe_slug) => `${prefix}/recipes/${recipe_slug}`, - recipesRecipeSlugAssets: (recipe_slug) => `${prefix}/recipes/${recipe_slug}/assets`, - recipesRecipeSlugImage: (recipe_slug) => `${prefix}/recipes/${recipe_slug}/image`, - recipesSlugComments: (slug) => `${prefix}/recipes/${slug}/comments`, + aboutEventsId: id => `${prefix}/about/events/${id}`, + aboutEventsNotificationsId: id => `${prefix}/about/events/notifications/${id}`, + backupsFileNameDelete: file_name => `${prefix}/backups/${file_name}/delete`, + backupsFileNameDownload: file_name => `${prefix}/backups/${file_name}/download`, + backupsFileNameImport: file_name => `${prefix}/backups/${file_name}/import`, + categoriesCategory: category => `${prefix}/categories/${category}`, + debugLogNum: num => `${prefix}/debug/log/${num}`, + groupsId: id => `${prefix}/groups/${id}`, + mealPlansId: id => `${prefix}/meal-plans/${id}`, + mealPlansIdShoppingList: id => `${prefix}/meal-plans/${id}/shopping-list`, + mealPlansPlanId: plan_id => `${prefix}/meal-plans/${plan_id}`, + mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) => + `${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`, + mediaRecipesRecipeSlugImagesFileName: (recipe_slug, file_name) => + `${prefix}/media/recipes/${recipe_slug}/images/${file_name}`, + migrationsImportTypeFileNameDelete: (import_type, file_name) => + `${prefix}/migrations/${import_type}/${file_name}/delete`, + migrationsImportTypeFileNameImport: (import_type, file_name) => + `${prefix}/migrations/${import_type}/${file_name}/import`, + migrationsImportTypeUpload: import_type => `${prefix}/migrations/${import_type}/upload`, + recipesRecipeSlug: recipe_slug => `${prefix}/recipes/${recipe_slug}`, + recipesRecipeSlugAssets: recipe_slug => `${prefix}/recipes/${recipe_slug}/assets`, + recipesRecipeSlugImage: recipe_slug => `${prefix}/recipes/${recipe_slug}/image`, + recipesSlugComments: slug => `${prefix}/recipes/${slug}/comments`, recipesSlugCommentsId: (slug, id) => `${prefix}/recipes/${slug}/comments/${id}`, - shoppingListsId: (id) => `${prefix}/shopping-lists/${id}`, - siteSettingsCustomPagesId: (id) => `${prefix}/site-settings/custom-pages/${id}`, - tagsTag: (tag) => `${prefix}/tags/${tag}`, - themesId: (id) => `${prefix}/themes/${id}`, - usersApiTokensTokenId: (token_id) => `${prefix}/users-tokens/${token_id}`, - usersId: (id) => `${prefix}/users/${id}`, - usersIdFavorites: (id) => `${prefix}/users/${id}/favorites`, + shoppingListsId: id => `${prefix}/shopping-lists/${id}`, + siteSettingsCustomPagesId: id => `${prefix}/site-settings/custom-pages/${id}`, + tagsTag: tag => `${prefix}/tags/${tag}`, + themesId: id => `${prefix}/themes/${id}`, + usersApiTokensTokenId: token_id => `${prefix}/users-tokens/${token_id}`, + usersId: id => `${prefix}/users/${id}`, + usersIdFavorites: id => `${prefix}/users/${id}/favorites`, usersIdFavoritesSlug: (id, slug) => `${prefix}/users/${id}/favorites/${slug}`, - usersIdImage: (id) => `${prefix}/users/${id}/image`, - usersIdPassword: (id) => `${prefix}/users/${id}/password`, - usersIdResetPassword: (id) => `${prefix}/users/${id}/reset-password`, - usersSignUpsToken: (token) => `${prefix}/users/sign-ups/${token}`, -} \ No newline at end of file + usersIdImage: id => `${prefix}/users/${id}/image`, + usersIdPassword: id => `${prefix}/users/${id}/password`, + usersIdResetPassword: id => `${prefix}/users/${id}/reset-password`, + usersSignUpsToken: token => `${prefix}/users/sign-ups/${token}`, +}; diff --git a/frontend/src/api/backup.js b/frontend/src/api/backup.js index f729eefc..31c354c0 100644 --- a/frontend/src/api/backup.js +++ b/frontend/src/api/backup.js @@ -1,18 +1,7 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import { store } from "@/store"; import i18n from "@/i18n.js"; - -const backupBase = baseURL + "backups/"; - -export const backupURLs = { - // Backup - available: `${backupBase}available`, - createBackup: `${backupBase}export/database`, - importBackup: fileName => `${backupBase}${fileName}/import`, - deleteBackup: fileName => `${backupBase}${fileName}/delete`, - downloadBackup: fileName => `${backupBase}${fileName}/download`, -}; +import { API_ROUTES } from "./apiRoutes"; export const backupAPI = { /** @@ -20,7 +9,7 @@ export const backupAPI = { * @returns {Array} List of Available Backups */ async requestAvailable() { - let response = await apiReq.get(backupURLs.available); + let response = await apiReq.get(API_ROUTES.backupsAvailable); return response.data; }, /** @@ -30,7 +19,7 @@ export const backupAPI = { * @returns A report containing status of imported items */ async import(fileName, data) { - let response = await apiReq.post(backupURLs.importBackup(fileName), data); + let response = await apiReq.post(API_ROUTES.backupsFileNameImport(fileName), data); store.dispatch("requestRecentRecipes"); return response; }, @@ -40,7 +29,7 @@ export const backupAPI = { */ async delete(fileName) { return apiReq.delete( - backupURLs.deleteBackup(fileName), + API_ROUTES.backupsFileNameDelete(fileName), null, () => i18n.t("settings.backup.unable-to-delete-backup"), () => i18n.t("settings.backup.backup-deleted") @@ -53,7 +42,7 @@ export const backupAPI = { */ async create(options) { return apiReq.post( - backupURLs.createBackup, + API_ROUTES.backupsExportDatabase, options, () => i18n.t("settings.backup.error-creating-backup-see-log-file"), response => { @@ -67,7 +56,7 @@ export const backupAPI = { * @returns Download URL */ async download(fileName) { - const url = backupURLs.downloadBackup(fileName); + const url = API_ROUTES.backupsFileNameDownload(fileName); apiReq.download(url); }, }; diff --git a/frontend/src/api/category.js b/frontend/src/api/category.js index f457b27a..d06d4b5b 100644 --- a/frontend/src/api/category.js +++ b/frontend/src/api/category.js @@ -1,30 +1,20 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import { store } from "@/store"; import i18n from "@/i18n.js"; - -const prefix = baseURL + "categories"; - -const categoryURLs = { - getAll: `${prefix}`, - getEmpty: `${prefix}/empty`, - getCategory: category => `${prefix}/${category}`, - deleteCategory: category => `${prefix}/${category}`, - updateCategory: category => `${prefix}/${category}`, -}; +import { API_ROUTES } from "./apiRoutes"; export const categoryAPI = { async getAll() { - let response = await apiReq.get(categoryURLs.getAll); + let response = await apiReq.get(API_ROUTES.categories); return response.data; }, async getEmpty() { - let response = await apiReq.get(categoryURLs.getEmpty); + let response = await apiReq.get(API_ROUTES.categoriesEmpty); return response.data; }, async create(name) { const response = await apiReq.post( - categoryURLs.getAll, + API_ROUTES.categories, { name: name }, () => i18n.t("category.category-creation-failed"), () => i18n.t("category.category-created") @@ -35,12 +25,12 @@ export const categoryAPI = { } }, async getRecipesInCategory(category) { - let response = await apiReq.get(categoryURLs.getCategory(category)); + let response = await apiReq.get(API_ROUTES.categoriesCategory(category)); return response.data; }, async update(name, newName, overrideRequest = false) { const response = await apiReq.put( - categoryURLs.updateCategory(name), + API_ROUTES.categoriesCategory(name), { name: newName }, () => i18n.t("category.category-update-failed"), () => i18n.t("category.category-updated") @@ -52,7 +42,7 @@ export const categoryAPI = { }, async delete(category, overrideRequest = false) { const response = await apiReq.delete( - categoryURLs.deleteCategory(category), + API_ROUTES.categoriesCategory(category), null, () => i18n.t("category.category-deletion-failed"), () => i18n.t("category.category-deleted") @@ -64,28 +54,18 @@ export const categoryAPI = { }, }; -const tagPrefix = baseURL + "tags"; - -const tagURLs = { - getAll: `${tagPrefix}`, - getEmpty: `${tagPrefix}/empty`, - getTag: tag => `${tagPrefix}/${tag}`, - deleteTag: tag => `${tagPrefix}/${tag}`, - updateTag: tag => `${tagPrefix}/${tag}`, -}; - export const tagAPI = { async getAll() { - let response = await apiReq.get(tagURLs.getAll); + let response = await apiReq.get(API_ROUTES.tags); return response.data; }, async getEmpty() { - let response = await apiReq.get(tagURLs.getEmpty); + let response = await apiReq.get(API_ROUTES.tagsEmpty); return response.data; }, async create(name) { const response = await apiReq.post( - tagURLs.getAll, + API_ROUTES.tags, { name: name }, () => i18n.t("tag.tag-creation-failed"), () => i18n.t("tag.tag-created") @@ -96,12 +76,12 @@ export const tagAPI = { } }, async getRecipesInTag(tag) { - let response = await apiReq.get(tagURLs.getTag(tag)); + let response = await apiReq.get(API_ROUTES.tagsTag(tag)); return response.data; }, async update(name, newName, overrideRequest = false) { const response = await apiReq.put( - tagURLs.updateTag(name), + API_ROUTES.tagsTag(name), { name: newName }, () => i18n.t("tag.tag-update-failed"), () => i18n.t("tag.tag-updated") @@ -116,7 +96,7 @@ export const tagAPI = { }, async delete(tag, overrideRequest = false) { const response = await apiReq.delete( - tagURLs.deleteTag(tag), + API_ROUTES.tagsTag(tag), null, () => i18n.t("tag.tag-deletion-failed"), () => i18n.t("tag.tag-deleted") diff --git a/frontend/src/api/groups.js b/frontend/src/api/groups.js index 0ee1f177..62ad406b 100644 --- a/frontend/src/api/groups.js +++ b/frontend/src/api/groups.js @@ -1,15 +1,6 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import i18n from "@/i18n.js"; -const groupPrefix = baseURL + "groups"; - -const groupsURLs = { - groups: `${groupPrefix}`, - create: `${groupPrefix}`, - delete: id => `${groupPrefix}/${id}`, - current: `${groupPrefix}/self`, - update: id => `${groupPrefix}/${id}`, -}; +import { API_ROUTES } from "./apiRoutes"; function deleteErrorText(response) { switch (response.data.detail) { @@ -29,31 +20,31 @@ function deleteErrorText(response) { export const groupAPI = { async allGroups() { - let response = await apiReq.get(groupsURLs.groups); + let response = await apiReq.get(API_ROUTES.groups); return response.data; }, create(name) { return apiReq.post( - groupsURLs.create, + API_ROUTES.groups, { name: name }, () => i18n.t("group.user-group-creation-failed"), () => i18n.t("group.user-group-created") ); }, delete(id) { - return apiReq.delete(groupsURLs.delete(id), null, deleteErrorText, function() { + return apiReq.delete(API_ROUTES.groupsId(id), null, deleteErrorText, function() { return i18n.t("group.group-deleted"); }); }, async current() { - const response = await apiReq.get(groupsURLs.current, null, null); + const response = await apiReq.get(API_ROUTES.groupsSelf, null, null); if (response) { return response.data; } }, update(data) { return apiReq.put( - groupsURLs.update(data.id), + API_ROUTES.groupsId(data.id), data, () => i18n.t("group.error-updating-group"), () => i18n.t("settings.group-settings-updated") diff --git a/frontend/src/api/mealplan.js b/frontend/src/api/mealplan.js index 5296b694..f1e3dc8d 100644 --- a/frontend/src/api/mealplan.js +++ b/frontend/src/api/mealplan.js @@ -1,25 +1,11 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import i18n from "@/i18n.js"; - -const prefix = baseURL + "meal-plans/"; - -const mealPlanURLs = { - // Meals - all: `${prefix}all`, - create: `${prefix}create`, - thisWeek: `${prefix}this-week`, - byId: planID => `${prefix}${planID}`, - update: planID => `${prefix}${planID}`, - delete: planID => `${prefix}${planID}`, - today: `${prefix}today`, - shopping: planID => `${prefix}${planID}/shopping-list`, -}; +import { API_ROUTES } from "./apiRoutes"; export const mealplanAPI = { create(postBody) { return apiReq.post( - mealPlanURLs.create, + API_ROUTES.mealPlansCreate, postBody, () => i18n.t("meal-plan.mealplan-creation-failed"), () => i18n.t("meal-plan.mealplan-created") @@ -27,28 +13,28 @@ export const mealplanAPI = { }, async all() { - let response = await apiReq.get(mealPlanURLs.all); + let response = await apiReq.get(API_ROUTES.mealPlansAll); return response; }, async thisWeek() { - let response = await apiReq.get(mealPlanURLs.thisWeek); + let response = await apiReq.get(API_ROUTES.mealPlansThisWeek); return response.data; }, async today() { - let response = await apiReq.get(mealPlanURLs.today); + let response = await apiReq.get(API_ROUTES.mealPlansToday); return response; }, async getById(id) { - let response = await apiReq.get(mealPlanURLs.byId(id)); + let response = await apiReq.get(API_ROUTES.mealPlansId(id)); return response.data; }, delete(id) { return apiReq.delete( - mealPlanURLs.delete(id), + API_ROUTES.mealPlansId(id), null, () => i18n.t("meal-plan.mealplan-deletion-failed"), () => i18n.t("meal-plan.mealplan-deleted") @@ -57,7 +43,7 @@ export const mealplanAPI = { update(id, body) { return apiReq.put( - mealPlanURLs.update(id), + API_ROUTES.mealPlansId(id), body, () => i18n.t("meal-plan.mealplan-update-failed"), () => i18n.t("meal-plan.mealplan-updated") @@ -65,7 +51,7 @@ export const mealplanAPI = { }, async shoppingList(id) { - let response = await apiReq.get(mealPlanURLs.shopping(id)); + let response = await apiReq.get(API_ROUTES.mealPlansIdShoppingList(id)); return response.data; }, }; diff --git a/frontend/src/api/meta.js b/frontend/src/api/meta.js index 16f7477b..995fb2b8 100644 --- a/frontend/src/api/meta.js +++ b/frontend/src/api/meta.js @@ -1,45 +1,29 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; - -const prefix = baseURL + "debug"; - -const debugURLs = { - version: `${prefix}/version`, - debug: `${prefix}`, - lastRecipe: `${prefix}/last-recipe-json`, - demo: `${prefix}/is-demo`, - log: num => `${prefix}/log/${num}`, - statistics: `${prefix}/statistics`, -}; +import { API_ROUTES } from "./apiRoutes"; export const metaAPI = { async getAppInfo() { - const response = await apiReq.get(debugURLs.version); + const response = await apiReq.get(API_ROUTES.debugVersion); return response.data; }, async getDebugInfo() { - const response = await apiReq.get(debugURLs.debug); + const response = await apiReq.get(API_ROUTES.debug); return response.data; }, async getLogText(num) { - const response = await apiReq.get(debugURLs.log(num)); + const response = await apiReq.get(API_ROUTES.debugLog(num)); return response.data; }, async getLastJson() { - const response = await apiReq.get(debugURLs.lastRecipe); - return response.data; - }, - - async getIsDemo() { - const response = await apiReq.get(debugURLs.demo); + const response = await apiReq.get(API_ROUTES.debugLastRecipeJson); return response.data; }, async getStatistics() { - const response = await apiReq.get(debugURLs.statistics); + const response = await apiReq.get(API_ROUTES.debugStatistics); return response.data; }, }; diff --git a/frontend/src/api/migration.js b/frontend/src/api/migration.js index 321ab0ac..96d148a8 100644 --- a/frontend/src/api/migration.js +++ b/frontend/src/api/migration.js @@ -1,25 +1,16 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import { store } from "../store"; import i18n from "@/i18n.js"; - -const migrationBase = baseURL + "migrations"; - -const migrationURLs = { - // New - all: migrationBase, - delete: (folder, file) => `${migrationBase}/${folder}/${file}/delete`, - import: (folder, file) => `${migrationBase}/${folder}/${file}/import`, -}; +import { API_ROUTES } from "./apiRoutes"; export const migrationAPI = { async getMigrations() { - let response = await apiReq.get(migrationURLs.all); + let response = await apiReq.get(API_ROUTES.migrations); return response.data; }, async delete(folder, file) { const response = await apiReq.delete( - migrationURLs.delete(folder, file), + API_ROUTES.migrationsImportTypeFileNameDelete(folder, file), null, () => i18n.t("general.file-folder-not-found"), () => i18n.t("migration.migration-data-removed") @@ -27,7 +18,7 @@ export const migrationAPI = { return response; }, async import(folder, file) { - let response = await apiReq.post(migrationURLs.import(folder, file)); + let response = await apiReq.post(API_ROUTES.migrationsImportTypeFileNameImport(folder, file)); store.dispatch("requestRecentRecipes"); return response.data; }, diff --git a/frontend/src/api/recipe.js b/frontend/src/api/recipe.js index c05189d2..ed45e0ec 100644 --- a/frontend/src/api/recipe.js +++ b/frontend/src/api/recipe.js @@ -1,28 +1,8 @@ import { API_ROUTES } from "./apiRoutes"; import { apiReq } from "./api-utils"; -import { baseURL } from "./api-utils"; import { store } from "../store"; import i18n from "@/i18n.js"; -const prefix = baseURL + "recipes/"; - -const recipeURLs = { - allRecipes: baseURL + "recipes", - summary: baseURL + "recipes" + "/summary", - allRecipesByCategory: prefix + "category", - create: prefix + "create", - createByURL: prefix + "create-url", - testParseURL: prefix + "test-scrape-url", - recipe: slug => prefix + slug, - update: slug => prefix + slug, - delete: slug => prefix + slug, - createAsset: slug => `${prefix}${slug}/assets`, - recipeImage: slug => `${prefix}${slug}/image`, - updateImage: slug => `${prefix}${slug}/image`, - untagged: prefix + "summary/untagged", - uncategorized: prefix + "summary/uncategorized ", -}; - export const recipeAPI = { /** * Create a Recipe by URL @@ -30,7 +10,7 @@ export const recipeAPI = { * @returns {string} Recipe Slug */ async createByURL(recipeURL) { - const response = await apiReq.post(recipeURLs.createByURL, { url: recipeURL }, false, () => + const response = await apiReq.post(API_ROUTES.recipesCreateUrl, { url: recipeURL }, false, () => i18n.t("recipe.recipe-created") ); @@ -39,13 +19,13 @@ export const recipeAPI = { }, async getAllByCategory(categories) { - let response = await apiReq.post(recipeURLs.allRecipesByCategory, categories); + let response = await apiReq.post(API_ROUTES.recipesCategory, categories); return response.data; }, async create(recipeData) { const response = await apiReq.post( - recipeURLs.create, + API_ROUTES.recipesCreate, recipeData, () => i18n.t("recipe.recipe-creation-failed"), () => i18n.t("recipe.recipe-created") @@ -55,7 +35,7 @@ export const recipeAPI = { }, async requestDetails(recipeSlug) { - let response = await apiReq.get(recipeURLs.recipe(recipeSlug)); + let response = await apiReq.get(API_ROUTES.recipesRecipeSlug(recipeSlug)); return response.data; }, @@ -72,7 +52,7 @@ export const recipeAPI = { } return apiReq.put( - recipeURLs.updateImage(recipeSlug), + API_ROUTES.recipesRecipeSlugImage(recipeSlug), formData, () => i18n.t("general.image-upload-failed"), successMessage @@ -85,13 +65,13 @@ export const recipeAPI = { fd.append("extension", fileObject.name.split(".").pop()); fd.append("name", name); fd.append("icon", icon); - const response = apiReq.post(recipeURLs.createAsset(recipeSlug), fd); + const response = apiReq.post(API_ROUTES.recipesRecipeSlugAssets(recipeSlug), fd); return response; }, updateImagebyURL(slug, url) { return apiReq.post( - recipeURLs.updateImage(slug), + API_ROUTES.recipesRecipeSlugImage(slug), { url: url }, () => i18n.t("general.image-upload-failed"), () => i18n.t("recipe.recipe-image-updated") @@ -100,7 +80,7 @@ export const recipeAPI = { async update(data) { let response = await apiReq.put( - recipeURLs.update(data.slug), + API_ROUTES.recipesRecipeSlug(data.slug), data, () => i18n.t("recipe.recipe-update-failed"), () => i18n.t("recipe.recipe-updated") @@ -112,14 +92,14 @@ export const recipeAPI = { }, async patch(data) { - let response = await apiReq.patch(recipeURLs.update(data.slug), data); + let response = await apiReq.patch(API_ROUTES.recipesRecipeSlug(data.slug), data); store.dispatch("patchRecipe", response.data); return response.data; }, async delete(recipeSlug) { const response = await apiReq.delete( - recipeURLs.delete(recipeSlug), + API_ROUTES.recipesRecipeSlug(recipeSlug), null, () => i18n.t("recipe.unable-to-delete-recipe"), () => i18n.t("recipe.recipe-deleted") @@ -129,19 +109,19 @@ export const recipeAPI = { }, async allSummary(start = 0, limit = 9999) { - const response = await apiReq.get(recipeURLs.summary, { + const response = await apiReq.get(API_ROUTES.recipesSummary, { params: { start: start, limit: limit }, }); return response.data; }, async allUntagged() { - const response = await apiReq.get(recipeURLs.untagged); + const response = await apiReq.get(API_ROUTES.recipesSummaryUntagged); return response.data; }, async allUnategorized() { - const response = await apiReq.get(recipeURLs.uncategorized); + const response = await apiReq.get(API_ROUTES.recipesSummaryUncategorized); return response.data; }, @@ -186,7 +166,7 @@ export const recipeAPI = { }, async testScrapeURL(url) { - const response = await apiReq.post(recipeURLs.testParseURL, { url: url }); + const response = await apiReq.post(API_ROUTES.recipesTestScrapeUrl, { url: url }); return response.data; }, }; diff --git a/frontend/src/api/settings.js b/frontend/src/api/settings.js index 88ebced4..ffb2af34 100644 --- a/frontend/src/api/settings.js +++ b/frontend/src/api/settings.js @@ -1,27 +1,19 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; - -const settingsBase = baseURL + "site-settings"; - -const settingsURLs = { - siteSettings: `${settingsBase}`, - updateSiteSettings: `${settingsBase}`, - testWebhooks: `${settingsBase}/webhooks/test`, -}; +import { API_ROUTES } from "./apiRoutes"; export const settingsAPI = { async requestAll() { - let response = await apiReq.get(settingsURLs.siteSettings); + let response = await apiReq.get(API_ROUTES.siteSettings); return response.data; }, async testWebhooks() { - let response = await apiReq.post(settingsURLs.testWebhooks); + let response = await apiReq.post(API_ROUTES.siteSettingsWebhooksTest); return response.data; }, async update(body) { - let response = await apiReq.put(settingsURLs.updateSiteSettings, body); + let response = await apiReq.put(API_ROUTES.siteSettings, body); return response.data; }, }; diff --git a/frontend/src/api/signUps.js b/frontend/src/api/signUps.js index e3842844..946a8ea7 100644 --- a/frontend/src/api/signUps.js +++ b/frontend/src/api/signUps.js @@ -1,24 +1,15 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import i18n from "@/i18n.js"; - -const signUpPrefix = baseURL + "users/sign-ups"; - -const signUpURLs = { - all: `${signUpPrefix}`, - createToken: `${signUpPrefix}`, - deleteToken: token => `${signUpPrefix}/${token}`, - createUser: token => `${signUpPrefix}/${token}`, -}; +import { API_ROUTES } from "./apiRoutes"; export const signupAPI = { async getAll() { - let response = await apiReq.get(signUpURLs.all); + let response = await apiReq.get(API_ROUTES.usersSignUps); return response.data; }, async createToken(data) { let response = await apiReq.post( - signUpURLs.createToken, + API_ROUTES.usersSignUps, data, () => i18n.t("signup.sign-up-link-creation-failed"), () => i18n.t("signup.sign-up-link-created") @@ -27,7 +18,7 @@ export const signupAPI = { }, async deleteToken(token) { return await apiReq.delete( - signUpURLs.deleteToken(token), + API_ROUTES.usersSignUpsToken(token), null, () => i18n.t("signup.sign-up-token-deletion-failed"), () => i18n.t("signup.sign-up-token-deleted") @@ -35,7 +26,7 @@ export const signupAPI = { }, async createUser(token, data) { return apiReq.post( - signUpURLs.createUser(token), + API_ROUTES.usersSignUpsToken(token), data, () => i18n.t("user.you-are-not-allowed-to-create-a-user"), () => i18n.t("user.user-created") diff --git a/frontend/src/api/siteSettings.js b/frontend/src/api/siteSettings.js index 504ec339..0d25479d 100644 --- a/frontend/src/api/siteSettings.js +++ b/frontend/src/api/siteSettings.js @@ -1,27 +1,17 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import { store } from "@/store"; import i18n from "@/i18n.js"; - -const settingsBase = baseURL + "site-settings"; - -const settingsURLs = { - siteSettings: `${settingsBase}`, - updateSiteSettings: `${settingsBase}`, - testWebhooks: `${settingsBase}/webhooks/test`, - customPages: `${settingsBase}/custom-pages`, - customPage: id => `${settingsBase}/custom-pages/${id}`, -}; +import { API_ROUTES } from "./apiRoutes"; export const siteSettingsAPI = { async get() { - let response = await apiReq.get(settingsURLs.siteSettings); + let response = await apiReq.get(API_ROUTES.siteSettings); return response.data; }, async update(body) { const response = await apiReq.put( - settingsURLs.updateSiteSettings, + API_ROUTES.siteSettings, body, () => i18n.t("settings.settings-update-failed"), () => i18n.t("settings.settings-updated") @@ -33,18 +23,18 @@ export const siteSettingsAPI = { }, async getPages() { - let response = await apiReq.get(settingsURLs.customPages); + let response = await apiReq.get(API_ROUTES.siteSettingsCustomPages); return response.data; }, async getPage(id) { - let response = await apiReq.get(settingsURLs.customPage(id)); + let response = await apiReq.get(API_ROUTES.siteSettingsCustomPagesId(id)); return response.data; }, createPage(body) { return apiReq.post( - settingsURLs.customPages, + API_ROUTES.siteSettingsCustomPages, body, () => i18n.t("page.page-creation-failed"), () => i18n.t("page.new-page-created") @@ -53,7 +43,7 @@ export const siteSettingsAPI = { async deletePage(id) { return await apiReq.delete( - settingsURLs.customPage(id), + API_ROUTES.siteSettingsCustomPagesId(id), null, () => i18n.t("page.page-deletion-failed"), () => i18n.t("page.page-deleted") @@ -62,7 +52,7 @@ export const siteSettingsAPI = { updatePage(body) { return apiReq.put( - settingsURLs.customPage(body.id), + API_ROUTES.siteSettingsCustomPagesId(body.id), body, () => i18n.t("page.page-update-failed"), () => i18n.t("page.page-updated") @@ -71,7 +61,7 @@ export const siteSettingsAPI = { async updateAllPages(allPages) { let response = await apiReq.put( - settingsURLs.customPages, + API_ROUTES.siteSettingsCustomPages, allPages, () => i18n.t("page.pages-update-failed"), () => i18n.t("page.pages-updated") diff --git a/frontend/src/api/themes.js b/frontend/src/api/themes.js index 0917afb3..ba19e008 100644 --- a/frontend/src/api/themes.js +++ b/frontend/src/api/themes.js @@ -1,31 +1,21 @@ -import { baseURL } from "./api-utils"; import { apiReq } from "./api-utils"; import i18n from "@/i18n.js"; - -const prefix = baseURL + "themes"; - -const settingsURLs = { - allThemes: `${baseURL}themes`, - specificTheme: id => `${prefix}/${id}`, - createTheme: `${prefix}/create`, - updateTheme: id => `${prefix}/${id}`, - deleteTheme: id => `${prefix}/${id}`, -}; +import { API_ROUTES } from "./apiRoutes"; export const themeAPI = { async requestAll() { - let response = await apiReq.get(settingsURLs.allThemes); + let response = await apiReq.get(API_ROUTES.themes); return response.data; }, async requestByName(name) { - let response = await apiReq.get(settingsURLs.specificTheme(name)); + let response = await apiReq.get(API_ROUTES.themesId(name)); return response.data; }, async create(postBody) { return await apiReq.post( - settingsURLs.createTheme, + API_ROUTES.themesCreate, postBody, () => i18n.t("settings.theme.error-creating-theme-see-log-file"), () => i18n.t("settings.theme.theme-saved") @@ -34,7 +24,7 @@ export const themeAPI = { update(data) { return apiReq.put( - settingsURLs.updateTheme(data.id), + API_ROUTES.themesId(data.id), data, () => i18n.t("settings.theme.error-updating-theme"), () => i18n.t("settings.theme.theme-updated") @@ -43,7 +33,7 @@ export const themeAPI = { delete(id) { return apiReq.delete( - settingsURLs.deleteTheme(id), + API_ROUTES.themesId(id), null, () => i18n.t("settings.theme.error-deleting-theme"), () => i18n.t("settings.theme.theme-deleted") diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index 97299281..4e37009a 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -1,62 +1,44 @@ -import { baseURL } from "./api-utils"; import { API_ROUTES } from "./apiRoutes"; import { apiReq } from "./api-utils"; import axios from "axios"; import i18n from "@/i18n.js"; -const authPrefix = baseURL + "auth"; -const userPrefix = baseURL + "users"; - -const authURLs = { - token: `${authPrefix}/token`, - refresh: `${authPrefix}/refresh`, -}; - -const usersURLs = { - users: `${userPrefix}`, - self: `${userPrefix}/self`, - userID: id => `${userPrefix}/${id}`, - password: id => `${userPrefix}/${id}/password`, - resetPassword: id => `${userPrefix}/${id}/reset-password`, - userAPICreate: `${userPrefix}/api-tokens`, - userAPIDelete: id => `${userPrefix}/api-tokens/${id}`, -}; export const userAPI = { async login(formData) { - let response = await apiReq.post(authURLs.token, formData, null, function() { + let response = await apiReq.post(API_ROUTES.authToken, formData, null, function() { return i18n.t("user.user-successfully-logged-in"); }); return response; }, async refresh() { - let response = await axios.get(authURLs.refresh).catch(function(event) { + let response = await axios.get(API_ROUTES.authRefresh).catch(function(event) { console.log("Fetch failed", event); }); return response.data ? response.data : false; }, async allUsers() { - let response = await apiReq.get(usersURLs.users); + let response = await apiReq.get(API_ROUTES.users); return response.data; }, create(user) { return apiReq.post( - usersURLs.users, + API_ROUTES.users, user, () => i18n.t("user.user-creation-failed"), () => i18n.t("user.user-created") ); }, async self() { - let response = await apiReq.get(usersURLs.self); + let response = await apiReq.get(API_ROUTES.usersSelf); return response.data; }, async byID(id) { - let response = await apiReq.get(usersURLs.userID(id)); + let response = await apiReq.get(API_ROUTES.usersId(id)); return response.data; }, update(user) { return apiReq.put( - usersURLs.userID(user.id), + API_ROUTES.usersId(user.id), user, () => i18n.t("user.user-update-failed"), () => i18n.t("user.user-updated") @@ -64,7 +46,7 @@ export const userAPI = { }, changePassword(id, password) { return apiReq.put( - usersURLs.password(id), + API_ROUTES.usersIdPassword(id), password, () => i18n.t("user.existing-password-does-not-match"), () => i18n.t("user.password-updated") @@ -72,24 +54,24 @@ export const userAPI = { }, delete(id) { - return apiReq.delete(usersURLs.userID(id), null, deleteErrorText, () => { + return apiReq.delete(API_ROUTES.usersId(id), null, deleteErrorText, () => { return i18n.t("user.user-deleted"); }); }, resetPassword(id) { return apiReq.put( - usersURLs.resetPassword(id), + API_ROUTES.usersIdResetPassword(id), null, () => i18n.t("user.password-reset-failed"), () => i18n.t("user.password-has-been-reset-to-the-default-password") ); }, async createAPIToken(name) { - const response = await apiReq.post(usersURLs.userAPICreate, { name }); + const response = await apiReq.post(API_ROUTES.usersApiTokens, { name }); return response.data; }, async deleteAPIToken(id) { - const response = await apiReq.delete(usersURLs.userAPIDelete(id)); + const response = await apiReq.delete(API_ROUTES.usersApiTokensTokenId(id)); return response.data; }, /** Adds a Recipe to the users favorites diff --git a/frontend/src/components/UI/Dialogs/ImportDialog.vue b/frontend/src/components/UI/Dialogs/ImportDialog.vue index aab88a54..8a8de2e5 100644 --- a/frontend/src/components/UI/Dialogs/ImportDialog.vue +++ b/frontend/src/components/UI/Dialogs/ImportDialog.vue @@ -44,7 +44,7 @@ import { api } from "@/api"; import BaseDialog from "./BaseDialog"; import ImportOptions from "@/components/FormHelpers/ImportOptions"; import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue"; -import { backupURLs } from "@/api/backup"; +import { API_ROUTES } from "@/api/apiRoutes"; export default { components: { ImportOptions, TheDownloadBtn, BaseDialog }, props: { @@ -73,7 +73,7 @@ export default { }, computed: { downloadUrl() { - return backupURLs.downloadBackup(this.name); + return API_ROUTES.backupsFileNameDownload(this.name); }, }, methods: { diff --git a/frontend/src/main.js b/frontend/src/main.js index 2a87e397..aeed236b 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -8,7 +8,7 @@ import { globals } from "@/utils/globals"; import i18n from "./i18n"; import "@mdi/font/css/materialdesignicons.css"; import "typeface-roboto/index.css"; -import './registerServiceWorker' +import "./registerServiceWorker"; Vue.config.productionTip = false; Vue.use(VueRouter); diff --git a/frontend/src/utils/globals.js b/frontend/src/utils/globals.js index 44841fec..b17bbe61 100644 --- a/frontend/src/utils/globals.js +++ b/frontend/src/utils/globals.js @@ -23,12 +23,3 @@ const icons = { export const globals = { icons, }; - -/* - -import { globals } from "@/utils/globals" -globals: globals, - -{{ globals.icons. }} - -*/