mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-24 08:43:11 -05:00
Compare commits
52 Commits
v1.0.0-bet
...
v1.0.0beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56eb0bca71 | ||
|
|
eca8a96509 | ||
|
|
7eb80d18d2 | ||
|
|
37a673b34d | ||
|
|
3e7b8d4b71 | ||
|
|
abb114c375 | ||
|
|
12f480eb75 | ||
|
|
bc175d4ca9 | ||
|
|
f78c5eb359 | ||
|
|
5a0c034391 | ||
|
|
52fbf6b833 | ||
|
|
592b1de39d | ||
|
|
f29d5f1dff | ||
|
|
738ef0aaa7 | ||
|
|
f1fdec5afe | ||
|
|
4c594a48dc | ||
|
|
00f144a622 | ||
|
|
d2a9f7ca24 | ||
|
|
f831791db2 | ||
|
|
c3bdfe7b3b | ||
|
|
3542bb0927 | ||
|
|
e898c80f59 | ||
|
|
27c5cfc56b | ||
|
|
369cda0a61 | ||
|
|
b981cf62bf | ||
|
|
ee93d77ace | ||
|
|
3dcfcc1fa9 | ||
|
|
80f1a9add8 | ||
|
|
137bf9de91 | ||
|
|
1534f0df77 | ||
|
|
d751e3b35b | ||
|
|
07bf5be3ec | ||
|
|
a96f94a149 | ||
|
|
78a8204b58 | ||
|
|
649e34f66e | ||
|
|
010aafa69b | ||
|
|
d66d6c55ae | ||
|
|
7609715d9e | ||
|
|
921fceddea | ||
|
|
01f3fef21f | ||
|
|
8f7c7c39bb | ||
|
|
30d19c6503 | ||
|
|
ea503a0235 | ||
|
|
c05c123880 | ||
|
|
6f45de6167 | ||
|
|
d634e2fbe1 | ||
|
|
43a566339a | ||
|
|
cc284a0ceb | ||
|
|
3c19105d8b | ||
|
|
b8ee1a4bd8 | ||
|
|
c30ffbc851 | ||
|
|
3ddbc033b2 |
6
.github/stale.yml
vendored
6
.github/stale.yml
vendored
@@ -6,8 +6,12 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- early-stages
|
||||
- "bug: confirmed"
|
||||
- feedback
|
||||
- task
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
|
||||
4
.github/workflows/backend-docker-nightly.yml
vendored
4
.github/workflows/backend-docker-nightly.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
concurrency:
|
||||
group: backend-nightly-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
concurrency:
|
||||
group: frontend-nightly-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -46,5 +46,13 @@
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.sortImports.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",
|
||||
"netlify.toml": "runtime.txt",
|
||||
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
|
||||
"README.md": "LICENSE, SECURITY.md"
|
||||
}
|
||||
}
|
||||
|
||||
18
README.md
18
README.md
@@ -5,12 +5,6 @@
|
||||
[![MIT License][license-shield]][license-url]
|
||||
[![Docker Pulls][docker-pull]][docker-pull]
|
||||
[](https://www.codefactor.io/repository/github/hay-kot/mealie)
|
||||
[](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.release.yml)
|
||||
[](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
|
||||
[](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml)
|
||||
[](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
|
||||
|
||||
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
@@ -26,20 +20,14 @@
|
||||
<p align="center">
|
||||
A Place for All Your Recipes
|
||||
<br />
|
||||
<a href="https://hay-kot.github.io/mealie/"><strong>Explore the docs »</strong></a>
|
||||
<a href="https://nightly.mealie.io"><strong>Explore the docs »</strong></a>
|
||||
<a href="https://github.com/hay-kot/mealie">
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://mealie-demo.hay-kot.dev/">View Demo</a>
|
||||
<a href="https://demo.mealie.io/">View Demo</a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://hay-kot.github.io/mealie/api/redoc/">API</a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">
|
||||
Request Feature
|
||||
</a>
|
||||
·
|
||||
<a href="https://hub.docker.com/r/hkotel/mealie"> Docker Hub
|
||||
</a>
|
||||
</p>
|
||||
@@ -63,7 +51,7 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you're going to be working on the code-base you'll want to use the nightly documentation to ensure you get the latest information.
|
||||
|
||||
- See the [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/) for help getting started.
|
||||
- We use VSCode Dev Contains to make it easy for contributors to get started!
|
||||
- We use [VSCode Dev Containers](https://code.visualstudio.com/docs/remote/containers) to make it easy for contributors to get started!
|
||||
|
||||
If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
|
||||
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Since this software is still considered beta/WIP support is always only given for the latest version. Security patches are only available for the latest version and not back-ported to older versions.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
For general security vulnerabilities you're welcome to open a GitHub issues or contribute a fix. If you feel the vulnerability should not be disclosed you can open a generic issue on GitHub and email to the details to [ob92oy0sl@mozmail.com](mailto:ob92oy0sl@mozmail.com) which is monitored by the maintainer.
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Add use_abbreviation column to ingredients
|
||||
|
||||
Revision ID: ab0bae02578f
|
||||
Revises: 09dfc897ad62
|
||||
Create Date: 2022-06-01 11:12:06.748383
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ab0bae02578f"
|
||||
down_revision = "09dfc897ad62"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("ingredient_units", sa.Column("use_abbreviation", sa.Boolean(), nullable=True))
|
||||
|
||||
op.execute("UPDATE ingredient_units SET use_abbreviation = FALSE WHERE use_abbreviation IS NULL")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("ingredient_units", "use_abbreviation")
|
||||
# ### end Alembic commands ###
|
||||
64
cliff.toml
Normal file
64
cliff.toml
Normal file
@@ -0,0 +1,64 @@
|
||||
# configuration file for git-cliff (0.1.0)
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/hay-kot/mealie/issues/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features"},
|
||||
{ message = "^fix", group = "Bug Fixes"},
|
||||
{ message = "^doc", group = "Documentation"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^test", group = "Testing"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||
{ body = ".*security", group = "Security"},
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags chronologically
|
||||
date_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
@@ -1,3 +1,4 @@
|
||||
# WARNING: currently not functional, see #756, #1072
|
||||
# Use root/example as user/password credentials
|
||||
version: "3.4"
|
||||
services:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:80 {
|
||||
root * /srv
|
||||
encode gzip
|
||||
uri strip_suffix /
|
||||
|
||||
handle {
|
||||
try_files {path} {path}/ /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,11 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
wiki:
|
||||
container_name: mealie-docs
|
||||
image: mealie-docs
|
||||
ports:
|
||||
- 8888:80
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
29
docs/docs/changelog/v1.0.0beta-2.md
Normal file
29
docs/docs/changelog/v1.0.0beta-2.md
Normal file
@@ -0,0 +1,29 @@
|
||||
### Bug Fixes
|
||||
|
||||
- Bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend ([#1257](https://github.com/hay-kot/mealie/issues/1257))
|
||||
- Bump @nuxtjs/auth-next in /frontend ([#1265](https://github.com/hay-kot/mealie/issues/1265))
|
||||
- Bad dev dependency ([#1281](https://github.com/hay-kot/mealie/issues/1281))
|
||||
- Add touch support for mealplanner delete ([#1298](https://github.com/hay-kot/mealie/issues/1298))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add references for VSCode dev containers ([#1299](https://github.com/hay-kot/mealie/issues/1299))
|
||||
- Docker-compose.dev.yml is currently not functional ([#1300](https://github.com/hay-kot/mealie/issues/1300))
|
||||
|
||||
### Features
|
||||
|
||||
- Add reports to bulk recipe import (url) ([#1294](https://github.com/hay-kot/mealie/issues/1294))
|
||||
- Rewrite print implementation to support new ing ([#1305](https://github.com/hay-kot/mealie/issues/1305))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Github stalebot changes ([#1271](https://github.com/hay-kot/mealie/issues/1271))
|
||||
- Bump eslint-plugin-nuxt in /frontend ([#1258](https://github.com/hay-kot/mealie/issues/1258))
|
||||
- Bump @vue/runtime-dom in /frontend ([#1259](https://github.com/hay-kot/mealie/issues/1259))
|
||||
- Bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend ([#1260](https://github.com/hay-kot/mealie/issues/1260))
|
||||
- Bump vue2-script-setup-transform in /frontend ([#1263](https://github.com/hay-kot/mealie/issues/1263))
|
||||
- Update dev dependencies ([#1282](https://github.com/hay-kot/mealie/issues/1282))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Split up recipe create page ([#1283](https://github.com/hay-kot/mealie/issues/1283))
|
||||
36
docs/docs/changelog/v1.0.0beta-3.md
Normal file
36
docs/docs/changelog/v1.0.0beta-3.md
Normal file
@@ -0,0 +1,36 @@
|
||||
### Bug Fixes
|
||||
|
||||
- Update issue links in v1.0.0beta-2 changelog ([#1312](https://github.com/hay-kot/mealie/issues/1312))
|
||||
- Bad import path ([#1313](https://github.com/hay-kot/mealie/issues/1313))
|
||||
- Printer page refs ([#1314](https://github.com/hay-kot/mealie/issues/1314))
|
||||
- Consolidate stores to fix mismatched state
|
||||
- Bump @vue/composition-api from 1.6.1 to 1.6.2 in /frontend ([#1275](https://github.com/hay-kot/mealie/issues/1275))
|
||||
- Shopping list label editor ([#1333](https://github.com/hay-kot/mealie/issues/1333))
|
||||
|
||||
### Features
|
||||
|
||||
- Default unit fractions to True
|
||||
- Add unit abbreviation support ([#1332](https://github.com/hay-kot/mealie/issues/1332))
|
||||
- Attached images by drag and drop for recipe steps ([#1341](https://github.com/hay-kot/mealie/issues/1341))
|
||||
|
||||
### Docs
|
||||
|
||||
- Render homepage social media link images at 32x32 size ([#1310](https://github.com/hay-kot/mealie/issues/1310))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Init git-cliff config
|
||||
- Bump @types/sortablejs in /frontend ([#1287](https://github.com/hay-kot/mealie/issues/1287))
|
||||
- Bump @babel/eslint-parser in /frontend ([#1290](https://github.com/hay-kot/mealie/issues/1290))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Unify recipe-organizer components ([#1340](https://github.com/hay-kot/mealie/issues/1340))
|
||||
|
||||
### Security
|
||||
|
||||
- Delay server response whenever username is non existing ([#1338](https://github.com/hay-kot/mealie/issues/1338))
|
||||
|
||||
### Wip
|
||||
|
||||
- Pagination-repository ([#1316](https://github.com/hay-kot/mealie/issues/1316))
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
After reading through the [Code Contributions Guide](../developers-guide/code-contributions.md) and forking the repo you can start working. This project is developed with :whale: docker and as such you will be greatly aided by using docker for development. It's not necessary but it is helpful.
|
||||
|
||||
## With VS Code Dev Containers
|
||||
## With [VSCode Dev Containers](https://code.visualstudio.com/docs/remote/containers)
|
||||
|
||||
Prerequisites
|
||||
|
||||
@@ -110,7 +110,7 @@ frontend 🎬 Start Mealie Frontend Development Server
|
||||
frontend-build 🏗 Build Frontend in frontend/dist
|
||||
frontend-generate 🏗 Generate Code for Frontend
|
||||
frontend-lint 🧺 Run yarn lint
|
||||
docker-dev 🐳 Build and Start Docker Development Stack
|
||||
docker-dev 🐳 Build and Start Docker Development Stack (currently not functional, see #756, #1072)
|
||||
docker-prod 🐳 Build and Start Docker Production Stack
|
||||
|
||||
```
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## 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.
|
||||
You can change the theme by settings the environment variables on the frontend container.
|
||||
|
||||
Links:
|
||||
|
||||
- [Frontend Theme](/mealie/documentation/getting-started/installation/frontend-config#themeing)
|
||||
- [Frontend Theme](./installation/frontend-config#themeing)
|
||||
|
||||
## How can I change the language?
|
||||
|
||||
@@ -14,29 +22,29 @@ Languages need to be set on the frontend and backend containers as ENV variables
|
||||
|
||||
Links
|
||||
|
||||
- [Frontend Config](/mealie/documentation/getting-started/installation/frontend-config/)
|
||||
- [Backend Config](/mealie/documentation/getting-started/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](/mealie/documentation/getting-started/installation/backend-config/)
|
||||
- [Backend Config](./installation/backend-config/)
|
||||
|
||||
## Can I serve Mealie on a subpath?
|
||||
|
||||
No. Due to limitations from the Javascript Framework, mealie doesn't support serving Mealie on a subpath.
|
||||
|
||||
## Can I install Mealie without docker?
|
||||
## Can I install Mealie without docker?
|
||||
|
||||
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](/mealie/documentation/getting-started/installation/advanced/)
|
||||
- [Advanced Installation](./installation/advanced/)
|
||||
|
||||
## How I can attach an Image or Video to a Recipe?
|
||||
## 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>
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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://beta.mealie.io/recipe/create?tab=url){ .md-button .md-button--primary .align-right }
|
||||
[Demo](https://beta.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||
|
||||
### Importing Recipes
|
||||
|
||||
|
||||
@@ -85,6 +85,17 @@ These backups are just plain .zip files that you can download from the UI or acc
|
||||
|
||||
## Appendix
|
||||
|
||||
### Docker Tags
|
||||
|
||||
`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.
|
||||
|
||||
`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.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-nightly
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-3
|
||||
container_name: mealie-frontend
|
||||
depends_on:
|
||||
- mealie-api
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-nightly
|
||||
image: hkotel/mealie:api-v1.0.0beta-3
|
||||
container_name: mealie-api
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
@@ -12,7 +12,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-nightly
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-3
|
||||
container_name: mealie-frontend
|
||||
environment:
|
||||
# Set Frontend ENV Variables Here
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-nightly
|
||||
image: hkotel/mealie:api-v1.0.0beta-3
|
||||
container_name: mealie-api
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
|
||||
@@ -5,16 +5,12 @@
|
||||
|
||||
You should likely find bugs, errors, and unfinished pages within the application. To find the current status of the release you can checkout the [project on github](https://github.com/hay-kot/mealie/projects/7) or reach out on discord.
|
||||
|
||||
You should also be aware that Mealie v1 Beta does not have the backup/export feature available. This is the next priority for Mealie v1
|
||||
and is currently being worked out.
|
||||
|
||||
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
|
||||
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
|
||||
|
||||
[Remember to join the Discord](https://discord.gg/QuStdQGSGK)
|
||||
|
||||
|
||||
|
||||
|
||||
## Key Features
|
||||
- 🔍 Fuzzy search
|
||||
- 🏷️ Tag recipes with categories or tags to flexible sorting
|
||||
@@ -40,15 +36,15 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
## FAQ
|
||||
|
||||
### 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.
|
||||
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.
|
||||
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.
|
||||
- **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
|
||||
|
||||
@@ -68,7 +64,6 @@ As to why we need a database?
|
||||
|
||||
Contributions are what make the open source community such an amazing place to learn, develop, and create. Any contributions you make are **greatly appreciated**. See the [Contributors Guide](../../contributors/non-coders.md) for help getting started.
|
||||
|
||||
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and help me to know that there is a real demand for project development.
|
||||
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and help me to know that there is a real demand for project development.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -382,7 +382,7 @@
|
||||
target="_blank"
|
||||
title="github.com"
|
||||
>
|
||||
<svg viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
@@ -395,7 +395,7 @@
|
||||
target="_blank"
|
||||
title="twitter.com"
|
||||
>
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
@@ -408,7 +408,7 @@
|
||||
target="_blank"
|
||||
title="www.linkedin.com"
|
||||
>
|
||||
<svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
|
||||
@@ -88,6 +88,8 @@ nav:
|
||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||
|
||||
- Change Log:
|
||||
- v1.0.0beta-3: "changelog/v1.0.0beta-3.md"
|
||||
- v1.0.0beta-2: "changelog/v1.0.0beta-2.md"
|
||||
- v1.0.0 Beta: "changelog/v1.0.0.md"
|
||||
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
|
||||
- v0.5.1 Bug Fixes: "changelog/v0.5.1.md"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
settings: {
|
||||
"import/ignore": ["@vueuse*"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -35,6 +38,7 @@ module.exports = {
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/no-mutating-props": "off",
|
||||
"vue/no-v-text-v-html-on-component": "warn",
|
||||
"vue/no-v-for-template-key-on-child": "off",
|
||||
"vue/valid-v-slot": [
|
||||
"error",
|
||||
@@ -48,6 +52,8 @@ module.exports = {
|
||||
"ts-ignore": "allow-with-description",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": ["error", { paths: ["@vue/reactivity", "@vue/runtime-dom", "@vue/composition-api"] }],
|
||||
|
||||
// TODO Gradually activate all rules
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
|
||||
@@ -28,9 +28,13 @@
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.title"></v-list-item-title>
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle v-text="item.text"></v-list-item-subtitle>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.text }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>
|
||||
{{ $d(Date.parse(item.timeStamp), "long") }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -103,5 +107,4 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" label="Meal Type"></v-select>
|
||||
</div>
|
||||
|
||||
<RecipeCategoryTagSelector v-model="inputCategories" />
|
||||
<RecipeCategoryTagSelector v-model="inputTags" :tag-selector="true" />
|
||||
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
||||
|
||||
{{ 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` }}
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { RecipeTag, RecipeCategory } from "~/types/api-types/group";
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
{ text: "Breakfast", value: "breakfast" },
|
||||
@@ -38,7 +39,7 @@ const MEAL_DAY_OPTIONS = [
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagSelector,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
@@ -50,11 +51,11 @@ export default defineComponent({
|
||||
default: "unset",
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
type: Array as () => RecipeCategory[],
|
||||
default: () => [],
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
type: Array as () => RecipeTag[],
|
||||
default: () => [],
|
||||
},
|
||||
showHelp: {
|
||||
|
||||
@@ -10,13 +10,17 @@
|
||||
<v-list-item-icon class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon v-bind="attrs" v-on="on" v-text="getIconDefinition(item.icon).icon"></v-icon>
|
||||
<v-icon v-bind="attrs" v-on="on">
|
||||
{{ getIconDefinition(item.icon).icon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ getIconDefinition(item.icon).title }}</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title>
|
||||
<v-list-item-title class="pl-2">
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
|
||||
@@ -35,21 +39,21 @@
|
||||
<div class="d-flex ml-auto mt-2">
|
||||
<v-spacer></v-spacer>
|
||||
<BaseDialog
|
||||
v-model="newAssetDialog"
|
||||
:title="$t('asset.new-asset')"
|
||||
:icon="getIconDefinition(newAsset.icon).icon"
|
||||
v-model="state.newAssetDialog"
|
||||
:title="$tc('asset.new-asset')"
|
||||
:icon="getIconDefinition(state.newAsset.icon).icon"
|
||||
@submit="addAsset"
|
||||
>
|
||||
<template #activator>
|
||||
<BaseButton v-if="edit" small create @click="newAssetDialog = true" />
|
||||
<BaseButton v-if="edit" small create @click="state.newAssetDialog = true" />
|
||||
</template>
|
||||
<v-card-text class="pt-4">
|
||||
<v-text-field v-model="newAsset.name" dense :label="$t('general.name')"></v-text-field>
|
||||
<v-text-field v-model="state.newAsset.name" dense :label="$t('general.name')"></v-text-field>
|
||||
<div class="d-flex justify-space-between">
|
||||
<v-select
|
||||
v-model="newAsset.icon"
|
||||
v-model="state.newAsset.icon"
|
||||
dense
|
||||
:prepend-icon="getIconDefinition(newAsset.icon).icon"
|
||||
:prepend-icon="getIconDefinition(state.newAsset.icon).icon"
|
||||
:items="iconOptions"
|
||||
item-text="title"
|
||||
item-value="name"
|
||||
@@ -66,7 +70,7 @@
|
||||
</v-select>
|
||||
<AppButtonUpload :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
|
||||
</div>
|
||||
{{ fileObject.name }}
|
||||
{{ state.fileObject.name }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
@@ -74,9 +78,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, reactive, useContext } from "@nuxtjs/composition-api";
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { RecipeAsset } from "~/types/api-types/recipe";
|
||||
|
||||
const BASE_URL = window.location.origin;
|
||||
|
||||
@@ -91,7 +96,7 @@ export default defineComponent({
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
type: Array as () => RecipeAsset[],
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
@@ -181,7 +186,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
state,
|
||||
addAsset,
|
||||
assetURL,
|
||||
assetEmbed,
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot>
|
||||
<v-btn icon class="mt-n1" @click="dialog = true">
|
||||
<v-icon :color="color">{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</slot>
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-card>
|
||||
<v-app-bar dense dark color="primary mb-2">
|
||||
<v-icon large left class="mt-1">
|
||||
{{ $globals.icons.tags }}
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-title> </v-card-title>
|
||||
<v-form @submit.prevent="select">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="itemName"
|
||||
dense
|
||||
:label="inputLabel"
|
||||
:rules="[rules.required]"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton cancel @click="dialog = false" />
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton type="submit" create :disabled="!itemName" />
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const CREATED_ITEM_EVENT = "created-item";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
tagDialog: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const title = computed(() => props.tagDialog ? "Create a Tag" : "Create a Category");
|
||||
const inputLabel = computed(() => props.tagDialog ? "Tag Name" : "Category Name");
|
||||
|
||||
const rules = {
|
||||
required: (val: string) => !!val || "A Name is Required",
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dialog: false,
|
||||
itemName: "",
|
||||
});
|
||||
|
||||
watch(() => state.dialog, (val: boolean) => {
|
||||
if (!val) state.itemName = "";
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
async function select() {
|
||||
const newItem = await (async () => {
|
||||
if (props.tagDialog) {
|
||||
const { data } = await api.tags.createOne({ name: state.itemName });
|
||||
return data;
|
||||
} else {
|
||||
const { data } = await api.categories.createOne({ name: state.itemName });
|
||||
return data;
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(newItem);
|
||||
|
||||
context.emit(CREATED_ITEM_EVENT, newItem);
|
||||
state.dialog = false;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
title,
|
||||
inputLabel,
|
||||
rules,
|
||||
select,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,164 +0,0 @@
|
||||
//TODO: Prevent fetching Categories/Tags multiple time when selector is on page multiple times
|
||||
|
||||
<template>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
:items="activeItems"
|
||||
:value="value"
|
||||
:label="inputLabel"
|
||||
chips
|
||||
deletable-chips
|
||||
:dense="dense"
|
||||
item-text="name"
|
||||
persistent-hint
|
||||
multiple
|
||||
:hide-details="hideDetails"
|
||||
:hint="hint"
|
||||
:solo="solo"
|
||||
:return-object="returnObject"
|
||||
:prepend-inner-icon="$globals.icons.tags"
|
||||
v-bind="$attrs"
|
||||
@input="emitChange"
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
v-if="showSelected"
|
||||
:key="data.index"
|
||||
:small="dense"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
@click:close="removeByIndex(data.index)"
|
||||
>
|
||||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template #append-outer>
|
||||
<RecipeCategoryTagDialog v-if="showAdd" :tag-dialog="tagSelector" @created-item="pushToItem" />
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
|
||||
import { useTags, useCategories } from "~/composables/recipes";
|
||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||
|
||||
const MOUNTED_EVENT = "mounted";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagDialog,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | string)[],
|
||||
required: true,
|
||||
},
|
||||
solo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dense: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
returnObject: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tagSelector: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showAdd: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSelected: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hideDetails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||
getAllCategories();
|
||||
getAllTags();
|
||||
|
||||
const state = reactive({
|
||||
selected: props.value,
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
state.selected = val;
|
||||
}
|
||||
);
|
||||
|
||||
const { i18n } = useContext();
|
||||
const inputLabel = computed(() => {
|
||||
if (!props.showLabel) return null;
|
||||
return props.tagSelector ? i18n.t("tag.tags") : i18n.t("recipe.categories");
|
||||
});
|
||||
|
||||
const activeItems = computed(() => {
|
||||
let itemObjects: RecipeTag[] | RecipeCategory[] | null;
|
||||
if (props.tagSelector) itemObjects = allTags.value;
|
||||
else {
|
||||
itemObjects = allCategories.value;
|
||||
}
|
||||
if (props.returnObject) return itemObjects;
|
||||
else {
|
||||
return itemObjects?.map((x: RecipeTag | RecipeCategory) => x.name);
|
||||
}
|
||||
});
|
||||
|
||||
function emitChange() {
|
||||
context.emit("input", state.selected);
|
||||
}
|
||||
|
||||
// TODO Is this needed?
|
||||
onMounted(() => {
|
||||
context.emit(MOUNTED_EVENT);
|
||||
});
|
||||
|
||||
function removeByIndex(index: number) {
|
||||
state.selected.splice(index, 1);
|
||||
}
|
||||
|
||||
function pushToItem(createdItem: RecipeTag | RecipeCategory) {
|
||||
// TODO: Remove excessive get calls
|
||||
getAllCategories();
|
||||
getAllTags();
|
||||
state.selected.push(createdItem);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
inputLabel,
|
||||
activeItems,
|
||||
emitChange,
|
||||
removeByIndex,
|
||||
pushToItem,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,205 +0,0 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<BaseDialog
|
||||
v-model="ItemDeleteDialog"
|
||||
:title="`Delete ${itemName}`"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
@confirm="deleteItem()"
|
||||
>
|
||||
<v-card-text> Are you sure you want to delete this {{ itemName }}? </v-card-text>
|
||||
</BaseDialog>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
:bottom="!menuTop"
|
||||
:nudge-bottom="!menuTop ? '5' : '0'"
|
||||
:top="menuTop"
|
||||
:nudge-top="menuTop ? '5' : '0'"
|
||||
allow-overflow
|
||||
close-delay="125"
|
||||
open-on-hover
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="item.color" v-text="item.icon"></v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export interface ContextMenuIncludes {
|
||||
delete: boolean;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
title: string;
|
||||
icon: string;
|
||||
color: string | undefined;
|
||||
event: string;
|
||||
}
|
||||
|
||||
const ItemTypes = {
|
||||
tag: "tags",
|
||||
category: "categories",
|
||||
tool: "tools",
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
itemType: {
|
||||
type: String as () => string,
|
||||
required: true,
|
||||
},
|
||||
useItems: {
|
||||
type: Object as () => ContextMenuIncludes,
|
||||
default: () => ({
|
||||
delete: true,
|
||||
}),
|
||||
},
|
||||
// Append items are added at the end of the useItems list
|
||||
appendItems: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
default: () => [],
|
||||
},
|
||||
// Append items are added at the beginning of the useItems list
|
||||
leadingItems: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
default: () => [],
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: colors.grey.darken2,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
menuIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
id: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
ItemDeleteDialog: false,
|
||||
loading: false,
|
||||
menuItems: [] as ContextMenuItem[],
|
||||
itemName: "tag",
|
||||
});
|
||||
|
||||
const { i18n, $globals } = useContext();
|
||||
|
||||
let apiRoute = "tags" as "tags" | "categories" | "tools";
|
||||
|
||||
switch (props.itemType) {
|
||||
case ItemTypes.tag:
|
||||
state.itemName = "tag";
|
||||
apiRoute = "tags";
|
||||
break;
|
||||
case ItemTypes.category:
|
||||
state.itemName = "category";
|
||||
apiRoute = "categories";
|
||||
break;
|
||||
case ItemTypes.tool:
|
||||
state.itemName = "tool";
|
||||
apiRoute = "tools";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Setup
|
||||
|
||||
const defaultItems: { [key: string]: ContextMenuItem } = {
|
||||
delete: {
|
||||
title: i18n.t("general.delete") as string,
|
||||
icon: $globals.icons.delete,
|
||||
color: undefined,
|
||||
event: "delete",
|
||||
},
|
||||
};
|
||||
|
||||
// Get Default Menu Items Specified in Props
|
||||
for (const [key, value] of Object.entries(props.useItems)) {
|
||||
if (value) {
|
||||
const item = defaultItems[key];
|
||||
if (item) {
|
||||
state.menuItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add leading and Apppending Items
|
||||
state.menuItems = [...props.leadingItems, ...state.menuItems, ...props.appendItems];
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
async function deleteItem() {
|
||||
await api[apiRoute].deleteOne(props.id);
|
||||
context.emit("delete", props.id);
|
||||
}
|
||||
|
||||
// Note: Print is handled as an event in the parent component
|
||||
const eventHandlers: { [key: string]: () => void } = {
|
||||
delete: () => {
|
||||
state.ItemDeleteDialog = true;
|
||||
},
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
const handler = eventHandlers[eventKey];
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
handler();
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
context.emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
contextMenuEventHandler,
|
||||
deleteItem,
|
||||
icon,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,123 +0,0 @@
|
||||
<template>
|
||||
<div v-if="items">
|
||||
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
||||
<v-icon large left>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ headline }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
||||
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
||||
<v-row>
|
||||
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
||||
<v-card class="left-border" hover :to="`/recipes/${itemType}/${item.slug}`">
|
||||
<v-card-actions>
|
||||
<v-icon>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-card-title class="py-1">
|
||||
{{ item.name }}
|
||||
</v-card-title>
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeCategoryTagToolContextMenu
|
||||
:id="item.id"
|
||||
:item-type="itemType"
|
||||
:slug="item.slug"
|
||||
:name="item.name"
|
||||
:use-items="{
|
||||
delete: true,
|
||||
}"
|
||||
@delete="$emit('delete', item.id)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext, computed, useMeta } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolContextMenu from "./RecipeCategoryTagToolContextMenu.vue";
|
||||
|
||||
type ItemType = "tags" | "categories" | "tools";
|
||||
|
||||
const ItemTypes = {
|
||||
tag: "tags",
|
||||
category: "categories",
|
||||
tool: "tools",
|
||||
};
|
||||
|
||||
interface GenericItem {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCategoryTagToolContextMenu },
|
||||
props: {
|
||||
itemType: {
|
||||
type: String as () => ItemType,
|
||||
required: true,
|
||||
},
|
||||
items: {
|
||||
type: Array as () => GenericItem[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { i18n, $globals } = useContext();
|
||||
|
||||
const state = reactive({
|
||||
headline: "tags",
|
||||
icon: $globals.icons.tags,
|
||||
});
|
||||
|
||||
switch (props.itemType) {
|
||||
case ItemTypes.tag:
|
||||
state.headline = i18n.t("tag.tags") as string;
|
||||
break;
|
||||
case ItemTypes.category:
|
||||
state.headline = i18n.t("category.categories") as string;
|
||||
break;
|
||||
case ItemTypes.tool:
|
||||
state.headline = i18n.t("tool.tools") as string;
|
||||
state.icon = $globals.icons.potSteam;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
useMeta(() => ({
|
||||
title: state.headline,
|
||||
}));
|
||||
|
||||
const itemsSorted = computed(() => {
|
||||
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
||||
|
||||
if (!props.items) return byLetter;
|
||||
|
||||
props.items.forEach((item) => {
|
||||
const letter = item.name[0].toUpperCase();
|
||||
if (!byLetter[letter]) {
|
||||
byLetter[letter] = [];
|
||||
}
|
||||
|
||||
byLetter[letter].push(item);
|
||||
});
|
||||
|
||||
return byLetter;
|
||||
});
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
itemsSorted,
|
||||
};
|
||||
},
|
||||
// Needed for useMeta
|
||||
head: {},
|
||||
});
|
||||
</script>
|
||||
@@ -81,7 +81,7 @@
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="item.color" v-text="item.icon"></v-icon>
|
||||
<v-icon :color="item.color"> {{ item.icon }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -307,7 +307,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// Note: Print is handled as an event in the parent component
|
||||
const eventHandlers: { [key: string]: () => void } = {
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
delete: () => {
|
||||
state.recipeDeleteDialog = true;
|
||||
},
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
<img src="https://i.pravatar.cc/300" alt="John" />
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="getMember(item.userId)"></v-list-item-title>
|
||||
<v-list-item-title>
|
||||
{{ getMember(item.userId) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -95,31 +97,30 @@ export default defineComponent({
|
||||
context.emit(INPUT_EVENT, value);
|
||||
}
|
||||
|
||||
const show = props.showHeaders;
|
||||
const headers = computed(() => {
|
||||
const hdrs = [];
|
||||
|
||||
if (show.id) {
|
||||
if (props.showHeaders.id) {
|
||||
hdrs.push({ text: "Id", value: "id" });
|
||||
}
|
||||
if (show.owner) {
|
||||
if (props.showHeaders.owner) {
|
||||
hdrs.push({ text: "Owner", value: "userId", align: "center" });
|
||||
}
|
||||
hdrs.push({ text: "Name", value: "name" });
|
||||
if (show.categories) {
|
||||
if (props.showHeaders.categories) {
|
||||
hdrs.push({ text: "Categories", value: "recipeCategory" });
|
||||
}
|
||||
|
||||
if (show.tags) {
|
||||
if (props.showHeaders.tags) {
|
||||
hdrs.push({ text: "Tags", value: "tags" });
|
||||
}
|
||||
if (show.tools) {
|
||||
if (props.showHeaders.tools) {
|
||||
hdrs.push({ text: "Tools", value: "tools" });
|
||||
}
|
||||
if (show.recipeYield) {
|
||||
if (props.showHeaders.recipeYield) {
|
||||
hdrs.push({ text: "Yield", value: "recipeYield" });
|
||||
}
|
||||
if (show.dateAdded) {
|
||||
if (props.showHeaders.dateAdded) {
|
||||
hdrs.push({ text: "Date Added", value: "dateAdded" });
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, toRefs, reactive, useContext } from "@nuxtjs/composition-api";
|
||||
import { whenever } from "@vueuse/shared";
|
||||
import { useClipboard, useShare } from "@vueuse/core";
|
||||
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
||||
import { RecipeShareToken } from "~/types/api-types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRefs } from "@nuxtjs/composition-api";
|
||||
import { useFoods, useUnits } from "~/composables/recipes";
|
||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { RecipeIngredient } from "~/types/api-types/recipe";
|
||||
|
||||
@@ -134,28 +134,30 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { value } = props;
|
||||
|
||||
// ==================================================
|
||||
// Foods
|
||||
const { foods, workingFoodData, actions: foodActions } = useFoods();
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
const foodSearch = ref("");
|
||||
|
||||
async function createAssignFood() {
|
||||
workingFoodData.name = foodSearch.value;
|
||||
await foodActions.createOne();
|
||||
value.food = foods.value?.find((food) => food.name === foodSearch.value);
|
||||
foodData.data.name = foodSearch.value;
|
||||
await foodStore.actions.createOne(foodData.data);
|
||||
props.value.food = foodStore.foods.value?.find((food) => food.name === foodSearch.value);
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Units
|
||||
const { units, workingUnitData, actions: unitActions } = useUnits();
|
||||
const unitStore = useUnitStore();
|
||||
const unitsData = useUnitData();
|
||||
const unitSearch = ref("");
|
||||
|
||||
async function createAssignUnit() {
|
||||
workingUnitData.name = unitSearch.value;
|
||||
await unitActions.createOne();
|
||||
value.unit = units.value?.find((unit) => unit.name === unitSearch.value);
|
||||
unitsData.data.name = unitSearch.value;
|
||||
await unitStore.actions.createOne(unitsData.data);
|
||||
props.value.unit = unitStore.units.value?.find((unit) => unit.name === unitSearch.value);
|
||||
unitsData.reset();
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
@@ -165,7 +167,7 @@ export default defineComponent({
|
||||
|
||||
function toggleTitle() {
|
||||
if (state.showTitle) {
|
||||
value.title = "";
|
||||
props.value.title = "";
|
||||
}
|
||||
state.showTitle = !state.showTitle;
|
||||
}
|
||||
@@ -175,13 +177,21 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function handleUnitEnter() {
|
||||
if (value.unit === undefined || value.unit === null || !value.unit.name.includes(unitSearch.value)) {
|
||||
if (
|
||||
props.value.unit === undefined ||
|
||||
props.value.unit === null ||
|
||||
!props.value.unit.name.includes(unitSearch.value)
|
||||
) {
|
||||
createAssignUnit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFoodEnter() {
|
||||
if (value.food === undefined || value.food === null || !value.food.name.includes(foodSearch.value)) {
|
||||
if (
|
||||
props.value.food === undefined ||
|
||||
props.value.food === null ||
|
||||
!props.value.food.name.includes(foodSearch.value)
|
||||
) {
|
||||
createAssignFood();
|
||||
}
|
||||
}
|
||||
@@ -202,7 +212,7 @@ export default defineComponent({
|
||||
// });
|
||||
// }
|
||||
|
||||
if (value.originalText) {
|
||||
if (props.value.originalText) {
|
||||
options.push({
|
||||
text: "See Original Text",
|
||||
event: "toggle-original",
|
||||
@@ -220,22 +230,22 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
quantityFilter,
|
||||
toggleOriginalText,
|
||||
contextMenuOptions,
|
||||
handleUnitEnter,
|
||||
handleFoodEnter,
|
||||
...toRefs(state),
|
||||
createAssignFood,
|
||||
createAssignUnit,
|
||||
foods,
|
||||
foods: foodStore.foods,
|
||||
foodSearch,
|
||||
toggleTitle,
|
||||
unitActions,
|
||||
units,
|
||||
unitActions: unitStore.actions,
|
||||
units: unitStore.units,
|
||||
unitSearch,
|
||||
validators,
|
||||
workingUnitData,
|
||||
workingUnitData: unitsData.data,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
@click="toggleCollapseSection(index)"
|
||||
>
|
||||
<v-toolbar-title v-if="!edit" class="headline">
|
||||
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
|
||||
<v-app-bar-title> {{ step.title }} </v-app-bar-title>
|
||||
</v-toolbar-title>
|
||||
<v-text-field
|
||||
v-if="edit"
|
||||
@@ -168,12 +168,26 @@
|
||||
</v-icon>
|
||||
</v-fade-transition>
|
||||
</v-card-title>
|
||||
<v-card-text v-if="edit">
|
||||
|
||||
<!-- Content -->
|
||||
<v-card-text
|
||||
v-if="edit"
|
||||
:class="{
|
||||
blur: imageUploadMode,
|
||||
}"
|
||||
@drop.stop.prevent="handleImageDrop(index, $event)"
|
||||
>
|
||||
<MarkdownEditor
|
||||
v-model="value[index]['text']"
|
||||
class="mb-2"
|
||||
:preview.sync="previewStates[index]"
|
||||
:display-preview="false"
|
||||
:textarea="{
|
||||
hint: 'Attach images by dragging & dropping them into the editor',
|
||||
persistentHint: true,
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="ing in step.ingredientReferences"
|
||||
:key="ing.referenceId"
|
||||
@@ -199,10 +213,11 @@
|
||||
import draggable from "vuedraggable";
|
||||
// @ts-ignore vue-markdown has no types
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { ref, toRefs, reactive, defineComponent, watch, onMounted } from "@nuxtjs/composition-api";
|
||||
import { RecipeStep, IngredientReferences, RecipeIngredient } from "~/types/api-types/recipe";
|
||||
import { ref, toRefs, reactive, defineComponent, watch, onMounted, useContext } from "@nuxtjs/composition-api";
|
||||
import { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset } from "~/types/api-types/recipe";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import { uuid4 } from "~/composables/use-utils";
|
||||
import { uuid4, detectServerBaseUrl } from "~/composables/use-utils";
|
||||
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
||||
|
||||
interface MergerHistory {
|
||||
target: number;
|
||||
@@ -237,9 +252,26 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
recipeSlug: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
assets: {
|
||||
type: Array as () => RecipeAsset[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
const { req } = useContext();
|
||||
const BASE_URL = detectServerBaseUrl(req);
|
||||
|
||||
console.log("Base URL", BASE_URL);
|
||||
|
||||
const state = reactive({
|
||||
dialog: false,
|
||||
disabledSteps: [] as number[],
|
||||
@@ -368,7 +400,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function autoSetReferences() {
|
||||
// Ingore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
|
||||
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
|
||||
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
|
||||
// at the food variable and seeing if the food is in the instructions, but I still need to support those who don't want to provide the value
|
||||
// and only use the "notes" feature.
|
||||
@@ -493,7 +525,59 @@ export default defineComponent({
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
// ===============================================================
|
||||
// Image Uploader
|
||||
const api = useUserApi();
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
|
||||
const imageUploadMode = ref(false);
|
||||
|
||||
function toggleDragMode() {
|
||||
console.log("Toggling Drag Mode");
|
||||
imageUploadMode.value = !imageUploadMode.value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.assets === undefined) {
|
||||
context.emit("update:assets", []);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleImageDrop(index: number, e: DragEvent) {
|
||||
if (!e.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the file is an image
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (!file || !file.type.startsWith("image/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.recipes.createAsset(props.recipeSlug, {
|
||||
name: file.name,
|
||||
icon: "mdi-file-image",
|
||||
file,
|
||||
extension: file.name.split(".").pop() || "",
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return; // TODO: Handle error
|
||||
}
|
||||
|
||||
context.emit("update:assets", [...props.assets, data]);
|
||||
const assetUrl = BASE_URL + recipeAssetPath(props.recipeId, data.fileName as string);
|
||||
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
|
||||
props.value[index].text += text;
|
||||
}
|
||||
|
||||
return {
|
||||
// Image Uploader
|
||||
toggleDragMode,
|
||||
handleImageDrop,
|
||||
imageUploadMode,
|
||||
|
||||
// Rest
|
||||
drag,
|
||||
togglePreviewState,
|
||||
toggleCollapseSection,
|
||||
@@ -553,4 +637,21 @@ export default defineComponent({
|
||||
.list-group-item i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blur {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.upload-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
152
frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
Normal file
152
frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-card>
|
||||
<v-app-bar dense dark color="primary mb-2">
|
||||
<v-icon large left class="mt-1">
|
||||
{{ itemType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags }}
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ properties.title }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-title> </v-card-title>
|
||||
<v-form @submit.prevent="select">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
dense
|
||||
:label="properties.label"
|
||||
:rules="[rules.required]"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<v-checkbox v-if="itemType === Organizer.Tool" v-model="onHand" label="On Hand"></v-checkbox>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton cancel @click="dialog = false" />
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton type="submit" create :disabled="!name" />
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { RecipeOrganizer, Organizer } from "~/types/recipe/organizers";
|
||||
|
||||
const CREATED_ITEM_EVENT = "created-item";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
tagDialog: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
itemType: {
|
||||
type: String as () => RecipeOrganizer,
|
||||
default: "category",
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
name: "",
|
||||
onHand: false,
|
||||
});
|
||||
|
||||
const dialog = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
context.emit("input", value);
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val: boolean) => {
|
||||
if (!val) state.name = "";
|
||||
}
|
||||
);
|
||||
|
||||
const userApi = useUserApi();
|
||||
|
||||
const store = (() => {
|
||||
switch (props.itemType) {
|
||||
case Organizer.Tag:
|
||||
return useTagStore();
|
||||
case Organizer.Tool:
|
||||
return useToolStore();
|
||||
default:
|
||||
return useCategoryStore();
|
||||
}
|
||||
})();
|
||||
|
||||
const properties = computed(() => {
|
||||
switch (props.itemType) {
|
||||
case Organizer.Tag:
|
||||
return {
|
||||
title: "Create a Tag",
|
||||
label: "Tag Name",
|
||||
api: userApi.tags,
|
||||
};
|
||||
case Organizer.Tool:
|
||||
return {
|
||||
title: "Create a Tool",
|
||||
label: "Tool Name",
|
||||
api: userApi.tools,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Create a Category",
|
||||
label: "Category Name",
|
||||
api: userApi.categories,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const rules = {
|
||||
required: (val: string) => !!val || "A Name is Required",
|
||||
};
|
||||
|
||||
async function select() {
|
||||
if (store) {
|
||||
// @ts-ignore - only property really required is the name
|
||||
await store.actions.createOne({ name: state.name });
|
||||
}
|
||||
|
||||
const newItem = store.items.value.find((item) => item.name === state.name);
|
||||
|
||||
context.emit(CREATED_ITEM_EVENT, newItem);
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
dialog,
|
||||
properties,
|
||||
rules,
|
||||
select,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
139
frontend/components/Domain/Recipe/RecipeOrganizerPage.vue
Normal file
139
frontend/components/Domain/Recipe/RecipeOrganizerPage.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div v-if="items">
|
||||
<RecipeOrganizerDialog v-model="dialog" :item-type="itemType" />
|
||||
|
||||
<BaseDialog
|
||||
v-if="deleteTarget"
|
||||
v-model="deleteDialog"
|
||||
:title="`Delete ${deleteTarget.name}`"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
@confirm="deleteOne()"
|
||||
>
|
||||
<v-card-text> Are you sure you want to delete this {{ deleteTarget.name }}? </v-card-text>
|
||||
</BaseDialog>
|
||||
<v-app-bar color="transparent" flat class="mt-n1 rounded align-center">
|
||||
<v-icon large left>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
<slot name="title">
|
||||
{{ headline }}
|
||||
</slot>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton create @click="dialog = true" />
|
||||
</v-app-bar>
|
||||
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
||||
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
||||
<v-row>
|
||||
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
||||
<v-card class="left-border" hover :to="`/recipes/${itemType}/${item.slug}`">
|
||||
<v-card-actions>
|
||||
<v-icon>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-card-title class="py-1">
|
||||
{{ item.name }}
|
||||
</v-card-title>
|
||||
<v-spacer></v-spacer>
|
||||
<ContextMenu :items="[presets.delete]" @delete="confirmDelete(item)" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
import { useContextPresets } from "~/composables/use-context-presents";
|
||||
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||
import { RecipeOrganizer } from "~/types/recipe/organizers";
|
||||
|
||||
interface GenericItem {
|
||||
id?: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeOrganizerDialog,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => GenericItem[],
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemType: {
|
||||
type: String as () => RecipeOrganizer,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
// =================================================================
|
||||
// Sorted Items
|
||||
const itemsSorted = computed(() => {
|
||||
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
||||
|
||||
if (!props.items) return byLetter;
|
||||
|
||||
props.items.forEach((item) => {
|
||||
const letter = item.name[0].toUpperCase();
|
||||
if (!byLetter[letter]) {
|
||||
byLetter[letter] = [];
|
||||
}
|
||||
byLetter[letter].push(item);
|
||||
});
|
||||
|
||||
for (const key in byLetter) {
|
||||
byLetter[key] = byLetter[key].sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
return byLetter;
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Context Menu
|
||||
const presets = useContextPresets();
|
||||
|
||||
const deleteTarget = ref<GenericItem | null>(null);
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
function confirmDelete(item: GenericItem) {
|
||||
deleteTarget.value = item;
|
||||
deleteDialog.value = true;
|
||||
}
|
||||
|
||||
function deleteOne() {
|
||||
if (!deleteTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("delete", deleteTarget.value.id);
|
||||
}
|
||||
|
||||
const dialog = ref(false);
|
||||
|
||||
return {
|
||||
dialog,
|
||||
confirmDelete,
|
||||
deleteOne,
|
||||
deleteDialog,
|
||||
deleteTarget,
|
||||
presets,
|
||||
itemsSorted,
|
||||
};
|
||||
},
|
||||
// Needed for useMeta
|
||||
head: {},
|
||||
});
|
||||
</script>
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
:items="storeItem"
|
||||
:value="value"
|
||||
:label="label"
|
||||
chips
|
||||
deletable-chips
|
||||
item-text="name"
|
||||
multiple
|
||||
:prepend-inner-icon="$globals.icons.tags"
|
||||
:prepend-inner-icon="selectorType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags"
|
||||
return-object
|
||||
v-bind="inputAttrs"
|
||||
>
|
||||
@@ -17,6 +17,7 @@
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
small
|
||||
close
|
||||
label
|
||||
color="accent"
|
||||
@@ -26,41 +27,55 @@
|
||||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-if="showAdd" #append-outer>
|
||||
<v-btn icon @click="dialog = true">
|
||||
<v-icon>
|
||||
{{ $globals.icons.create }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<RecipeOrganizerDialog v-model="dialog" :item-type="selectorType" @created-item="appendCreated" />
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { computed, onMounted } from "vue-demi";
|
||||
import RecipeOrganizerDialog from "./RecipeOrganizerDialog.vue";
|
||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||
import { RecipeTool } from "~/types/api-types/admin";
|
||||
|
||||
type OrganizerType = "tag" | "category" | "tool";
|
||||
import { useTagStore } from "~/composables/store/use-tag-store";
|
||||
import { useCategoryStore, useToolStore } from "~/composables/store";
|
||||
import { Organizer, RecipeOrganizer } from "~/types/recipe/organizers";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeOrganizerDialog,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[] | undefined,
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool | string)[] | undefined,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* The type of organizer to use.
|
||||
*/
|
||||
selectorType: {
|
||||
type: String as () => OrganizerType,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* List of items that are available to be chosen from
|
||||
*/
|
||||
items: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[],
|
||||
type: String as () => RecipeOrganizer,
|
||||
required: true,
|
||||
},
|
||||
inputAttrs: {
|
||||
type: Object as () => Record<string, any>,
|
||||
default: () => ({}),
|
||||
},
|
||||
returnObject: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAdd: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
@@ -81,27 +96,62 @@ export default defineComponent({
|
||||
|
||||
const label = computed(() => {
|
||||
switch (props.selectorType) {
|
||||
case "tag":
|
||||
case Organizer.Tag:
|
||||
return i18n.t("tag.tags");
|
||||
case "category":
|
||||
case Organizer.Category:
|
||||
return i18n.t("category.categories");
|
||||
case "tool":
|
||||
return "Tools";
|
||||
case Organizer.Tool:
|
||||
return i18n.t("tool.tools");
|
||||
default:
|
||||
return "Organizer";
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Store & Items Setup
|
||||
|
||||
const store = (() => {
|
||||
switch (props.selectorType) {
|
||||
case Organizer.Tag:
|
||||
return useTagStore();
|
||||
case Organizer.Tool:
|
||||
return useToolStore();
|
||||
default:
|
||||
return useCategoryStore();
|
||||
}
|
||||
})();
|
||||
|
||||
const items = computed(() => {
|
||||
if (!props.returnObject) {
|
||||
return store.items.value.map((item) => item.name);
|
||||
}
|
||||
return store.items.value;
|
||||
});
|
||||
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
|
||||
function appendCreated(item: RecipeTag | RecipeCategory | RecipeTool) {
|
||||
console.log(item);
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
selected.value = [...selected.value, item];
|
||||
}
|
||||
|
||||
const dialog = ref(false);
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
appendCreated,
|
||||
dialog,
|
||||
storeItem: items,
|
||||
label,
|
||||
selected,
|
||||
removeByIndex,
|
||||
|
||||
@@ -1,77 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="recipe" class="container print">
|
||||
<div>
|
||||
<h1>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#E58325"
|
||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||
/>
|
||||
</svg>
|
||||
{{ recipe.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="time-container">
|
||||
<RecipeTimeCard
|
||||
:prep-time="recipe.prepTime"
|
||||
:total-time="recipe.totalTime"
|
||||
:perform-time="recipe.performTime"
|
||||
/>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
<div>
|
||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
||||
<h2>{{ $t("recipe.ingredients") }}</h2>
|
||||
<ul>
|
||||
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
|
||||
<v-icon>
|
||||
{{ $globals.icons.checkboxBlankOutline }}
|
||||
</v-icon>
|
||||
<p>{{ ingredient.note }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ $t("recipe.instructions") }}</h2>
|
||||
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
||||
<h2 v-if="step.title">{{ step.title }}</h2>
|
||||
<div class="ml-5">
|
||||
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
|
||||
<VueMarkdown :source="step.text"> </VueMarkdown>
|
||||
</div>
|
||||
<div class="print-container">
|
||||
<section>
|
||||
<v-card-title class="headline pl-0">
|
||||
<v-icon left color="primary">
|
||||
{{ $globals.icons.primary }}
|
||||
</v-icon>
|
||||
{{ recipe.name }}
|
||||
</v-card-title>
|
||||
<RecipeTimeCard :prep-time="recipe.prepTime" :total-time="recipe.totalTime" :perform-time="recipe.performTime" />
|
||||
</section>
|
||||
|
||||
<v-card-text class="px-0">
|
||||
<VueMarkdown :source="recipe.description" />
|
||||
</v-card-text>
|
||||
|
||||
<section>
|
||||
<v-card-title class="headline pl-0"> {{ $t("recipe.ingredients") }} </v-card-title>
|
||||
<div class="ingredient-grid">
|
||||
<div class="ingredient-col-1">
|
||||
<ul>
|
||||
<li v-for="(text, index) in splitIngredients.firstHalf" :key="index" v-html="text" />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
|
||||
|
||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||
<h3>{{ note.title }}</h3>
|
||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
||||
<div class="ingredient-col-2">
|
||||
<ul>
|
||||
<li v-for="(text, index) in splitIngredients.secondHalf" :key="index" v-html="text" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<v-card-title class="headline pl-0">{{ $t("recipe.instructions") }}</v-card-title>
|
||||
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
||||
<h3 v-if="step.title" class="mb-2">{{ step.title }}</h3>
|
||||
<div class="ml-5">
|
||||
<h4>{{ $t("recipe.step-index", { step: index + 1 }) }}</h4>
|
||||
<VueMarkdown :source="step.text" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<v-divider v-if="hasNotes" class="grey my-4"></v-divider>
|
||||
|
||||
<section>
|
||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||
<h4>{{ note.title }}</h4>
|
||||
<VueMarkdown :source="note.text" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||
// @ts-ignore vue-markdown has no types
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
|
||||
type SplitIngredients = {
|
||||
firstHalf: string[];
|
||||
secondHalf: string[];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeTimeCard,
|
||||
@@ -83,6 +76,36 @@ export default defineComponent({
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const splitIngredients = computed<SplitIngredients>(() => {
|
||||
const firstHalf = props.recipe.recipeIngredient
|
||||
?.slice(0, Math.ceil(props.recipe.recipeIngredient.length / 2))
|
||||
.map((ingredient) => {
|
||||
return parseIngredientText(ingredient, props.recipe?.settings?.disableAmount || false);
|
||||
});
|
||||
|
||||
const secondHalf = props.recipe.recipeIngredient
|
||||
?.slice(Math.ceil(props.recipe.recipeIngredient.length / 2))
|
||||
.map((ingredient) => {
|
||||
return parseIngredientText(ingredient, props.recipe?.settings?.disableAmount || false);
|
||||
});
|
||||
|
||||
return {
|
||||
firstHalf: firstHalf || [],
|
||||
secondHalf: secondHalf || [],
|
||||
};
|
||||
});
|
||||
|
||||
const hasNotes = computed(() => {
|
||||
return props.recipe.notes && props.recipe.notes.length > 0;
|
||||
});
|
||||
|
||||
return {
|
||||
hasNotes,
|
||||
splitIngredients,
|
||||
parseIngredientText,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -90,26 +113,46 @@ export default defineComponent({
|
||||
@media print {
|
||||
body,
|
||||
html {
|
||||
margin-top: -40px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.v-main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.v-main__wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
h1 {
|
||||
margin-top: 0 !important;
|
||||
display: -webkit-box;
|
||||
display: flex;
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.015625em;
|
||||
font-weight: 300;
|
||||
padding: 0;
|
||||
<style scoped>
|
||||
.print-container {
|
||||
display: none;
|
||||
background-color: white;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.25rem;
|
||||
p {
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.25rem;
|
||||
.v-card__text {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ingredient-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -117,51 +160,7 @@ ul {
|
||||
}
|
||||
|
||||
li {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
list-style-type: none;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-left: 0.25rem;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.03125em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-right: 0.5rem;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.time-container {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.time-chip {
|
||||
border-radius: 0.25rem;
|
||||
border-color: black;
|
||||
border: 1px;
|
||||
border-top: 1px;
|
||||
}
|
||||
|
||||
.print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.print {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -122,8 +122,5 @@ export default defineComponent({
|
||||
listItem,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "vbase-nuxt",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -145,6 +145,8 @@ import { AutoFormItems } from "~/types/auto-forms";
|
||||
|
||||
const BLUR_EVENT = "blur";
|
||||
|
||||
type ValidatorKey = keyof typeof validators;
|
||||
|
||||
export default defineComponent({
|
||||
name: "AutoForm",
|
||||
props: {
|
||||
@@ -178,7 +180,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
function rulesByKey(keys?: string[] | null) {
|
||||
function rulesByKey(keys?: ValidatorKey[] | null) {
|
||||
if (keys === undefined || keys === null) {
|
||||
return [];
|
||||
}
|
||||
@@ -193,7 +195,7 @@ export default defineComponent({
|
||||
return list;
|
||||
}
|
||||
|
||||
const defaultRules = computed(() => rulesByKey(props.globalRules));
|
||||
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
|
||||
|
||||
function removeByIndex(list: never[], index: number) {
|
||||
// Removes the item at the index
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
||||
dark
|
||||
>
|
||||
<v-icon v-if="icon" size="40" v-text="icon" />
|
||||
<v-icon v-if="icon" size="40"> {{ icon }} </v-icon>
|
||||
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
||||
</v-sheet>
|
||||
</slot>
|
||||
|
||||
56
frontend/components/global/ContextMenu.vue
Normal file
56
frontend/components/global/ContextMenu.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
:bottom="!menuTop"
|
||||
:nudge-bottom="!menuTop ? '5' : '0'"
|
||||
:top="menuTop"
|
||||
:nudge-top="menuTop ? '5' : '0'"
|
||||
allow-overflow
|
||||
close-delay="125"
|
||||
open-on-hover
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||
<v-icon>{{ $globals.icons.dotsVertical }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in items" :key="index" @click="$emit(item.event)">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="item.color ? item.color : undefined">
|
||||
{{ item.icon }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { ContextMenuItem } from "~/composables/use-context-presents";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
required: true,
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "grey darken-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -2,18 +2,10 @@
|
||||
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$tc('language-dialog.choose-language')">
|
||||
<v-card-text>
|
||||
{{ $t("language-dialog.select-description") }}
|
||||
<v-autocomplete
|
||||
v-model="locale"
|
||||
:items="locales"
|
||||
item-text="name"
|
||||
class="my-3"
|
||||
hide-details
|
||||
outlined
|
||||
offset
|
||||
>
|
||||
<v-autocomplete v-model="locale" :items="locales" item-text="name" class="my-3" hide-details outlined offset>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||
<v-list-item-subtitle> {{ item.progress }}% {{ $tc("language-dialog.translated") }} </v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:buttons="[
|
||||
{
|
||||
icon: previewState ? $globals.icons.edit : $globals.icons.eye,
|
||||
text: previewState ? $t('general.edit') : 'Preview Markdown',
|
||||
text: previewState ? $tc('general.edit') : 'Preview Markdown',
|
||||
event: 'toggle',
|
||||
},
|
||||
]"
|
||||
@@ -14,14 +14,15 @@
|
||||
</div>
|
||||
<v-textarea
|
||||
v-if="!previewState"
|
||||
v-bind="textarea"
|
||||
v-model="inputVal"
|
||||
:class="label == '' ? '' : 'mt-5'"
|
||||
:label="label"
|
||||
auto-grow
|
||||
dense
|
||||
rows="4"
|
||||
></v-textarea>
|
||||
<VueMarkdown v-else :source="value"> </VueMarkdown>
|
||||
/>
|
||||
<VueMarkdown v-else :source="value" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +54,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const fallbackPreview = ref(false);
|
||||
@@ -84,5 +89,3 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<slot v-bind="{ state, toggle }"></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch } from "@nuxtjs/composition-api";
|
||||
import { useToggle } from "@vueuse/shared";
|
||||
import { useToggle } from "@vueuse/core";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -34,4 +34,3 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useContext } from "@nuxtjs/composition-api";
|
||||
import { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import { AdminAPI, Api } from "~/api";
|
||||
import { ApiRequestInstance, RequestResponse } from "~/types/api";
|
||||
import { PublicApi } from "~/api/public-api";
|
||||
|
||||
@@ -11,15 +11,15 @@ export const useStaticRoutes = () => {
|
||||
|
||||
// Methods to Generate reference urls for assets/images *
|
||||
function recipeImage(recipeId: string, version = "", key = 1) {
|
||||
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?&rnd=${key}&version=${version}`;
|
||||
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
function recipeSmallImage(recipeId: string, version = "", key = 1) {
|
||||
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?&rnd=${key}&version=${version}`;
|
||||
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
function recipeTinyImage(recipeId: string, version = "", key = 1) {
|
||||
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
|
||||
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
function recipeAssetPath(recipeId: string, assetName: string) {
|
||||
|
||||
95
frontend/composables/partials/use-actions-factory.ts
Normal file
95
frontend/composables/partials/use-actions-factory.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Ref, useAsync } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { BaseCRUDAPI } from "~/api/_base";
|
||||
|
||||
type BoundT = {
|
||||
id?: string | number;
|
||||
};
|
||||
|
||||
interface StoreActions<T extends BoundT> {
|
||||
getAll(): Ref<T[] | null>;
|
||||
refresh(): Promise<void>;
|
||||
createOne(createData: T): Promise<void>;
|
||||
updateOne(updateData: T): Promise<void>;
|
||||
deleteOne(id: string | number): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* useStoreActions is a factory function that returns a set of methods
|
||||
* that can be reused to manage the state of a data store without using
|
||||
* Vuex. This is primarily used for basic CRUD operations that required
|
||||
* a lot of refreshing hooks to be called on operations
|
||||
*/
|
||||
export function useStoreActions<T extends BoundT>(
|
||||
api: BaseCRUDAPI<unknown, T, unknown>,
|
||||
allRef: Ref<T[] | null> | null,
|
||||
loading: Ref<boolean>
|
||||
): StoreActions<T> {
|
||||
function getAll() {
|
||||
loading.value = true;
|
||||
const allItems = useAsync(async () => {
|
||||
const { data } = await api.getAll();
|
||||
|
||||
if (allRef) {
|
||||
allRef.value = data;
|
||||
}
|
||||
|
||||
return data ?? [];
|
||||
}, useAsyncKey());
|
||||
|
||||
loading.value = false;
|
||||
return allItems;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
const { data } = await api.getAll();
|
||||
|
||||
if (data && allRef) {
|
||||
allRef.value = data;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function createOne(createData: T) {
|
||||
loading.value = true;
|
||||
const { data } = await api.createOne(createData);
|
||||
if (data && allRef?.value) {
|
||||
allRef.value.push(data);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function updateOne(updateData: T) {
|
||||
if (!updateData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.updateOne(updateData.id, updateData);
|
||||
if (data && allRef?.value) {
|
||||
refresh();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { response } = await api.deleteOne(id);
|
||||
if (response && allRef?.value) {
|
||||
refresh();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
getAll,
|
||||
refresh,
|
||||
createOne,
|
||||
updateOne,
|
||||
deleteOne,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
export { useFraction } from "./use-fraction";
|
||||
export { useRecipe } from "./use-recipe";
|
||||
export { useFoods } from "./use-recipe-foods";
|
||||
export { useUnits } from "./use-recipe-units";
|
||||
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes";
|
||||
export { useTags, useCategories, allCategories, allTags } from "./use-tags-categories";
|
||||
export { parseIngredientText } from "./use-recipe-ingredients";
|
||||
export { useRecipeSearch } from "./use-recipe-search";
|
||||
export { useTools } from "./use-recipe-tools";
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import { IngredientFood } from "~/types/api-types/recipe";
|
||||
|
||||
let foodStore: Ref<IngredientFood[] | null> | null = null;
|
||||
|
||||
export const useFoods = function () {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
const deleteTargetId = ref(0);
|
||||
const validForm = ref(true);
|
||||
|
||||
const workingFoodData = reactive<IngredientFood>({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
labelId: undefined,
|
||||
});
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
loading.value = true;
|
||||
const units = useAsync(async () => {
|
||||
const { data } = await api.foods.getAll();
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
loading.value = true;
|
||||
const { data } = await api.foods.getAll();
|
||||
|
||||
if (data && foodStore) {
|
||||
foodStore.value = data;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
async createOne(domForm: VForm | null = null) {
|
||||
if (domForm && !domForm.validate()) {
|
||||
validForm.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.foods.createOne(workingFoodData);
|
||||
if (data && foodStore?.value) {
|
||||
foodStore.value.push(data);
|
||||
return data;
|
||||
} else {
|
||||
this.refreshAll();
|
||||
}
|
||||
domForm?.reset();
|
||||
validForm.value = true;
|
||||
this.resetWorking();
|
||||
loading.value = false;
|
||||
},
|
||||
async updateOne() {
|
||||
if (!workingFoodData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
console.log(workingFoodData);
|
||||
const { data } = await api.foods.updateOne(workingFoodData.id, workingFoodData);
|
||||
if (data && foodStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
loading.value = false;
|
||||
},
|
||||
async deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.foods.deleteOne(id);
|
||||
if (data && foodStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
},
|
||||
resetWorking() {
|
||||
workingFoodData.id = "";
|
||||
workingFoodData.name = "";
|
||||
workingFoodData.description = "";
|
||||
workingFoodData.labelId = undefined;
|
||||
},
|
||||
setWorking(item: IngredientFood) {
|
||||
workingFoodData.id = item.id;
|
||||
workingFoodData.name = item.name;
|
||||
workingFoodData.description = item.description || "";
|
||||
workingFoodData.labelId = item.labelId;
|
||||
},
|
||||
flushStore() {
|
||||
foodStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!foodStore) {
|
||||
foodStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { foods: foodStore, workingFoodData, deleteTargetId, actions, validForm };
|
||||
};
|
||||
@@ -19,6 +19,8 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
|
||||
|
||||
let returnQty = "";
|
||||
|
||||
let unitDisplay = unit?.name;
|
||||
|
||||
// casting to number is required as sometimes quantity is a string
|
||||
if (quantity && Number(quantity) !== 0) {
|
||||
console.log("Using Quantity", quantity, typeof quantity);
|
||||
@@ -34,8 +36,12 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
|
||||
} else {
|
||||
returnQty = (quantity * scale).toString();
|
||||
}
|
||||
|
||||
if (unit?.useAbbreviation && unit.abbreviation) {
|
||||
unitDisplay = unit.abbreviation;
|
||||
}
|
||||
}
|
||||
|
||||
const text = `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
|
||||
const text = `${returnQty} ${unitDisplay || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
|
||||
return sanitizeIngredientHTML(text);
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import { IngredientUnit } from "~/types/api-types/recipe";
|
||||
|
||||
let unitStore: Ref<IngredientUnit[] | null> | null = null;
|
||||
|
||||
export const useUnits = function () {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
const deleteTargetId = ref(0);
|
||||
const validForm = ref(true);
|
||||
|
||||
const workingUnitData: IngredientUnit = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
fraction: true,
|
||||
abbreviation: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
loading.value = true;
|
||||
const units = useAsync(async () => {
|
||||
const { data } = await api.units.getAll();
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
loading.value = true;
|
||||
const { data } = await api.units.getAll();
|
||||
|
||||
if (data && unitStore) {
|
||||
unitStore.value = data;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
async createOne(domForm: VForm | null = null) {
|
||||
if (domForm && !domForm.validate()) {
|
||||
validForm.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.units.createOne(workingUnitData);
|
||||
if (data && unitStore?.value) {
|
||||
unitStore.value.push(data);
|
||||
} else {
|
||||
this.refreshAll();
|
||||
}
|
||||
domForm?.reset();
|
||||
validForm.value = true;
|
||||
this.resetWorking();
|
||||
loading.value = false;
|
||||
},
|
||||
async updateOne() {
|
||||
if (!workingUnitData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.units.updateOne(workingUnitData.id, workingUnitData);
|
||||
if (data && unitStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
loading.value = false;
|
||||
},
|
||||
async deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.units.deleteOne(id);
|
||||
if (data && unitStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
},
|
||||
resetWorking() {
|
||||
workingUnitData.id = "";
|
||||
workingUnitData.name = "";
|
||||
workingUnitData.abbreviation = "";
|
||||
workingUnitData.description = "";
|
||||
},
|
||||
setWorking(item: IngredientUnit) {
|
||||
workingUnitData.id = item.id;
|
||||
workingUnitData.name = item.name;
|
||||
workingUnitData.fraction = item.fraction;
|
||||
workingUnitData.abbreviation = item.abbreviation;
|
||||
workingUnitData.description = item.description;
|
||||
},
|
||||
flushStore() {
|
||||
unitStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!unitStore) {
|
||||
unitStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { units: unitStore, workingUnitData, deleteTargetId, actions, validForm };
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Ref, ref, useAsync } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "../api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { CategoriesAPI } from "~/api/class-interfaces/organizer-categories";
|
||||
import { TagsAPI } from "~/api/class-interfaces/organizer-tags";
|
||||
import { RecipeTag, RecipeCategory } from "~/types/api-types/recipe";
|
||||
|
||||
export const allCategories = ref<RecipeCategory[] | null>([]);
|
||||
export const allTags = ref<RecipeTag[] | null>([]);
|
||||
|
||||
function baseTagsCategories(
|
||||
reference: Ref<RecipeCategory[] | null> | Ref<RecipeTag[] | null>,
|
||||
api: TagsAPI | CategoriesAPI
|
||||
) {
|
||||
function useAsyncGetAll() {
|
||||
useAsync(async () => {
|
||||
await refreshItems();
|
||||
}, useAsyncKey());
|
||||
}
|
||||
|
||||
async function refreshItems() {
|
||||
const { data } = await api.getAll();
|
||||
// @ts-ignore hotfix
|
||||
reference.value = data;
|
||||
}
|
||||
|
||||
async function createOne(payload: { name: string }) {
|
||||
const { data } = await api.createOne(payload);
|
||||
if (data) {
|
||||
refreshItems();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteOne(slug: string) {
|
||||
const { data } = await api.deleteOne(slug);
|
||||
if (data) {
|
||||
refreshItems();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateOne(slug: string, payload: { name: string }) {
|
||||
// @ts-ignore // TODO: Fix Typescript Issue - Unsure how to fix this while also keeping mixins
|
||||
const { data } = await api.updateOne(slug, payload);
|
||||
if (data) {
|
||||
refreshItems();
|
||||
}
|
||||
}
|
||||
|
||||
return { useAsyncGetAll, refreshItems, createOne, deleteOne, updateOne };
|
||||
}
|
||||
|
||||
export const useTags = function () {
|
||||
const api = useUserApi();
|
||||
return {
|
||||
allTags,
|
||||
...baseTagsCategories(allTags, api.tags),
|
||||
};
|
||||
};
|
||||
export const useCategories = function () {
|
||||
const api = useUserApi();
|
||||
return {
|
||||
allCategories,
|
||||
...baseTagsCategories(allCategories, api.categories),
|
||||
};
|
||||
};
|
||||
6
frontend/composables/store/index.ts
Normal file
6
frontend/composables/store/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { useFoodStore, useFoodData } from "./use-food-store";
|
||||
export { useUnitStore, useUnitData } from "./use-unit-store";
|
||||
export { useLabelStore, useLabelData } from "./use-label-store";
|
||||
export { useToolStore, useToolData } from "./use-tool-store";
|
||||
export { useCategoryStore, useCategoryData } from "./use-category-store";
|
||||
export { useTagStore, useTagData } from "./use-tag-store";
|
||||
47
frontend/composables/store/use-category-store.ts
Normal file
47
frontend/composables/store/use-category-store.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeCategory } from "~/types/api-types/admin";
|
||||
|
||||
const categoryStore: Ref<RecipeCategory[]> = ref([]);
|
||||
|
||||
export function useCategoryData() {
|
||||
const data = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
slug: undefined,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.slug = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCategoryStore() {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
|
||||
flushStore() {
|
||||
categoryStore.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!categoryStore.value || categoryStore.value?.length === 0) {
|
||||
actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
items: categoryStore,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
50
frontend/composables/store/use-food-store.ts
Normal file
50
frontend/composables/store/use-food-store.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientFood } from "~/types/api-types/recipe";
|
||||
|
||||
let foodStore: Ref<IngredientFood[] | null> | null = null;
|
||||
|
||||
/**
|
||||
* useFoodData returns a template reactive object
|
||||
* for managing the creation of units. It also provides a
|
||||
* function to reset the data back to the initial state.
|
||||
*/
|
||||
export const useFoodData = function () {
|
||||
const data: IngredientFood = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
labelId: undefined,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.description = "";
|
||||
data.labelId = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
|
||||
export const useFoodStore = function () {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
...useStoreActions(api.foods, foodStore, loading),
|
||||
flushStore() {
|
||||
foodStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!foodStore) {
|
||||
foodStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { foods: foodStore, actions };
|
||||
};
|
||||
49
frontend/composables/store/use-label-store.ts
Normal file
49
frontend/composables/store/use-label-store.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { MultiPurposeLabelOut } from "~/types/api-types/labels";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
let labelStore: Ref<MultiPurposeLabelOut[] | null> | null = null;
|
||||
|
||||
export function useLabelData() {
|
||||
const data = reactive({
|
||||
groupId: "",
|
||||
id: "",
|
||||
name: "",
|
||||
color: "",
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.groupId = "";
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.color = "";
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLabelStore() {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<MultiPurposeLabelOut>(api.multiPurposeLabels, labelStore, loading),
|
||||
flushStore() {
|
||||
labelStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!labelStore) {
|
||||
labelStore = actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
labels: labelStore,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
47
frontend/composables/store/use-tag-store.ts
Normal file
47
frontend/composables/store/use-tag-store.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeTag } from "~/types/api-types/admin";
|
||||
|
||||
const items: Ref<RecipeTag[]> = ref([]);
|
||||
|
||||
export function useTagData() {
|
||||
const data = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
slug: undefined,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.slug = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTagStore() {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<RecipeTag>(api.tags, items, loading),
|
||||
flushStore() {
|
||||
items.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!items.value || items.value?.length === 0) {
|
||||
actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
49
frontend/composables/store/use-tool-store.ts
Normal file
49
frontend/composables/store/use-tool-store.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeTool } from "~/types/api-types/recipe";
|
||||
|
||||
const toolStore: Ref<RecipeTool[]> = ref([]);
|
||||
|
||||
export function useToolData() {
|
||||
const data = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
slug: undefined,
|
||||
onHand: false,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.slug = undefined;
|
||||
data.onHand = false;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export function useToolStore() {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<RecipeTool>(api.tools, toolStore, loading),
|
||||
flushStore() {
|
||||
toolStore.value = [];
|
||||
},
|
||||
};
|
||||
|
||||
if (!toolStore.value || toolStore.value?.length === 0) {
|
||||
actions.getAll();
|
||||
}
|
||||
|
||||
return {
|
||||
items: toolStore,
|
||||
actions,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
52
frontend/composables/store/use-unit-store.ts
Normal file
52
frontend/composables/store/use-unit-store.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientUnit } from "~/types/api-types/recipe";
|
||||
|
||||
let unitStore: Ref<IngredientUnit[] | null> | null = null;
|
||||
|
||||
/**
|
||||
* useUnitData returns a template reactive object
|
||||
* for managing the creation of units. It also provides a
|
||||
* function to reset the data back to the initial state.
|
||||
*/
|
||||
export const useUnitData = function () {
|
||||
const data: IngredientUnit = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
fraction: true,
|
||||
abbreviation: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
function reset() {
|
||||
data.id = "";
|
||||
data.name = "";
|
||||
data.fraction = true;
|
||||
data.abbreviation = "";
|
||||
data.description = "";
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUnitStore = function () {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = {
|
||||
...useStoreActions<IngredientUnit>(api.units, unitStore, loading),
|
||||
flushStore() {
|
||||
unitStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!unitStore) {
|
||||
unitStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { units: unitStore, actions };
|
||||
};
|
||||
30
frontend/composables/use-context-presents.ts
Normal file
30
frontend/composables/use-context-presents.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
export interface ContextMenuItem {
|
||||
title: string;
|
||||
icon: string;
|
||||
event: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function useContextPresets(): { [key: string]: ContextMenuItem } {
|
||||
const { $globals, i18n } = useContext();
|
||||
|
||||
return {
|
||||
delete: {
|
||||
title: i18n.tc("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
event: "delete",
|
||||
},
|
||||
edit: {
|
||||
title: i18n.tc("general.edit"),
|
||||
icon: $globals.icons.edit,
|
||||
event: "edit",
|
||||
},
|
||||
save: {
|
||||
title: i18n.tc("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
event: "save",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"short": {
|
||||
"month": "short",
|
||||
"day": "numeric",
|
||||
"weekday": "long"
|
||||
"month": "kurz",
|
||||
"day": "numerisch",
|
||||
"weekday": "lang"
|
||||
},
|
||||
"medium": {
|
||||
"month": "long",
|
||||
"day": "numeric",
|
||||
"weekday": "long",
|
||||
"year": "numeric"
|
||||
"month": "lang",
|
||||
"day": "numerisch",
|
||||
"weekday": "lang",
|
||||
"year": "numerisch"
|
||||
},
|
||||
"long": {
|
||||
"year": "numeric",
|
||||
"month": "long",
|
||||
"day": "numeric",
|
||||
"weekday": "long",
|
||||
"hour": "numeric",
|
||||
"minute": "numeric"
|
||||
"year": "numerisch",
|
||||
"month": "lang",
|
||||
"day": "numerisch",
|
||||
"weekday": "lang",
|
||||
"hour": "numerisch",
|
||||
"minute": "numerisch"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"about": "Über",
|
||||
"about-mealie": "Über Mealie",
|
||||
"api-docs": "API Dokumentation",
|
||||
"api-port": "API Port",
|
||||
"api-port": "API-Port",
|
||||
"application-mode": "Anwendungsmodus",
|
||||
"database-type": "Datenbanktyp",
|
||||
"database-url": "Datenbank URL",
|
||||
@@ -33,7 +33,7 @@
|
||||
"show-assets": "Anhänge anzeigen"
|
||||
},
|
||||
"category": {
|
||||
"categories": "Categories",
|
||||
"categories": "Kategorien",
|
||||
"category-created": "Kategorie angelegt",
|
||||
"category-creation-failed": "Anlegen der Kategorie fehlgeschlagen",
|
||||
"category-deleted": "Kategorie entfernt",
|
||||
@@ -44,7 +44,7 @@
|
||||
"uncategorized-count": "{count} nicht kategorisierte"
|
||||
},
|
||||
"events": {
|
||||
"apprise-url": "Apprise URL",
|
||||
"apprise-url": "Apprise-URL",
|
||||
"database": "Datenbank",
|
||||
"delete-event": "Ereignis löschen",
|
||||
"new-notification-form-description": "Mealie verwendet die Apprise-Bibliothek, um Benachrichtigungen zu erzeugen. Sie bietet viele Optionen für Dienste an, die für Benachrichtigungen genutzt werden können. Werfe einen Blick in ihr Wiki für eine umfassende Anleitung zum Erstellen der URL für Ihren Dienst. Falls verfügbar, kann die Auswahl des Benachrichtigungstyps zusätzliche Funktionen enthalten.",
|
||||
@@ -66,7 +66,7 @@
|
||||
"create": "Erstellen",
|
||||
"created": "Erstellt",
|
||||
"custom": "Benutzerdefiniert",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboard": "Übersicht",
|
||||
"delete": "Löschen",
|
||||
"disabled": "Deaktiviert",
|
||||
"download": "Herunterladen",
|
||||
@@ -88,7 +88,7 @@
|
||||
"image-upload-failed": "Das Bild konnte nicht hochgeladen werden",
|
||||
"import": "Importieren",
|
||||
"json": "JSON",
|
||||
"keyword": "Keyword",
|
||||
"keyword": "Schlüsselwort",
|
||||
"link-copied": "Link kopiert",
|
||||
"loading-recipes": "Lade Rezepte",
|
||||
"monday": "Montag",
|
||||
@@ -118,7 +118,7 @@
|
||||
"success-count": "Erfolgreich: {count}",
|
||||
"sunday": "Sonntag",
|
||||
"templates": "Vorlagen:",
|
||||
"test": "Test",
|
||||
"test": "Testen",
|
||||
"themes": "Themen",
|
||||
"thursday": "Donnerstag",
|
||||
"token": "Token",
|
||||
@@ -131,10 +131,10 @@
|
||||
"view": "Ansicht",
|
||||
"wednesday": "Mittwoch",
|
||||
"yes": "Ja",
|
||||
"foods": "Foods",
|
||||
"units": "Units",
|
||||
"back": "Back",
|
||||
"next": "Next"
|
||||
"foods": "Speisen",
|
||||
"units": "Einheiten",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Bist du dir sicher, dass du die Gruppe <b>{groupName}<b/> löschen möchtest?",
|
||||
@@ -156,8 +156,8 @@
|
||||
"user-group-created": "Benutzergruppe angelegt",
|
||||
"user-group-creation-failed": "Anlegen der Benutzergruppe fehlgeschlagen",
|
||||
"settings": {
|
||||
"keep-my-recipes-private": "Keep My Recipes Private",
|
||||
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
|
||||
"keep-my-recipes-private": "Meine Rezepte privat halten",
|
||||
"keep-my-recipes-private-description": "Setzt Ihre Gruppe und alle Rezepte standardmäßig privat. Sie können dies später jederzeit ändern."
|
||||
}
|
||||
},
|
||||
"meal-plan": {
|
||||
@@ -206,7 +206,7 @@
|
||||
"error-details": "Mealie kann Rezepte nur von Webseiten importieren, die Id+json oder Mikrodaten enthalten. Die meisten großen Rezeptwebseiten unterstützen diese Datenstruktur. Wenn das Rezept nicht importiert werden kann, aber JSON-Daten im Log vorhanden sind, melde es bitte mit der URL und diesen Daten auf GitHub.",
|
||||
"error-title": "Anscheinend konnten wir nichts finden",
|
||||
"from-url": "Von URL",
|
||||
"github-issues": "GitHub Issues",
|
||||
"github-issues": "GitHub Fehlermeldungen",
|
||||
"google-ld-json-info": "Google ld+json Info",
|
||||
"must-be-a-valid-url": "Muss eine gültige URL sein",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Füge deine Rezeptdaten ein. Jede Zeile wird als Eintrag in einer Liste dargestellt",
|
||||
@@ -216,9 +216,9 @@
|
||||
"upload-individual-zip-file": "Lade eine individuelle .zip-Datei hoch, die von einer anderen Mealie-Instanz exportiert wird.",
|
||||
"url-form-hint": "Kopiere einen Link von deiner Lieblingsrezept-Website und füge ihn ein",
|
||||
"view-scraped-data": "Gesammelte Daten anzeigen",
|
||||
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
|
||||
"trim-prefix-description": "Trim first character from each line",
|
||||
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
|
||||
"trim-whitespace-description": "Leerzeichen am Anfang und Ende sowie leere Zeilen entfernen",
|
||||
"trim-prefix-description": "Erste Zeichen aus jeder Zeile entfernen",
|
||||
"split-by-numbered-line-description": "Absätze nach dem Schema '1)' oder '1.' aufzuteilen versuchen"
|
||||
},
|
||||
"page": {
|
||||
"404-page-not-found": "404 Seite nicht gefunden",
|
||||
@@ -291,7 +291,7 @@
|
||||
"title": "Titel",
|
||||
"total-time": "Gesamtzeit",
|
||||
"unable-to-delete-recipe": "Rezept kann nicht gelöscht werden",
|
||||
"no-recipe": "No Recipe"
|
||||
"no-recipe": "Kein Rezept"
|
||||
},
|
||||
"search": {
|
||||
"advanced-search": "Erweiterte Suche",
|
||||
@@ -396,7 +396,7 @@
|
||||
"webhooks": {
|
||||
"test-webhooks": "Teste Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Die unten stehenden URL's erhalten Webhooks welche die Rezeptdaten für den Menüplan am geplanten Tag enthalten. Derzeit werden die Webhooks ausgeführt um",
|
||||
"webhook-url": "Webhook URL",
|
||||
"webhook-url": "Webhook-URL",
|
||||
"webhooks-caps": "WEBHOOKS",
|
||||
"webhooks": "Webhooks"
|
||||
}
|
||||
@@ -413,9 +413,9 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Alle Rezepte",
|
||||
"backups": "Backups",
|
||||
"backups": "Sicherungen",
|
||||
"categories": "Kategorien",
|
||||
"cookbooks": "Cookbooks",
|
||||
"cookbooks": "Kochbücher",
|
||||
"dashboard": "Übersicht",
|
||||
"home-page": "Startseite",
|
||||
"manage-users": "Benutzer",
|
||||
@@ -425,7 +425,7 @@
|
||||
"site-settings": "Einstellungen",
|
||||
"tags": "Schlagworte",
|
||||
"toolbox": "Werkzeuge",
|
||||
"language": "Language"
|
||||
"language": "Sprache"
|
||||
},
|
||||
"signup": {
|
||||
"error-signing-up": "Fehler beim Registrieren",
|
||||
@@ -448,7 +448,7 @@
|
||||
"untagged-count": "{count} ohne Schlagworte"
|
||||
},
|
||||
"tool": {
|
||||
"tools": "Tools"
|
||||
"tools": "Werkzeuge"
|
||||
},
|
||||
"user": {
|
||||
"admin": "Admin",
|
||||
@@ -467,7 +467,7 @@
|
||||
"error-cannot-delete-super-user": "Fehler! Super Benutzer kann nicht gelöscht werden",
|
||||
"existing-password-does-not-match": "Bestehendes Passwort stimmt nicht überein",
|
||||
"full-name": "Vollständiger Name",
|
||||
"invite-only": "Invite Only",
|
||||
"invite-only": "Nur auf Einladung",
|
||||
"link-id": "Linkkennung",
|
||||
"link-name": "Linkname",
|
||||
"login": "Anmeldung",
|
||||
@@ -480,8 +480,8 @@
|
||||
"password-reset-failed": "Zurücksetzen des Passworts fehlgeschlagen",
|
||||
"password-updated": "Passwort aktualisiert",
|
||||
"password": "Passwort",
|
||||
"password-strength": "Password is {strength}",
|
||||
"register": "Register",
|
||||
"password-strength": "Das Passwort ist {strength}",
|
||||
"register": "Registrieren",
|
||||
"reset-password": "Passwort zurücksetzen",
|
||||
"sign-in": "Einloggen",
|
||||
"total-mealplans": "Alle Essenspläne",
|
||||
@@ -505,45 +505,45 @@
|
||||
"webhooks-enabled": "Webhooks aktiviert",
|
||||
"you-are-not-allowed-to-create-a-user": "Sie sind nicht berechtigt, einen Benutzer anzulegen",
|
||||
"you-are-not-allowed-to-delete-this-user": "Sie sind nicht berechtigt, diesen Benutzer zu entfernen",
|
||||
"enable-advanced-content": "Enable Advanced Content",
|
||||
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
|
||||
"enable-advanced-content": "Erweiterten Inhalt aktivieren",
|
||||
"enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden."
|
||||
},
|
||||
"language-dialog": {
|
||||
"translated": "translated",
|
||||
"choose-language": "Choose Language",
|
||||
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
|
||||
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
|
||||
"read-the-docs": "Read the docs"
|
||||
"translated": "übersetzt",
|
||||
"choose-language": "Sprache wählen",
|
||||
"select-description": "Wählen Sie die Sprache für die Mealie-Benutzeroberfläche. Die Einstellung gilt nur für Sie, nicht für andere Benutzer.",
|
||||
"how-to-contribute-description": "Ist etwas noch nicht übersetzt, falsch oder deine Sprache fehlt in der Liste? {read-the-docs-link} wie man beiträgt!",
|
||||
"read-the-docs": "Dokumentation lesen"
|
||||
},
|
||||
"data-pages": {
|
||||
"seed-data": "Seed Data",
|
||||
"seed-data": "Musterdaten",
|
||||
"foods": {
|
||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
||||
"merge-food-example": "Merging {food1} into {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
|
||||
"merge-dialog-text": "Zusammenführen der ausgewählten Lebensmittel führt diese zusammen in ein einzelnes Lebensmittel. Die Ausgangslebensmittel werden gelöscht und alle Verweise werden auf das zusammengeführte Lebensmittel angepasst.",
|
||||
"merge-food-example": "{food1} wird zu {food2} zusammengeführt",
|
||||
"seed-dialog-text": "Füllt die Datenbank mit Lebensmitteln basierend auf Ihrer Landessprache. Dadurch werden mehr als 200 gängige Lebensmittel eingetragen, die verwendet werden können, um die Datenbank zu organisieren. Die Speisen werden über einen Gemeinschaftsanstrengung übersetzt.",
|
||||
"seed-dialog-warning": "Sie haben bereits einige Elemente in Ihrer Datenbank. Diese Aktion wird Duplikate nicht ausgleichen, Sie müssen sie manuell verwalten."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Seed the database with common units based on your local language."
|
||||
"seed-dialog-text": "Füllt die Datenbank mit gängigen Maßeinheiten basierend auf Ihrer Sprache."
|
||||
},
|
||||
"labels": {
|
||||
"seed-dialog-text": "Seed the database with common labels based on your local language."
|
||||
"seed-dialog-text": "Füllt die Datenbank mit gängigen Etiketten basierend auf Ihrer Sprache."
|
||||
}
|
||||
},
|
||||
"user-registration": {
|
||||
"user-registration": "User Registration",
|
||||
"join-a-group": "Join a Group",
|
||||
"create-a-new-group": "Create a New Group",
|
||||
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
|
||||
"group-details": "Group Details",
|
||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||
"use-seed-data": "Use Seed Data",
|
||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
||||
"account-details": "Account Details"
|
||||
"user-registration": "Benutzerregistrierung",
|
||||
"join-a-group": "Gruppe beitreten",
|
||||
"create-a-new-group": "Neue Gruppe erstellen",
|
||||
"provide-registration-token-description": "Bitte gib den Registrierungstoken für die Gruppe ein, der du beitreten möchtest. Du kannst ihn von einem bestehenden Gruppenmitglied erhalten.",
|
||||
"group-details": "Gruppendetails",
|
||||
"group-details-description": "Bevor Sie ein Konto erstellen, müssen Sie eine Gruppe erstellen. Ihre Gruppe wird nur Sie enthalten, aber Sie können andere später einladen. Mitglieder in Ihrer Gruppe können Essenspläne, Einkaufslisten, Rezepte und vieles mehr teilen!",
|
||||
"use-seed-data": "Musterdaten",
|
||||
"use-seed-data-description": "Mealie enthält eine Sammlung von Lebensmitteln, Maßeinheiten und Etiketten, die verwendet werden können, um deine Gruppe mit hilfreichen Daten für die Organisation deiner Rezepte zu füllen.",
|
||||
"account-details": "Kontoinformationen"
|
||||
},
|
||||
"validation": {
|
||||
"group-name-is-taken": "Group name is taken",
|
||||
"username-is-taken": "Username is taken",
|
||||
"email-is-taken": "Email is taken"
|
||||
"group-name-is-taken": "Gruppenname ist schon vergeben",
|
||||
"username-is-taken": "Benutzername ist schon vergeben",
|
||||
"email-is-taken": "E-Mail-Adresse ist schon vergeben"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,8 +509,8 @@
|
||||
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
|
||||
},
|
||||
"language-dialog": {
|
||||
"translated": "translated",
|
||||
"choose-language": "Choose Language",
|
||||
"translated": "traduit",
|
||||
"choose-language": "Choisir la langue",
|
||||
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
|
||||
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
|
||||
"read-the-docs": "Read the docs"
|
||||
@@ -532,18 +532,18 @@
|
||||
},
|
||||
"user-registration": {
|
||||
"user-registration": "User Registration",
|
||||
"join-a-group": "Join a Group",
|
||||
"create-a-new-group": "Create a New Group",
|
||||
"join-a-group": "Rejoindre un groupe",
|
||||
"create-a-new-group": "Créer un nouveau groupe",
|
||||
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
|
||||
"group-details": "Group Details",
|
||||
"group-details": "Détails du groupe",
|
||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||
"use-seed-data": "Use Seed Data",
|
||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
||||
"account-details": "Account Details"
|
||||
"account-details": "Détails du compte"
|
||||
},
|
||||
"validation": {
|
||||
"group-name-is-taken": "Group name is taken",
|
||||
"username-is-taken": "Username is taken",
|
||||
"username-is-taken": "Nom d'utilisateur déjà utilisé",
|
||||
"email-is-taken": "Email is taken"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,7 +521,7 @@
|
||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
||||
"merge-food-example": "Unione di {food1} in {food2}",
|
||||
"seed-dialog-text": "Inizializza il database con alimenti in base alla tua lingua locale. Questo creerà oltre 200 alimenti comuni che possono essere utilizzati per organizzare il tuo database. Gli alimenti sono tradotti grazie al contributo della comunità di utenti.",
|
||||
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
|
||||
"seed-dialog-warning": "Hai già alcuni elementi nel tuo database. Questa azione non riconcilierà i duplicati, dovrai gestirli manualmente."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Seed the database with common units based on your local language."
|
||||
@@ -534,11 +534,11 @@
|
||||
"user-registration": "Registrazione Utente",
|
||||
"join-a-group": "Unisciti a un Gruppo",
|
||||
"create-a-new-group": "Crea un Nuovo Gruppo",
|
||||
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
|
||||
"provide-registration-token-description": "Fornisci il token di registrazione associato al gruppo a cui desideri partecipare. Dovrai ottenerlo da un membro di gruppo esistente.",
|
||||
"group-details": "Dettagli del Guppo",
|
||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||
"use-seed-data": "Use Seed Data",
|
||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
||||
"use-seed-data-description": "Mealie viene fornito con una raccolta di alimenti, unità ed etichette che possono essere utilizzate per popolare il tuo gruppo con dati utili per organizzare le tue ricette.",
|
||||
"account-details": "Dettagli dell'Account"
|
||||
},
|
||||
"validation": {
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
"read-the-docs": "Документація"
|
||||
},
|
||||
"data-pages": {
|
||||
"seed-data": "Seed Data",
|
||||
"seed-data": "Дані Seed",
|
||||
"foods": {
|
||||
"merge-dialog-text": "Об'єднання вибраних страв об'єднає вихідну страву та цільову страву до однієї страви. Вихідна страва буде видалена і всі посилання на вихідну страву будуть посилатися на цільову страву.",
|
||||
"merge-food-example": "Об'єднання {food1} в {food2}",
|
||||
|
||||
@@ -25,11 +25,17 @@
|
||||
<v-divider v-if="item.divider" :key="index" class="mx-2"></v-divider>
|
||||
<v-list-item v-else :key="item.title" :to="item.to" exact>
|
||||
<v-list-item-avatar>
|
||||
<v-icon v-text="item.icon"></v-icon>
|
||||
<v-icon>
|
||||
{{ item.icon }}
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.title"></v-list-item-title>
|
||||
<v-list-item-subtitle v-text="item.subtitle"></v-list-item-subtitle>
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.subtitle }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -116,7 +122,7 @@ export default defineComponent({
|
||||
icon: this.$globals.icons.link,
|
||||
title: "Import",
|
||||
subtitle: "Import a recipe by URL",
|
||||
to: "/recipe/create?tab=url",
|
||||
to: "/recipe/create/url",
|
||||
restricted: true,
|
||||
},
|
||||
{ divider: true },
|
||||
@@ -124,7 +130,7 @@ export default defineComponent({
|
||||
icon: this.$globals.icons.edit,
|
||||
title: "Create",
|
||||
subtitle: "Create a recipe manually",
|
||||
to: "/recipe/create?tab=new",
|
||||
to: "/recipe/create/new",
|
||||
restricted: true,
|
||||
},
|
||||
{ divider: true },
|
||||
|
||||
@@ -21,39 +21,36 @@
|
||||
"@nuxtjs/i18n": "^7.0.3",
|
||||
"@nuxtjs/proxy": "^2.1.0",
|
||||
"@nuxtjs/pwa": "^3.3.5",
|
||||
"@vue/composition-api": "^1.0.5",
|
||||
"@vueuse/core": "^6.8.0",
|
||||
"@vue/composition-api": "^1.6.2",
|
||||
"@vueuse/core": "^8.5.0",
|
||||
"core-js": "^3.15.1",
|
||||
"date-fns": "^2.23.0",
|
||||
"isomorphic-dompurify": "^0.18.0",
|
||||
"fuse.js": "^6.5.3",
|
||||
"isomorphic-dompurify": "^0.19.0",
|
||||
"nuxt": "^2.15.8",
|
||||
"v-jsoneditor": "^1.4.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.5.5"
|
||||
"vuetify": "^2.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.14.7",
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
"@nuxt/types": "^2.15.7",
|
||||
"@nuxt/typescript-build": "^2.1.0",
|
||||
"@nuxtjs/composition-api": "^0.31.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^6.0.1",
|
||||
"@nuxtjs/composition-api": "^0.32.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^10.0.0",
|
||||
"@nuxtjs/eslint-module": "^3.0.2",
|
||||
"@nuxtjs/google-fonts": "^1.3.0",
|
||||
"@nuxtjs/vuetify": "^1.12.1",
|
||||
"@types/sortablejs": "^1.10.7",
|
||||
"@vue/runtime-dom": "^3.2.9",
|
||||
"eslint": "^7.29.0",
|
||||
"@types/sortablejs": "^1.13.0",
|
||||
"@vue/runtime-dom": "^3.2.36",
|
||||
"eslint": "^8.16.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-nuxt": "^2.0.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-vue": "^7.12.1",
|
||||
"lint-staged": "^10.5.4",
|
||||
"nuxt-vite": "^0.1.1",
|
||||
"eslint-plugin-nuxt": "^3.2.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.0.1",
|
||||
"lint-staged": "^12.4.2",
|
||||
"nuxt-vite": "0.2.3",
|
||||
"prettier": "^2.3.2",
|
||||
"vue2-script-setup-transform": "^0.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"vite": "2.3.8"
|
||||
"vue2-script-setup-transform": "^0.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,9 @@
|
||||
<v-card-text v-if="cookbooks">
|
||||
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
|
||||
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea>
|
||||
<RecipeOrganizerSelector
|
||||
v-model="cookbooks[index].categories"
|
||||
:items="allCategories || []"
|
||||
selector-type="category"
|
||||
/>
|
||||
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tags" :items="allTags || []" selector-type="tag" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tools" :items="tools || []" selector-type="tool" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].categories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tags" selector-type="tags" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tools" selector-type="tools" />
|
||||
<v-switch v-model="cookbooks[index].public" hide-details single-line>
|
||||
<template #label>
|
||||
Public Cookbook
|
||||
@@ -102,26 +97,15 @@ import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import draggable from "vuedraggable";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useCategories, useTags, useTools } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable, RecipeOrganizerSelector },
|
||||
setup() {
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
const { tools } = useTools();
|
||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||
|
||||
getAllCategories();
|
||||
getAllTags();
|
||||
|
||||
return {
|
||||
allCategories,
|
||||
allTags,
|
||||
cookbooks,
|
||||
actions,
|
||||
tools,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<v-alert v-if="foods.length > 0" type="error" class="mb-0 text-body-2">
|
||||
<v-alert v-if="foods && foods.length > 0" type="error" class="mb-0 text-body-2">
|
||||
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -96,7 +96,7 @@
|
||||
<CrudTable
|
||||
:table-config="tableConfig"
|
||||
:headers.sync="tableHeaders"
|
||||
:data="foods"
|
||||
:data="foods || []"
|
||||
:bulk-actions="[]"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@@ -130,8 +130,8 @@ import { validators } from "~/composables/use-validators";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientFood } from "~/types/api-types/recipe";
|
||||
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
||||
import { MultiPurposeLabelSummary } from "~/types/api-types/labels";
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { useFoodStore, useLabelStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: { MultiPurposeLabel },
|
||||
@@ -163,32 +163,32 @@ export default defineComponent({
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
const foods = ref<IngredientFood[]>([]);
|
||||
async function refreshFoods() {
|
||||
const { data } = await userApi.foods.getAll();
|
||||
foods.value = data ?? [];
|
||||
}
|
||||
onMounted(() => {
|
||||
refreshFoods();
|
||||
});
|
||||
|
||||
const foodStore = useFoodStore();
|
||||
|
||||
// ===============================================================
|
||||
// Food Editor
|
||||
|
||||
const editDialog = ref(false);
|
||||
const editTarget = ref<IngredientFood | null>(null);
|
||||
|
||||
function editEventHandler(item: IngredientFood) {
|
||||
editTarget.value = item;
|
||||
editDialog.value = true;
|
||||
}
|
||||
|
||||
async function editSaveFood() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.foods.updateOne(editTarget.value.id, editTarget.value);
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
}
|
||||
|
||||
await foodStore.actions.updateOne(editTarget.value);
|
||||
editDialog.value = false;
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Food Delete
|
||||
|
||||
const deleteDialog = ref(false);
|
||||
const deleteTarget = ref<IngredientFood | null>(null);
|
||||
function deleteEventHandler(item: IngredientFood) {
|
||||
@@ -200,10 +200,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.foods.deleteOne(deleteTarget.value.id);
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
}
|
||||
await foodStore.actions.deleteOne(deleteTarget.value.id);
|
||||
deleteDialog.value = false;
|
||||
}
|
||||
|
||||
@@ -226,19 +223,14 @@ export default defineComponent({
|
||||
const { data } = await userApi.foods.merge(fromFood.value.id, toFood.value.id);
|
||||
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
foodStore.actions.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Labels
|
||||
|
||||
const allLabels = ref([] as MultiPurposeLabelSummary[]);
|
||||
|
||||
async function refreshLabels() {
|
||||
const { data } = await userApi.multiPurposeLabels.getAll();
|
||||
allLabels.value = data ?? [];
|
||||
}
|
||||
const { labels: allLabels } = useLabelStore();
|
||||
|
||||
// ============================================================
|
||||
// Seed
|
||||
@@ -260,15 +252,14 @@ export default defineComponent({
|
||||
const { data } = await userApi.seeders.foods({ locale: locale.value });
|
||||
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
foodStore.actions.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
refreshLabels();
|
||||
return {
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
foods,
|
||||
foods: foodStore.foods,
|
||||
allLabels,
|
||||
validators,
|
||||
// Edit
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -73,7 +73,7 @@
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<v-alert v-if="labels.length > 0" type="error" class="mb-0 text-body-2">
|
||||
<v-alert v-if="labels && labels.length > 0" type="error" class="mb-0 text-body-2">
|
||||
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -84,7 +84,7 @@
|
||||
<CrudTable
|
||||
:table-config="tableConfig"
|
||||
:headers.sync="tableHeaders"
|
||||
:data="labels"
|
||||
:data="labels || []"
|
||||
:bulk-actions="[]"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@@ -118,6 +118,7 @@ import { useUserApi } from "~/composables/api";
|
||||
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
||||
import { MultiPurposeLabelSummary } from "~/types/api-types/labels";
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { useLabelData, useLabelStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: { MultiPurposeLabel },
|
||||
@@ -149,31 +150,14 @@ export default defineComponent({
|
||||
// ============================================================
|
||||
// Labels
|
||||
|
||||
const labels = ref([] as MultiPurposeLabelSummary[]);
|
||||
|
||||
async function refreshLabels() {
|
||||
const { data } = await userApi.multiPurposeLabels.getAll();
|
||||
labels.value = data ?? [];
|
||||
}
|
||||
const labelData = useLabelData();
|
||||
const labelStore = useLabelStore();
|
||||
|
||||
// Create
|
||||
|
||||
const createLabelData = ref({
|
||||
groupId: "",
|
||||
id: "",
|
||||
name: "",
|
||||
color: "",
|
||||
});
|
||||
|
||||
async function createLabel() {
|
||||
await userApi.multiPurposeLabels.createOne(createLabelData.value);
|
||||
createLabelData.value = {
|
||||
groupId: "",
|
||||
id: "",
|
||||
name: "",
|
||||
color: "",
|
||||
};
|
||||
refreshLabels();
|
||||
await labelStore.actions.createOne(labelData.data);
|
||||
labelData.reset();
|
||||
state.createDialog = false;
|
||||
}
|
||||
|
||||
@@ -190,10 +174,7 @@ export default defineComponent({
|
||||
if (!deleteTarget.value) {
|
||||
return;
|
||||
}
|
||||
const { data } = await userApi.multiPurposeLabels.deleteOne(deleteTarget.value.id);
|
||||
if (data) {
|
||||
refreshLabels();
|
||||
}
|
||||
await labelStore.actions.deleteOne(deleteTarget.value.id);
|
||||
state.deleteDialog = false;
|
||||
}
|
||||
|
||||
@@ -214,15 +195,10 @@ export default defineComponent({
|
||||
if (!editLabel.value) {
|
||||
return;
|
||||
}
|
||||
const { data } = await userApi.multiPurposeLabels.updateOne(editLabel.value.id, editLabel.value);
|
||||
if (data) {
|
||||
refreshLabels();
|
||||
}
|
||||
await labelStore.actions.updateOne(editLabel.value);
|
||||
state.editDialog = false;
|
||||
}
|
||||
|
||||
refreshLabels();
|
||||
|
||||
// ============================================================
|
||||
// Seed
|
||||
|
||||
@@ -243,7 +219,7 @@ export default defineComponent({
|
||||
const { data } = await userApi.seeders.labels({ locale: locale.value });
|
||||
|
||||
if (data) {
|
||||
refreshLabels();
|
||||
labelStore.actions.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +227,7 @@ export default defineComponent({
|
||||
state,
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
labels,
|
||||
labels: labelStore.labels,
|
||||
validators,
|
||||
|
||||
deleteEventHandler,
|
||||
@@ -260,7 +236,7 @@ export default defineComponent({
|
||||
editEventHandler,
|
||||
editSaveLabel,
|
||||
createLabel,
|
||||
createLabelData,
|
||||
createLabelData: labelData.data,
|
||||
|
||||
// Seed
|
||||
seedDatabase,
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
@submit="dialog.callback"
|
||||
>
|
||||
<v-card-text v-if="dialog.mode == MODES.tag">
|
||||
<RecipeCategoryTagSelector v-model="toSetTags" :tag-selector="true" />
|
||||
<RecipeOrganizerSelector v-model="toSetTags" selector-type="tags" />
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="dialog.mode == MODES.category">
|
||||
<RecipeCategoryTagSelector v-model="toSetCategories" />
|
||||
<RecipeOrganizerSelector v-model="toSetCategories" selector-type="categories" />
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="dialog.mode == MODES.delete">
|
||||
<p class="h4">Are you sure you want to delete the following recipes? This action cannot be undone.</p>
|
||||
@@ -149,7 +149,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
||||
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
|
||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useRecipes, allRecipes } from "~/composables/recipes";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
@@ -165,7 +165,7 @@ const MODES = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeDataTable, RecipeCategoryTagSelector, GroupExportData },
|
||||
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData },
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
||||
@@ -315,8 +315,10 @@ export default defineComponent({
|
||||
title: "Tag Recipes",
|
||||
mode: MODES.tag,
|
||||
tag: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
callback: () => {},
|
||||
callback: () => {
|
||||
// Stub function to be overwritten
|
||||
return Promise.resolve();
|
||||
},
|
||||
icon: $globals.icons.tags,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,15 @@
|
||||
Combining the selected units will merge the Source Unit and Target Unit into a single unit. The
|
||||
<strong> Source Unit will be deleted </strong> and all of the references to the Source Unit will be updated to
|
||||
point to the Target Unit.
|
||||
<v-autocomplete v-model="fromUnit" return-object :items="units" item-text="name" label="Source Unit" />
|
||||
<v-autocomplete v-model="toUnit" return-object :items="units" item-text="name" label="Target Unit" />
|
||||
|
||||
<v-autocomplete v-model="fromUnit" return-object :items="units" item-text="id" label="Source Unit">
|
||||
<template #selection="{ item }"> {{ item.name }}</template>
|
||||
<template #item="{ item }"> {{ item.name }} </template>
|
||||
</v-autocomplete>
|
||||
<v-autocomplete v-model="toUnit" return-object :items="units" item-text="id" label="Target Unit">
|
||||
<template #selection="{ item }"> {{ item.name }}</template>
|
||||
<template #item="{ item }"> {{ item.name }} </template>
|
||||
</v-autocomplete>
|
||||
|
||||
<template v-if="canMerge && fromUnit && toUnit">
|
||||
<div class="text-center">Merging {{ fromUnit.name }} into {{ toUnit.name }}</div>
|
||||
@@ -29,6 +36,7 @@
|
||||
<v-text-field v-model="editTarget.abbreviation" label="Abbreviation"></v-text-field>
|
||||
<v-text-field v-model="editTarget.description" label="Description"></v-text-field>
|
||||
<v-checkbox v-model="editTarget.fraction" hide-details label="Display as Fraction"></v-checkbox>
|
||||
<v-checkbox v-model="editTarget.useAbbreviation" hide-details label="Use Abbreviation"></v-checkbox>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
@@ -69,7 +77,7 @@
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -77,7 +85,7 @@
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<v-alert v-if="units.length > 0" type="error" class="mb-0 text-body-2">
|
||||
<v-alert v-if="units && units.length > 0" type="error" class="mb-0 text-body-2">
|
||||
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -88,7 +96,7 @@
|
||||
<CrudTable
|
||||
:table-config="tableConfig"
|
||||
:headers.sync="tableHeaders"
|
||||
:data="units"
|
||||
:data="units || []"
|
||||
:bulk-actions="[]"
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
@@ -99,6 +107,11 @@
|
||||
Combine
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #item.useAbbreviation="{ item }">
|
||||
<v-icon :color="item.useAbbreviation ? 'success' : undefined">
|
||||
{{ item.useAbbreviation ? $globals.icons.check : $globals.icons.close }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #item.fraction="{ item }">
|
||||
<v-icon :color="item.fraction ? 'success' : undefined">
|
||||
{{ item.fraction ? $globals.icons.check : $globals.icons.close }}
|
||||
@@ -120,8 +133,8 @@ import type { LocaleObject } from "@nuxtjs/i18n";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientUnit } from "~/types/api-types/recipe";
|
||||
import { MultiPurposeLabelSummary } from "~/types/api-types/labels";
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { useUnitStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -146,10 +159,15 @@ export default defineComponent({
|
||||
value: "abbreviation",
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
text: "Use Abbv.",
|
||||
value: "useAbbreviation",
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
text: "Description",
|
||||
value: "description",
|
||||
show: true,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
text: "Fraction",
|
||||
@@ -157,47 +175,39 @@ export default defineComponent({
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
const units = ref<IngredientUnit[]>([]);
|
||||
async function refreshUnits() {
|
||||
const { data } = await userApi.units.getAll();
|
||||
units.value = data ?? [];
|
||||
}
|
||||
onMounted(() => {
|
||||
refreshUnits();
|
||||
});
|
||||
|
||||
const { units, actions: unitActions } = useUnitStore();
|
||||
|
||||
// Edit Units
|
||||
const editDialog = ref(false);
|
||||
const editTarget = ref<IngredientUnit | null>(null);
|
||||
function editEventHandler(item: IngredientUnit) {
|
||||
editTarget.value = item;
|
||||
editDialog.value = true;
|
||||
}
|
||||
|
||||
async function editSaveUnit() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.units.updateOne(editTarget.value.id, editTarget.value);
|
||||
if (data) {
|
||||
refreshUnits();
|
||||
}
|
||||
|
||||
await unitActions.updateOne(editTarget.value);
|
||||
editDialog.value = false;
|
||||
}
|
||||
|
||||
// Delete Units
|
||||
const deleteDialog = ref(false);
|
||||
const deleteTarget = ref<IngredientUnit | null>(null);
|
||||
function deleteEventHandler(item: IngredientUnit) {
|
||||
deleteTarget.value = item;
|
||||
deleteDialog.value = true;
|
||||
}
|
||||
|
||||
async function deleteUnit() {
|
||||
if (!deleteTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.units.deleteOne(deleteTarget.value.id);
|
||||
if (data) {
|
||||
refreshUnits();
|
||||
}
|
||||
await unitActions.deleteOne(deleteTarget.value.id);
|
||||
deleteDialog.value = false;
|
||||
}
|
||||
|
||||
@@ -220,22 +230,10 @@ export default defineComponent({
|
||||
const { data } = await userApi.units.merge(fromUnit.value.id, toUnit.value.id);
|
||||
|
||||
if (data) {
|
||||
refreshUnits();
|
||||
unitActions.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Labels
|
||||
|
||||
const allLabels = ref([] as MultiPurposeLabelSummary[]);
|
||||
|
||||
async function refreshLabels() {
|
||||
const { data } = await userApi.multiPurposeLabels.getAll();
|
||||
allLabels.value = data ?? [];
|
||||
}
|
||||
|
||||
refreshLabels();
|
||||
|
||||
// ============================================================
|
||||
// Seed
|
||||
|
||||
@@ -256,7 +254,7 @@ export default defineComponent({
|
||||
const { data } = await userApi.seeders.units({ locale: locale.value });
|
||||
|
||||
if (data) {
|
||||
refreshUnits();
|
||||
unitActions.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +262,6 @@ export default defineComponent({
|
||||
tableConfig,
|
||||
tableHeaders,
|
||||
units,
|
||||
allLabels,
|
||||
validators,
|
||||
// Edit
|
||||
editDialog,
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
<template v-if="edit">
|
||||
<draggable
|
||||
tag="div"
|
||||
handle=".handle"
|
||||
:value="plan.meals"
|
||||
group="meals"
|
||||
:data-index="index"
|
||||
@@ -102,7 +103,13 @@
|
||||
style="min-height: 150px"
|
||||
@end="onMoveCallback"
|
||||
>
|
||||
<v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1">
|
||||
<v-card
|
||||
v-for="mealplan in plan.meals"
|
||||
:key="mealplan.id"
|
||||
v-model="hover[mealplan.id]"
|
||||
class="my-1"
|
||||
:class="{ handle: $vuetify.breakpoint.smAndUp }"
|
||||
>
|
||||
<v-list-item :to="edit || !mealplan.recipe ? null : `/recipe/${mealplan.recipe.slug}`">
|
||||
<v-list-item-avatar :rounded="false">
|
||||
<RecipeCardImage
|
||||
@@ -126,7 +133,13 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<div class="py-2 px-2 d-flex">
|
||||
<div class="py-2 px-2 d-flex" style="align-items: center">
|
||||
<v-btn small icon :class="{ handle: !$vuetify.breakpoint.smAndUp }">
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent>
|
||||
@@ -146,8 +159,8 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" small icon @click="actions.deleteOne(mealplan.id)">
|
||||
|
||||
<v-btn class="ml-auto" small icon @click="actions.deleteOne(mealplan.id)">
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -291,10 +304,12 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function onMoveCallback(evt: SortableEvent) {
|
||||
const supportedEvents = ["drop", "touchend"];
|
||||
|
||||
// Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029
|
||||
const ogEvent: DragEvent = (evt as any).originalEvent;
|
||||
|
||||
if (ogEvent && ogEvent.type !== "drop") {
|
||||
if (ogEvent && ogEvent.type in supportedEvents) {
|
||||
// The drop was cancelled, unsure if anything needs to be done?
|
||||
console.log("Cancel Move Event");
|
||||
} else {
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<template #header>
|
||||
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/data-reports.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Recipe Data Migrations</template>
|
||||
Recipes can be migrated from another supported application to Mealie. This is a great way to get started with
|
||||
Mealie.
|
||||
<template #title> Report </template>
|
||||
</BasePageTitle>
|
||||
<v-container v-if="report">
|
||||
<BaseCardSectionTitle :title="report.name"> </BaseCardSectionTitle>
|
||||
@@ -31,8 +29,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useRoute, reactive, toRefs, onMounted } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, useRoute, ref, onMounted } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ReportOut } from "~/types/api-types/reports";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -41,16 +40,11 @@ export default defineComponent({
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
report: {},
|
||||
});
|
||||
const report = ref<ReportOut | null>(null);
|
||||
|
||||
async function getReport() {
|
||||
const { data } = await api.groupReports.getOne(id);
|
||||
|
||||
if (data) {
|
||||
state.report = data;
|
||||
}
|
||||
report.value = data ?? null;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -64,7 +58,7 @@ export default defineComponent({
|
||||
];
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
report,
|
||||
id,
|
||||
itemHeaders,
|
||||
};
|
||||
@@ -72,5 +66,4 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
hide-details
|
||||
class="pt-0 my-auto py-auto"
|
||||
color="secondary"
|
||||
@change="updateTool(recipe.tools[index])"
|
||||
@change="toolStore.actions.updateOne(recipe.tools[index])"
|
||||
>
|
||||
</v-checkbox>
|
||||
<v-list-item-content>
|
||||
@@ -256,12 +256,12 @@
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.recipeCategory"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
selector-type="categories"
|
||||
/>
|
||||
<RecipeChips v-else :items="recipe.recipeCategory" />
|
||||
</v-card-text>
|
||||
@@ -274,13 +274,12 @@
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.tags"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
selector-type="tags"
|
||||
/>
|
||||
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
||||
</v-card-text>
|
||||
@@ -291,7 +290,7 @@
|
||||
<v-card-title class="py-2"> Required Tools </v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text class="pt-0">
|
||||
<RecipeTools v-model="recipe.tools" :edit="form" />
|
||||
<RecipeOrganizerSelector v-model="recipe.tools" selector-type="tools" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -320,6 +319,9 @@
|
||||
:ingredients="recipe.recipeIngredient"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
:edit="form"
|
||||
:recipe-id="recipe.id"
|
||||
:recipe-slug="recipe.slug"
|
||||
:assets.sync="recipe.assets"
|
||||
/>
|
||||
<div v-if="form" class="d-flex">
|
||||
<RecipeDialogBulkAdd class="ml-auto my-2 mr-1" @bulk-data="addStep" />
|
||||
@@ -344,12 +346,12 @@
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.recipeCategory"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
selector-type="categories"
|
||||
/>
|
||||
<RecipeChips v-else :items="recipe.recipeCategory" />
|
||||
</v-card-text>
|
||||
@@ -362,14 +364,14 @@
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.tags"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
selector-type="tags"
|
||||
/>
|
||||
|
||||
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -454,12 +456,13 @@
|
||||
>
|
||||
<v-switch v-model="wakeLock" small label="Keep Screen Awake" />
|
||||
</div>
|
||||
|
||||
<RecipeComments
|
||||
v-if="recipe && !recipe.settings.disableComments && !form"
|
||||
v-model="recipe.comments"
|
||||
:slug="recipe.slug"
|
||||
:recipe-id="recipe.id"
|
||||
class="px-1 my-4"
|
||||
class="px-1 my-4 d-print-none"
|
||||
/>
|
||||
<RecipePrintView v-if="recipe" :recipe="recipe" />
|
||||
</v-container>
|
||||
@@ -483,7 +486,7 @@ import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import draggable from "vuedraggable";
|
||||
import { invoke, until, useWakeLock } from "@vueuse/core";
|
||||
import { onUnmounted } from "vue-demi";
|
||||
import RecipeCategoryTagSelector from "@/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import RecipeOrganizerSelector from "@/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue";
|
||||
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@@ -502,9 +505,10 @@ import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientE
|
||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||
import RecipeTools from "~/components/Domain/Recipe/RecipeTools.vue";
|
||||
import RecipeComments from "~/components/Domain/Recipe/RecipeComments.vue";
|
||||
import { Recipe, RecipeTool } from "~/types/api-types/recipe";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { uuid4, deepCopy } from "~/composables/use-utils";
|
||||
import { useRouteQuery } from "~/composables/use-router";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -515,7 +519,7 @@ export default defineComponent({
|
||||
return import(/* webpackChunkName: "RecipeAssets" */ "~/components/Domain/Recipe/RecipeAssets.vue");
|
||||
}
|
||||
},
|
||||
RecipeCategoryTagSelector,
|
||||
RecipeOrganizerSelector,
|
||||
RecipeChips,
|
||||
RecipeComments,
|
||||
RecipeDialogBulkAdd,
|
||||
@@ -757,18 +761,7 @@ export default defineComponent({
|
||||
// ===============================================================
|
||||
// Recipe Tools
|
||||
|
||||
async function updateTool(tool: RecipeTool) {
|
||||
if (tool.id === undefined) return;
|
||||
|
||||
const { response } = await api.tools.updateOne(tool.id, tool);
|
||||
|
||||
if (response?.status === 200) {
|
||||
console.log("Update Successful");
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Recipe API Extras
|
||||
const toolStore = useToolStore();
|
||||
|
||||
const apiNewKey = ref("");
|
||||
|
||||
@@ -863,13 +856,13 @@ export default defineComponent({
|
||||
deleteRecipe,
|
||||
printRecipe,
|
||||
closeEditor,
|
||||
updateTool,
|
||||
updateRecipe,
|
||||
uploadImage,
|
||||
validators,
|
||||
recipeImage,
|
||||
addIngredient,
|
||||
removeApiExtra,
|
||||
toolStore,
|
||||
};
|
||||
},
|
||||
head: {},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div>
|
||||
Mealie can use natural language processing to attempt to parse and create units, and foods for your Recipe
|
||||
ingredients. This is experimental and may not work as expected. If you choose to not use the parsed results
|
||||
you can seleect cancel and your changes will not be saved.
|
||||
you can select cancel and your changes will not be saved.
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
To use the ingredient parser, click the "Parse All" button and the process will start. When the processed
|
||||
ingredients are available, you can look through the items and verify that they were parsed correctly. The models
|
||||
confidence score is displayed on the right of the title item. This is an average of all scores and may not be
|
||||
wholey accurate.
|
||||
wholely accurate.
|
||||
|
||||
<div class="my-4">
|
||||
Alerts will be displayed if a matching foods or unit is found but does not exists in the database.
|
||||
@@ -84,11 +84,18 @@
|
||||
import { defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api";
|
||||
import { invoke, until } from "@vueuse/core";
|
||||
import { Parser } from "~/api/class-interfaces/recipes/recipe";
|
||||
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, ParsedIngredient } from "~/types/api-types/recipe";
|
||||
import {
|
||||
CreateIngredientFood,
|
||||
CreateIngredientUnit,
|
||||
IngredientFood,
|
||||
IngredientUnit,
|
||||
ParsedIngredient,
|
||||
} from "~/types/api-types/recipe";
|
||||
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useFoods, useRecipe, useUnits } from "~/composables/recipes";
|
||||
import { useRecipe } from "~/composables/recipes";
|
||||
import { RecipeIngredient } from "~/types/api-types/admin";
|
||||
import { useFoodData, useFoodStore, useUnitStore } from "~/composables/store";
|
||||
|
||||
interface Error {
|
||||
ingredientIndex: number;
|
||||
@@ -182,8 +189,9 @@ export default defineComponent({
|
||||
// =========================================================
|
||||
// Food and Ingredient Logic
|
||||
|
||||
const { foods, workingFoodData, actions } = useFoods();
|
||||
const { units } = useUnits();
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
const { units } = useUnitStore();
|
||||
|
||||
const errors = ref<Error[]>([]);
|
||||
|
||||
@@ -201,16 +209,17 @@ export default defineComponent({
|
||||
if (!food) {
|
||||
return false;
|
||||
}
|
||||
if (foods.value && food?.name) {
|
||||
return foods.value.some((f) => f.name === food.name);
|
||||
if (foodStore.foods.value && food?.name) {
|
||||
return foodStore.foods.value.some((f) => f.name === food.name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function createFood(food: CreateIngredientFood, index: number) {
|
||||
workingFoodData.name = food.name;
|
||||
await actions.createOne();
|
||||
foodData.data.name = food.name;
|
||||
await foodStore.actions.createOne(foodData.data);
|
||||
errors.value[index].foodError = false;
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
@@ -219,16 +228,16 @@ export default defineComponent({
|
||||
let ingredients = parsedIng.value.map((ing) => {
|
||||
return {
|
||||
...ing.ingredient,
|
||||
originalText: ing.input
|
||||
originalText: ing.input,
|
||||
} as RecipeIngredient;
|
||||
});
|
||||
|
||||
ingredients = ingredients.map((ing) => {
|
||||
if (!foods.value || !units.value) {
|
||||
if (!foodStore.foods.value || !units.value) {
|
||||
return ing;
|
||||
}
|
||||
// Get food from foods
|
||||
ing.food = foods.value.find((f) => f.name === ing.food?.name);
|
||||
ing.food = foodStore.foods.value.find((f) => f.name === ing.food?.name);
|
||||
|
||||
// Get unit from units
|
||||
ing.unit = units.value.find((u) => u.name === ing.unit?.name);
|
||||
@@ -252,8 +261,8 @@ export default defineComponent({
|
||||
saveAll,
|
||||
createFood,
|
||||
errors,
|
||||
actions,
|
||||
workingFoodData,
|
||||
actions: foodStore.actions,
|
||||
workingFoodData: foodData,
|
||||
isError,
|
||||
panels,
|
||||
asPercentage,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-container class="narrow-container flex-column pa-0">
|
||||
<v-container class="flex-column">
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="175" max-width="175" :src="require('~/static/svgs/recipes-create.svg')"></v-img>
|
||||
@@ -9,311 +9,17 @@
|
||||
Select one of the various ways to create a recipe
|
||||
<template #content>
|
||||
<div class="ml-auto">
|
||||
<BaseOverflowButton v-model="tab" rounded :items="tabs"> </BaseOverflowButton>
|
||||
<BaseOverflowButton v-model="subpage" rounded :items="subpages"> </BaseOverflowButton>
|
||||
</div>
|
||||
</template>
|
||||
</BasePageTitle>
|
||||
|
||||
<section>
|
||||
<v-tabs-items v-model="tab" class="mt-2">
|
||||
<!-- Create From URL -->
|
||||
<v-tab-item value="url" eager>
|
||||
<v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags)">
|
||||
<v-card flat>
|
||||
<v-card-title class="headline"> Scrape Recipe </v-card-title>
|
||||
<v-card-text>
|
||||
Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to
|
||||
scrape the recipe from that site and add it to your collection.
|
||||
<v-text-field
|
||||
v-model="recipeUrl"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
clearable
|
||||
class="rounded-lg mt-2"
|
||||
rounded
|
||||
:rules="[validators.url]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
<v-checkbox v-model="importKeywordsAsTags" label="Import original keywords as tags">
|
||||
</v-checkbox>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" />
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
<v-expand-transition>
|
||||
<v-alert v-show="error" color="error" class="mt-6 white--text">
|
||||
<v-card-title class="ma-0 pa-0">
|
||||
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
||||
{{ $t("new-recipe.error-title") }}
|
||||
</v-card-title>
|
||||
<v-divider class="my-3 mx-2"></v-divider>
|
||||
|
||||
<p>
|
||||
{{ $t("new-recipe.error-details") }}
|
||||
</p>
|
||||
<div class="d-flex row justify-space-around my-3 force-white">
|
||||
<a
|
||||
class="dark"
|
||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
{{ $t("new-recipe.google-ld-json-info") }}
|
||||
</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.github-issues") }}
|
||||
</a>
|
||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.recipe-markup-specification") }}
|
||||
</a>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Create By Name -->
|
||||
<v-tab-item value="new" eager>
|
||||
<v-card flat>
|
||||
<v-card-title class="headline"> Create Recipe </v-card-title>
|
||||
<v-card-text>
|
||||
Create a recipe by providing the name. All recipes must have unique names.
|
||||
<v-form ref="domCreateByName">
|
||||
<v-text-field
|
||||
v-model="newRecipeName"
|
||||
:label="$t('recipe.recipe-name')"
|
||||
:prepend-inner-icon="$globals.icons.primary"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
clearable
|
||||
class="rounded-lg mt-2"
|
||||
rounded
|
||||
:rules="[validators.required]"
|
||||
hint="New recipe names must be unique"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="newRecipeName === ''"
|
||||
rounded
|
||||
block
|
||||
:loading="loading"
|
||||
@click="createByName(newRecipeName)"
|
||||
/>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Create By Zip -->
|
||||
<v-tab-item value="zip" eager>
|
||||
<v-form>
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Import from Zip </v-card-title>
|
||||
<v-card-text>
|
||||
Import a single recipe that was exported from another Mealie instance.
|
||||
<v-file-input
|
||||
v-model="newRecipeZip"
|
||||
accept=".zip"
|
||||
label=".zip"
|
||||
filled
|
||||
clearable
|
||||
class="rounded-lg mt-2"
|
||||
rounded
|
||||
truncate-length="100"
|
||||
hint=".zip files must have been exported from Mealie"
|
||||
persistent-hint
|
||||
prepend-icon=""
|
||||
:prepend-inner-icon="$globals.icons.zip"
|
||||
>
|
||||
</v-file-input>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="newRecipeZip === null"
|
||||
large
|
||||
rounded
|
||||
block
|
||||
:loading="loading"
|
||||
@click="createByZip"
|
||||
/>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Create By Zip -->
|
||||
<v-tab-item value="debug" eager>
|
||||
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
||||
<v-card flat>
|
||||
<v-card-title class="headline"> Recipe Debugger </v-card-title>
|
||||
<v-card-text>
|
||||
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe
|
||||
scraper and the results will be displayed. If you don't see any data returned, the site you are trying
|
||||
to scrape is not supported by Mealie or it's scraper library.
|
||||
<v-text-field
|
||||
v-model="recipeUrl"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
validate-on-blur
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
autofocus
|
||||
filled
|
||||
clearable
|
||||
rounded
|
||||
class="rounded-lg mt-2"
|
||||
:rules="[validators.url]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="recipeUrl === null"
|
||||
rounded
|
||||
block
|
||||
type="submit"
|
||||
color="info"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
Debug
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item value="bulk" eager>
|
||||
<v-card flat>
|
||||
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||
<v-card-text>
|
||||
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the
|
||||
backend and running the task in the background. This can be useful when initially migrating to Mealie,
|
||||
or when you want to import a large number of recipes.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</section>
|
||||
<v-divider class="mt-5"></v-divider>
|
||||
</v-container>
|
||||
|
||||
<v-container tag="section">
|
||||
<!-- Debug Extras -->
|
||||
<section v-if="debugData && tab === 'debug'">
|
||||
<v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox>
|
||||
<LazyRecipeJsonEditor
|
||||
v-model="debugData"
|
||||
class="primary"
|
||||
:options="{
|
||||
mode: debugTreeView ? 'tree' : 'code',
|
||||
search: false,
|
||||
indentation: 4,
|
||||
mainMenuBar: false,
|
||||
}"
|
||||
height="700px"
|
||||
/>
|
||||
</section>
|
||||
<!-- Debug Extras -->
|
||||
<section v-else-if="tab === 'bulk'" class="mt-2">
|
||||
<v-row v-for="(bulkUrl, idx) in bulkUrls" :key="'bulk-url' + idx" class="my-1" dense>
|
||||
<v-col cols="12" xs="12" sm="12" md="12">
|
||||
<v-text-field
|
||||
v-model="bulkUrls[idx].url"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
dense
|
||||
single-line
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
hide-details
|
||||
clearable
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn color="error" icon x-small @click="bulkUrls.splice(idx, 1)">
|
||||
<v-icon>
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].categories"
|
||||
:items="allCategories || []"
|
||||
selector-type="category"
|
||||
:input-attrs="{
|
||||
filled: true,
|
||||
singleLine: true,
|
||||
dense: true,
|
||||
rounded: true,
|
||||
class: 'rounded-lg',
|
||||
hideDetails: true,
|
||||
clearable: true,
|
||||
}"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].tags"
|
||||
:items="allTags || []"
|
||||
selector-type="tag"
|
||||
:input-attrs="{
|
||||
filled: true,
|
||||
singleLine: true,
|
||||
dense: true,
|
||||
rounded: true,
|
||||
class: 'rounded-lg',
|
||||
hideDetails: true,
|
||||
clearable: true,
|
||||
}"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions class="justify-end">
|
||||
<BaseButton
|
||||
delete
|
||||
@click="
|
||||
bulkUrls = [];
|
||||
lockBulkImport = false;
|
||||
"
|
||||
>
|
||||
Clear
|
||||
</BaseButton>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton color="info" @click="bulkUrls.push({ url: '', categories: [], tags: [] })">
|
||||
<template #icon> {{ $globals.icons.createAlt }} </template> New
|
||||
</BaseButton>
|
||||
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport" @click="bulkCreate">
|
||||
<template #icon> {{ $globals.icons.check }} </template> Submit
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
<NuxtChild />
|
||||
</section>
|
||||
</v-container>
|
||||
|
||||
<AdvancedOnly>
|
||||
<v-container class="narrow-container d-flex justify-end">
|
||||
<v-container class="d-flex justify-end">
|
||||
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
||||
</v-container>
|
||||
</AdvancedOnly>
|
||||
@@ -321,39 +27,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
toRefs,
|
||||
ref,
|
||||
useRouter,
|
||||
useContext,
|
||||
computed,
|
||||
useRoute,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { onMounted } from "vue-demi";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import { defineComponent, useRouter, useContext, computed, useRoute } from "@nuxtjs/composition-api";
|
||||
import { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
||||
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useCategories, useTags } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: { AdvancedOnly, RecipeOrganizerSelector },
|
||||
components: { AdvancedOnly },
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { $globals } = useContext();
|
||||
|
||||
const tabs: MenuItem[] = [
|
||||
const subpages: MenuItem[] = [
|
||||
{
|
||||
icon: $globals.icons.link,
|
||||
text: "Import with URL",
|
||||
@@ -381,185 +64,21 @@ export default defineComponent({
|
||||
},
|
||||
];
|
||||
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||
if (response?.status !== 201) {
|
||||
state.error = true;
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||
}
|
||||
|
||||
const tab = computed({
|
||||
set(tab: string) {
|
||||
router.replace({ query: { ...route.value.query, tab } });
|
||||
const subpage = computed({
|
||||
set(subpage: string) {
|
||||
router.push({ path: `/recipe/create/${subpage}`, query: route.value.query });
|
||||
},
|
||||
get() {
|
||||
return route.value.query.tab as string;
|
||||
return route.value.path.split("/").pop() ?? "url";
|
||||
},
|
||||
});
|
||||
|
||||
const recipeUrl = computed({
|
||||
set(recipe_import_url: string | null) {
|
||||
if (recipe_import_url !== null) {
|
||||
recipe_import_url = recipe_import_url.trim();
|
||||
router.replace({ query: { ...route.value.query, recipe_import_url } });
|
||||
}
|
||||
},
|
||||
get() {
|
||||
return route.value.query.recipe_import_url as string | null;
|
||||
},
|
||||
});
|
||||
|
||||
const importKeywordsAsTags = computed({
|
||||
get() {
|
||||
return route.value.query.import_keywords_as_tags === "1";
|
||||
},
|
||||
set(keywordsAsTags: boolean) {
|
||||
let import_keywords_as_tags = "0"
|
||||
if (keywordsAsTags) {
|
||||
import_keywords_as_tags = "1"
|
||||
}
|
||||
router.replace({query: {...route.value.query, import_keywords_as_tags}})
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!recipeUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipeUrl.value.includes("https")) {
|
||||
createByUrl(recipeUrl.value, importKeywordsAsTags.value);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ===================================================
|
||||
// Recipe Debug URL Scraper
|
||||
|
||||
const debugTreeView = ref(false);
|
||||
|
||||
const debugData = ref<Recipe | null>(null);
|
||||
|
||||
async function debugUrl(url: string | null) {
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
const { data } = await api.recipes.testCreateOneUrl(url);
|
||||
|
||||
state.loading = false;
|
||||
debugData.value = data;
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// Recipe URL Import
|
||||
const domUrlForm = ref<VForm | null>(null);
|
||||
|
||||
async function createByUrl(url: string, importKeywordsAsTags: boolean) {
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!domUrlForm.value?.validate() || url === "") {
|
||||
console.log("Invalid URL", url);
|
||||
return;
|
||||
}
|
||||
state.loading = true;
|
||||
const { response } = await api.recipes.createOneByUrl(url, importKeywordsAsTags);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// Recipe Create By Name
|
||||
const newRecipeName = ref("");
|
||||
const domCreateByName = ref<VForm | null>(null);
|
||||
|
||||
async function createByName(name: string) {
|
||||
if (!domCreateByName.value?.validate() || name === "") {
|
||||
return;
|
||||
}
|
||||
const { response } = await api.recipes.createOne({ name });
|
||||
// TODO createOne claims to return a Recipe, but actually the API only returns a string
|
||||
// @ts-ignore See above
|
||||
handleResponse(response, true);
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// Recipe Import From Zip File
|
||||
const newRecipeZip = ref<File | null>(null);
|
||||
const newRecipeZipFileName = "archive";
|
||||
|
||||
async function createByZip() {
|
||||
if (!newRecipeZip.value) {
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
||||
|
||||
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// Bulk Importer
|
||||
|
||||
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
|
||||
const lockBulkImport = ref(false);
|
||||
|
||||
async function bulkCreate() {
|
||||
if (bulkUrls.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
|
||||
|
||||
if (response?.status === 202) {
|
||||
alert.success("Bulk Import process has started");
|
||||
lockBulkImport.value = true;
|
||||
} else {
|
||||
alert.error("Bulk import process has failed");
|
||||
}
|
||||
}
|
||||
|
||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||
|
||||
getAllTags();
|
||||
getAllCategories();
|
||||
|
||||
return {
|
||||
allTags,
|
||||
allCategories,
|
||||
tab,
|
||||
recipeUrl,
|
||||
importKeywordsAsTags,
|
||||
bulkCreate,
|
||||
bulkUrls,
|
||||
lockBulkImport,
|
||||
debugTreeView,
|
||||
tabs,
|
||||
domCreateByName,
|
||||
domUrlForm,
|
||||
newRecipeName,
|
||||
newRecipeZip,
|
||||
debugUrl,
|
||||
debugData,
|
||||
createByName,
|
||||
createByUrl,
|
||||
createByZip,
|
||||
...toRefs(state),
|
||||
validators,
|
||||
subpages,
|
||||
subpage,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
@@ -569,9 +88,3 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.force-white > a {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
190
frontend/pages/recipe/create/bulk.vue
Normal file
190
frontend/pages/recipe/create/bulk.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||
<v-card-text>
|
||||
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the backend and
|
||||
running the task in the background. This can be useful when initially migrating to Mealie, or when you want to
|
||||
import a large number of recipes.
|
||||
</v-card-text>
|
||||
</div>
|
||||
<section class="mt-2">
|
||||
<v-row v-for="(_, idx) in bulkUrls" :key="'bulk-url' + idx" class="my-1" dense>
|
||||
<v-col cols="12" xs="12" sm="12" md="12">
|
||||
<v-text-field
|
||||
v-model="bulkUrls[idx].url"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
dense
|
||||
single-line
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
hide-details
|
||||
clearable
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn style="margin-top: -2px" icon small @click="bulkUrls.splice(idx, 1)">
|
||||
<v-icon>
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<template v-if="showCatTags">
|
||||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].categories"
|
||||
selector-type="categories"
|
||||
:input-attrs="{
|
||||
filled: true,
|
||||
singleLine: true,
|
||||
dense: true,
|
||||
rounded: true,
|
||||
class: 'rounded-lg',
|
||||
hideDetails: true,
|
||||
clearable: true,
|
||||
}"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].tags"
|
||||
selector-type="tags"
|
||||
:input-attrs="{
|
||||
filled: true,
|
||||
singleLine: true,
|
||||
dense: true,
|
||||
rounded: true,
|
||||
class: 'rounded-lg',
|
||||
hideDetails: true,
|
||||
clearable: true,
|
||||
}"
|
||||
/>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-card-actions class="justify-end">
|
||||
<BaseButton
|
||||
delete
|
||||
@click="
|
||||
bulkUrls = [];
|
||||
lockBulkImport = false;
|
||||
"
|
||||
>
|
||||
Clear
|
||||
</BaseButton>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton class="mr-1" color="info" @click="bulkUrls.push({ url: '', categories: [], tags: [] })">
|
||||
<template #icon> {{ $globals.icons.createAlt }} </template>
|
||||
New
|
||||
</BaseButton>
|
||||
<RecipeDialogBulkAdd v-model="bulkDialog" @bulk-data="assignUrls" />
|
||||
</v-card-actions>
|
||||
<div class="px-1">
|
||||
<v-checkbox v-model="showCatTags" hide-details label="Set Categories and Tags " />
|
||||
</div>
|
||||
<v-card-actions class="justify-end">
|
||||
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport" @click="bulkCreate">
|
||||
<template #icon> {{ $globals.icons.check }} </template>
|
||||
Submit
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
</section>
|
||||
<section class="mt-12">
|
||||
<BaseCardSectionTitle title="Bulk Imports"> </BaseCardSectionTitle>
|
||||
<ReportTable :items="reports" @delete="deleteReport" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref } from "@nuxtjs/composition-api";
|
||||
import { whenever } from "@vueuse/shared";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { ReportSummary } from "~/types/api-types/reports";
|
||||
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeOrganizerSelector, RecipeDialogBulkAdd },
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
showCatTags: false,
|
||||
bulkDialog: false,
|
||||
});
|
||||
|
||||
whenever(
|
||||
() => !state.showCatTags,
|
||||
() => {
|
||||
console.log("showCatTags changed");
|
||||
}
|
||||
);
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
|
||||
const lockBulkImport = ref(false);
|
||||
|
||||
async function bulkCreate() {
|
||||
if (bulkUrls.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
|
||||
|
||||
if (response?.status === 202) {
|
||||
alert.success("Bulk Import process has started");
|
||||
lockBulkImport.value = true;
|
||||
} else {
|
||||
alert.error("Bulk import process has failed");
|
||||
}
|
||||
|
||||
fetchReports();
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Reports
|
||||
|
||||
const reports = ref<ReportSummary[]>([]);
|
||||
|
||||
async function fetchReports() {
|
||||
const { data } = await api.groupReports.getAll("bulk_import");
|
||||
reports.value = data ?? [];
|
||||
}
|
||||
|
||||
async function deleteReport(id: string) {
|
||||
console.log(id);
|
||||
const { response } = await api.groupReports.deleteOne(id);
|
||||
|
||||
if (response?.status === 200) {
|
||||
fetchReports();
|
||||
} else {
|
||||
alert.error("Report deletion failed");
|
||||
}
|
||||
}
|
||||
|
||||
fetchReports();
|
||||
|
||||
function assignUrls(urls: string[]) {
|
||||
bulkUrls.value = urls.map((url) => ({ url, categories: [], tags: [] }));
|
||||
}
|
||||
|
||||
return {
|
||||
assignUrls,
|
||||
reports,
|
||||
deleteReport,
|
||||
bulkCreate,
|
||||
bulkUrls,
|
||||
lockBulkImport,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
110
frontend/pages/recipe/create/debug.vue
Normal file
110
frontend/pages/recipe/create/debug.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
||||
<div>
|
||||
<v-card-title class="headline"> Recipe Debugger </v-card-title>
|
||||
<v-card-text>
|
||||
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper
|
||||
and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is
|
||||
not supported by Mealie or it's scraper library.
|
||||
<v-text-field
|
||||
v-model="recipeUrl"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
validate-on-blur
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
autofocus
|
||||
filled
|
||||
clearable
|
||||
rounded
|
||||
class="rounded-lg mt-2"
|
||||
:rules="[validators.url]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" color="info" :loading="loading">
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
Debug
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-form>
|
||||
<section v-if="debugData">
|
||||
<v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox>
|
||||
<LazyRecipeJsonEditor
|
||||
v-model="debugData"
|
||||
class="primary"
|
||||
:options="{
|
||||
mode: debugTreeView ? 'tree' : 'code',
|
||||
search: false,
|
||||
indentation: 4,
|
||||
mainMenuBar: false,
|
||||
}"
|
||||
height="700px"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const recipeUrl = computed({
|
||||
set(recipe_import_url: string | null) {
|
||||
if (recipe_import_url !== null) {
|
||||
recipe_import_url = recipe_import_url.trim();
|
||||
router.replace({ query: { ...route.value.query, recipe_import_url } });
|
||||
}
|
||||
},
|
||||
get() {
|
||||
return route.value.query.recipe_import_url as string | null;
|
||||
},
|
||||
});
|
||||
|
||||
const debugTreeView = ref(false);
|
||||
|
||||
const debugData = ref<Recipe | null>(null);
|
||||
|
||||
async function debugUrl(url: string | null) {
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
const { data } = await api.recipes.testCreateOneUrl(url);
|
||||
|
||||
state.loading = false;
|
||||
debugData.value = data;
|
||||
}
|
||||
|
||||
return {
|
||||
recipeUrl,
|
||||
debugTreeView,
|
||||
debugUrl,
|
||||
debugData,
|
||||
...toRefs(state),
|
||||
validators,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
18
frontend/pages/recipe/create/index.vue
Normal file
18
frontend/pages/recipe/create/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, useRouter } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
onMounted(() => {
|
||||
// Force redirect to first valid page
|
||||
router.replace("/recipe/create/url");
|
||||
});
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
82
frontend/pages/recipe/create/new.vue
Normal file
82
frontend/pages/recipe/create/new.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-title class="headline"> Create Recipe </v-card-title>
|
||||
<v-card-text>
|
||||
Create a recipe by providing the name. All recipes must have unique names.
|
||||
<v-form ref="domCreateByName">
|
||||
<v-text-field
|
||||
v-model="newRecipeName"
|
||||
:label="$t('recipe.recipe-name')"
|
||||
:prepend-inner-icon="$globals.icons.primary"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
clearable
|
||||
class="rounded-lg mt-2"
|
||||
rounded
|
||||
:rules="[validators.required]"
|
||||
hint="New recipe names must be unique"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton
|
||||
:disabled="newRecipeName === ''"
|
||||
rounded
|
||||
block
|
||||
:loading="loading"
|
||||
@click="createByName(newRecipeName)"
|
||||
/>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
const api = useUserApi();
|
||||
const router = useRouter();
|
||||
|
||||
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||
if (response?.status !== 201) {
|
||||
state.error = true;
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||
}
|
||||
|
||||
const newRecipeName = ref("");
|
||||
const domCreateByName = ref<VForm | null>(null);
|
||||
|
||||
async function createByName(name: string) {
|
||||
if (!domCreateByName.value?.validate() || name === "") {
|
||||
return;
|
||||
}
|
||||
const { response } = await api.recipes.createOne({ name });
|
||||
// TODO createOne claims to return a Recipe, but actually the API only returns a string
|
||||
// @ts-ignore See above
|
||||
handleResponse(response, true);
|
||||
}
|
||||
return {
|
||||
domCreateByName,
|
||||
newRecipeName,
|
||||
createByName,
|
||||
...toRefs(state),
|
||||
validators,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
159
frontend/pages/recipe/create/url.vue
Normal file
159
frontend/pages/recipe/create/url.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags)">
|
||||
<div>
|
||||
<v-card-title class="headline"> Scrape Recipe </v-card-title>
|
||||
<v-card-text>
|
||||
Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the
|
||||
recipe from that site and add it to your collection.
|
||||
<v-text-field
|
||||
v-model="recipeUrl"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
clearable
|
||||
class="rounded-lg mt-2"
|
||||
rounded
|
||||
:rules="[validators.url]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
<v-checkbox v-model="importKeywordsAsTags" label="Import original keywords as tags"> </v-checkbox>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" />
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-form>
|
||||
<v-expand-transition>
|
||||
<v-alert v-show="error" color="error" class="mt-6 white--text">
|
||||
<v-card-title class="ma-0 pa-0">
|
||||
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
||||
{{ $t("new-recipe.error-title") }}
|
||||
</v-card-title>
|
||||
<v-divider class="my-3 mx-2"></v-divider>
|
||||
|
||||
<p>
|
||||
{{ $t("new-recipe.error-details") }}
|
||||
</p>
|
||||
<div class="d-flex row justify-space-around my-3 force-white">
|
||||
<a
|
||||
class="dark"
|
||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
{{ $t("new-recipe.google-ld-json-info") }}
|
||||
</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.github-issues") }}
|
||||
</a>
|
||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.recipe-markup-specification") }}
|
||||
</a>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { onMounted } from "vue-demi";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||
if (response?.status !== 201) {
|
||||
state.error = true;
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||
}
|
||||
|
||||
const recipeUrl = computed({
|
||||
set(recipe_import_url: string | null) {
|
||||
if (recipe_import_url !== null) {
|
||||
recipe_import_url = recipe_import_url.trim();
|
||||
router.replace({ query: { ...route.value.query, recipe_import_url } });
|
||||
}
|
||||
},
|
||||
get() {
|
||||
return route.value.query.recipe_import_url as string | null;
|
||||
},
|
||||
});
|
||||
|
||||
const importKeywordsAsTags = computed({
|
||||
get() {
|
||||
return route.value.query.import_keywords_as_tags === "1";
|
||||
},
|
||||
set(keywordsAsTags: boolean) {
|
||||
let import_keywords_as_tags = "0";
|
||||
if (keywordsAsTags) {
|
||||
import_keywords_as_tags = "1";
|
||||
}
|
||||
router.replace({ query: { ...route.value.query, import_keywords_as_tags } });
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!recipeUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipeUrl.value.includes("https")) {
|
||||
createByUrl(recipeUrl.value, importKeywordsAsTags.value);
|
||||
}
|
||||
});
|
||||
|
||||
const domUrlForm = ref<VForm | null>(null);
|
||||
|
||||
async function createByUrl(url: string, importKeywordsAsTags: boolean) {
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!domUrlForm.value?.validate() || url === "") {
|
||||
console.log("Invalid URL", url);
|
||||
return;
|
||||
}
|
||||
state.loading = true;
|
||||
const { response } = await api.recipes.createOneByUrl(url, importKeywordsAsTags);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
return {
|
||||
recipeUrl,
|
||||
importKeywordsAsTags,
|
||||
domUrlForm,
|
||||
createByUrl,
|
||||
...toRefs(state),
|
||||
validators,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.force-white > a {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
78
frontend/pages/recipe/create/zip.vue
Normal file
78
frontend/pages/recipe/create/zip.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<v-form>
|
||||
<div>
|
||||
<v-card-title class="headline"> Import from Zip </v-card-title>
|
||||
<v-card-text>
|
||||
Import a single recipe that was exported from another Mealie instance.
|
||||
<v-file-input
|
||||
v-model="newRecipeZip"
|
||||
accept=".zip"
|
||||
label=".zip"
|
||||
filled
|
||||
clearable
|
||||
class="rounded-lg mt-2"
|
||||
rounded
|
||||
truncate-length="100"
|
||||
hint=".zip files must have been exported from Mealie"
|
||||
persistent-hint
|
||||
prepend-icon=""
|
||||
:prepend-inner-icon="$globals.icons.zip"
|
||||
>
|
||||
</v-file-input>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<div style="width: 250px">
|
||||
<BaseButton :disabled="newRecipeZip === null" large rounded block :loading="loading" @click="createByZip" />
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
const api = useUserApi();
|
||||
const router = useRouter();
|
||||
|
||||
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||
if (response?.status !== 201) {
|
||||
state.error = true;
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||
}
|
||||
|
||||
const newRecipeZip = ref<File | null>(null);
|
||||
const newRecipeZipFileName = "archive";
|
||||
|
||||
async function createByZip() {
|
||||
if (!newRecipeZip.value) {
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
||||
|
||||
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
return {
|
||||
newRecipeZip,
|
||||
createByZip,
|
||||
...toRefs(state),
|
||||
validators,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,44 +1,36 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<RecipeCategoryTagToolPage v-if="categories" :items="categories" item-type="categories" @delete="removeCat" />
|
||||
<RecipeOrganizerPage
|
||||
v-if="items"
|
||||
:items="items"
|
||||
:icon="$globals.icons.tags"
|
||||
item-type="categories"
|
||||
@delete="actions.deleteOne"
|
||||
>
|
||||
<template #title> {{ $tc("category.categories") }} </template>
|
||||
</RecipeOrganizerPage>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||
import { useCategoryStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagToolPage,
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
setup() {
|
||||
const userApi = useUserApi();
|
||||
const categories = useAsync(async () => {
|
||||
const { data } = await userApi.categories.getAll();
|
||||
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}, useAsyncKey());
|
||||
|
||||
function removeCat(id: string) {
|
||||
if (categories.value) {
|
||||
for (let i = 0; i < categories.value.length; i++) {
|
||||
if (categories.value[i].id === id) {
|
||||
categories.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { items, actions } = useCategoryStore();
|
||||
|
||||
return {
|
||||
categories,
|
||||
removeCat,
|
||||
items,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tags",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tags" @delete="removeTag" />
|
||||
<RecipeOrganizerPage
|
||||
v-if="items"
|
||||
:items="items"
|
||||
:icon="$globals.icons.tags"
|
||||
item-type="tags"
|
||||
@delete="actions.deleteOne"
|
||||
>
|
||||
<template #title> Tags </template>
|
||||
</RecipeOrganizerPage>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||
import { useTagStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagToolPage,
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
setup() {
|
||||
const userApi = useUserApi();
|
||||
const tools = useAsync(async () => {
|
||||
const { data } = await userApi.tags.getAll();
|
||||
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}, useAsyncKey());
|
||||
|
||||
function removeTag(id: string) {
|
||||
if (tools.value) {
|
||||
for (let i = 0; i < tools.value.length; i++) {
|
||||
if (tools.value[i].id === id) {
|
||||
tools.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { items, actions } = useTagStore();
|
||||
|
||||
return {
|
||||
tools,
|
||||
removeTag,
|
||||
items,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tags",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tools" @delete="removeTool" />
|
||||
<RecipeOrganizerPage
|
||||
v-if="tools"
|
||||
:icon="$globals.icons.potSteam"
|
||||
:items="tools"
|
||||
item-type="tools"
|
||||
@delete="actions.deleteOne"
|
||||
>
|
||||
<template #title> Tools </template>
|
||||
</RecipeOrganizerPage>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagToolPage,
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
setup() {
|
||||
const userApi = useUserApi();
|
||||
const tools = useAsync(async () => {
|
||||
const { data } = await userApi.tools.getAll();
|
||||
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}, useAsyncKey());
|
||||
|
||||
function removeTool(id: string) {
|
||||
if (tools.value) {
|
||||
for (let i = 0; i < tools.value.length; i++) {
|
||||
if (tools.value[i].id === id) {
|
||||
tools.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const toolStore = useToolStore();
|
||||
const dialog = ref(false);
|
||||
|
||||
return {
|
||||
tools,
|
||||
removeTool,
|
||||
dialog,
|
||||
tools: toolStore.items,
|
||||
actions: toolStore.actions,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tools",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -35,23 +35,30 @@
|
||||
<v-expand-transition>
|
||||
<v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around">
|
||||
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-model="includeCategories"
|
||||
hide-details
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
:input-attrs="{
|
||||
solo: true,
|
||||
hideDetails: true,
|
||||
dense: false,
|
||||
}"
|
||||
:show-add="false"
|
||||
:return-object="false"
|
||||
selector-type="categories"
|
||||
/>
|
||||
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
||||
</v-col>
|
||||
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-model="includeTags"
|
||||
hide-details
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
:input-attrs="{
|
||||
solo: true,
|
||||
hideDetails: true,
|
||||
dense: false,
|
||||
}"
|
||||
:show-add="false"
|
||||
:return-object="false"
|
||||
:tag-selector="true"
|
||||
selector-type="tags"
|
||||
/>
|
||||
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
|
||||
</v-col>
|
||||
@@ -106,12 +113,13 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { defineComponent, toRefs, computed, reactive } from "@nuxtjs/composition-api";
|
||||
import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue";
|
||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useRecipes, allRecipes, useFoods } from "~/composables/recipes";
|
||||
import { useRecipes, allRecipes } from "~/composables/recipes";
|
||||
import { RecipeSummary } from "~/types/api-types/recipe";
|
||||
import { useRouteQuery } from "~/composables/use-router";
|
||||
import { RecipeTag } from "~/types/api-types/user";
|
||||
import { useFoodStore } from "~/composables/store";
|
||||
|
||||
interface GenericFilter {
|
||||
exclude: boolean;
|
||||
@@ -120,7 +128,7 @@ interface GenericFilter {
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagSelector,
|
||||
RecipeOrganizerSelector,
|
||||
RecipeSearchFilterSelector,
|
||||
RecipeCardSection,
|
||||
},
|
||||
@@ -216,7 +224,7 @@ export default defineComponent({
|
||||
if (searchString.value.trim() === "") {
|
||||
return filteredRecipes.value;
|
||||
}
|
||||
const result = fuse.value.search(searchString.value.trim());
|
||||
const result = fuse.value.search(searchString.value.trim() as string);
|
||||
return result.map((x) => x.item);
|
||||
});
|
||||
|
||||
@@ -259,7 +267,7 @@ export default defineComponent({
|
||||
state.foodFilter = params;
|
||||
}
|
||||
|
||||
const { foods } = useFoods();
|
||||
const { foods } = useFoodStore();
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
:key="imageKey"
|
||||
:max-width="enableLandscape ? null : '50%'"
|
||||
:height="hideImage ? '50' : imageHeight"
|
||||
:src="recipeImage(recipe.slug, imageKey)"
|
||||
:src="recipeImage(recipe.id, imageKey)"
|
||||
class="d-print-none"
|
||||
@error="hideImage = true"
|
||||
>
|
||||
@@ -197,6 +197,9 @@
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
:edit="form"
|
||||
public
|
||||
:assets="recipe.assets"
|
||||
:recipe-id="recipe.id"
|
||||
:recipe-slug="recipe.slug"
|
||||
/>
|
||||
|
||||
<!-- TODO: Somehow fix duplicate code for mobile/desktop -->
|
||||
@@ -323,7 +326,6 @@ export default defineComponent({
|
||||
const { data } = await api.recipes.getShared(id);
|
||||
if (data) {
|
||||
if (data && data !== undefined) {
|
||||
console.log("Computed Meta. RefKey=");
|
||||
const imageURL = data.id ? recipeImage(data.id) : undefined;
|
||||
title.value = data.name;
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
<script lang="ts">
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
import { defineComponent, useAsync, useRoute, computed, ref } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, useAsync, useRoute, computed, ref, watch } from "@nuxtjs/composition-api";
|
||||
import { useToggle } from "@vueuse/core";
|
||||
import { useCopyList } from "~/composables/use-copy";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
@@ -367,11 +367,13 @@ export default defineComponent({
|
||||
return labels;
|
||||
});
|
||||
|
||||
const itemsByLabel = computed(() => {
|
||||
const items: { [prop: string]: ShoppingListItemCreate[] } = {};
|
||||
const itemsByLabel = ref<{ [key: string]: ShoppingListItemOut[] }>({});
|
||||
|
||||
function updateItemsByLabel() {
|
||||
const items: { [prop: string]: ShoppingListItemOut[] } = {};
|
||||
|
||||
const noLabel = {
|
||||
"No Label": [] as ShoppingListItemCreate[],
|
||||
"No Label": [] as ShoppingListItemOut[],
|
||||
};
|
||||
|
||||
shoppingList.value?.listItems?.forEach((item) => {
|
||||
@@ -394,7 +396,11 @@ export default defineComponent({
|
||||
items["No Label"] = noLabel["No Label"];
|
||||
}
|
||||
|
||||
return items;
|
||||
itemsByLabel.value = items;
|
||||
}
|
||||
|
||||
watch(shoppingList, () => {
|
||||
updateItemsByLabel();
|
||||
});
|
||||
|
||||
async function refreshLabels() {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Plugin } from "@nuxt/types";
|
||||
import { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
const toastPlugin: Plugin = ({ $axios }: { $axios: NuxtAxiosInstance }) => {
|
||||
$axios.onResponse((response) => {
|
||||
if (response?.data?.message) {
|
||||
alert.info(response.data.message);
|
||||
alert.info(response.data.message as string);
|
||||
}
|
||||
});
|
||||
$axios.onError((error) => {
|
||||
if (error.response?.data?.detail?.message) {
|
||||
alert.error(error.response.data.detail.message);
|
||||
alert.error(error.response.data.detail.message as string);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,7 +17,15 @@
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "@nuxtjs/i18n", "@nuxtjs/auth-next", "@nuxtjs/vuetify", "@types/sortablejs"]
|
||||
"types": [
|
||||
"@nuxt/types",
|
||||
"@nuxtjs/axios",
|
||||
"@types/node",
|
||||
"@nuxtjs/i18n",
|
||||
"@nuxtjs/auth-next",
|
||||
"@nuxtjs/vuetify",
|
||||
"@types/sortablejs"
|
||||
]
|
||||
},
|
||||
"include": ["**/*", ".eslintrc.js"],
|
||||
"exclude": ["node_modules", ".nuxt", "dist"],
|
||||
|
||||
@@ -133,6 +133,7 @@ export interface IngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
id: string;
|
||||
}
|
||||
export interface CreateIngredientUnit {
|
||||
@@ -140,6 +141,7 @@ export interface CreateIngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
}
|
||||
export interface IngredientFood {
|
||||
name: string;
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface IngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
id: string;
|
||||
}
|
||||
export interface CreateIngredientUnit {
|
||||
@@ -119,6 +120,7 @@ export interface CreateIngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
}
|
||||
export interface IngredientFood {
|
||||
name: string;
|
||||
|
||||
@@ -207,6 +207,7 @@ export interface IngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
id: string;
|
||||
}
|
||||
export interface ReadGroupPreferences {
|
||||
@@ -287,6 +288,7 @@ export interface CreateIngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
}
|
||||
export interface CreateIngredientFood {
|
||||
name: string;
|
||||
|
||||
@@ -148,6 +148,7 @@ export interface IngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
id: string;
|
||||
}
|
||||
export interface CreateIngredientUnit {
|
||||
@@ -155,6 +156,7 @@ export interface CreateIngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
}
|
||||
export interface IngredientFood {
|
||||
name: string;
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface CreateIngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
}
|
||||
export interface CreateRecipe {
|
||||
name: string;
|
||||
@@ -117,6 +118,7 @@ export interface IngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
id: string;
|
||||
}
|
||||
export interface IngredientsRequest {
|
||||
@@ -340,6 +342,7 @@ export interface SaveIngredientUnit {
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
useAbbreviation?: boolean;
|
||||
groupId: string;
|
||||
}
|
||||
export interface ScrapeRecipe {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user