Compare commits

..

52 Commits

Author SHA1 Message Date
Hayden
56eb0bca71 release: prep beta 3 (#1349)
* docs: add tag documentation

* bump docker-compose tags

* minor language changes

* beta3 changelog

* auto-gen API docs

* bump version

* bump recipe-scrapers
2022-06-07 11:19:24 -08:00
Hayden
eca8a96509 fix image display - closes #1189 (#1348) 2022-06-05 11:44:00 -08:00
Hayden
7eb80d18d2 feat: attached images by drag and drop for recipe steps (#1341)
* add drag and drop support for recipe steps

* fix recipe assets dialog state

* add attr support for markdown editor

* add persistent hint for recipe text editor
2022-06-05 11:28:38 -08:00
Benjamin Pabst
37a673b34d Update postgres to use most recent version (#1347) 2022-06-05 10:56:01 -08:00
Benjamin Pabst
3e7b8d4b71 Update to use most recent version (#1346) 2022-06-05 10:55:33 -08:00
Jurjen de Jonge
abb114c375 security: delay server response whenever username is non existing (#1338)
* Delay server response whenever username is non existing

* utilize hasher to achieve constant timing

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-04 10:27:30 -08:00
Hayden
12f480eb75 refactor: unify recipe-organizer components (#1340)
* use generic context menu

* implement organizer stores

* add basic organizer types

* refactor selectors to apply for all organizers

* remove legacy organizer composables
2022-06-03 20:12:32 -08:00
Hayden
bc175d4ca9 New translations en-US.json (Ukrainian) (#1339) 2022-06-03 19:49:58 -08:00
Hayden
f78c5eb359 New Crowdin updates (#1329)
* New translations en-US.json (German)

* New translations en-US.json (German)

* New translations en-US.json (Ukrainian)
2022-06-02 09:12:15 -08:00
Hayden
5a0c034391 fix: shopping list label editor (#1333)
* remove head props

* lazily compute itemsByLabel with watcher on fetch

* remove unused import
2022-06-02 09:12:05 -08:00
Hayden
52fbf6b833 feat: add unit abbreviation support (#1332)
* add 'use-abbreviation' db column

* type generation

* add view and edit elements

* check for use_abbreviation to display

* fix: alembic version check

* test: add use_abbreviation prop tests
2022-06-01 11:59:50 -08:00
dependabot[bot]
592b1de39d fix(deps): bump @vue/composition-api from 1.6.1 to 1.6.2 in /frontend (#1275)
Bumps [@vue/composition-api](https://github.com/vuejs/composition-api) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/vuejs/composition-api/releases)
- [Changelog](https://github.com/vuejs/composition-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/composition-api/compare/v1.6.1...v1.6.2)

---
updated-dependencies:
- dependency-name: "@vue/composition-api"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-31 08:48:57 -08:00
dependabot[bot]
f29d5f1dff chore(deps-dev): bump @babel/eslint-parser in /frontend (#1290)
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.15.4 to 7.18.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.2/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-31 08:48:33 -08:00
dependabot[bot]
738ef0aaa7 chore(deps-dev): bump @types/sortablejs in /frontend (#1287)
Bumps [@types/sortablejs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sortablejs) from 1.10.7 to 1.13.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sortablejs)

---
updated-dependencies:
- dependency-name: "@types/sortablejs"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-31 08:48:01 -08:00
Hayden
f1fdec5afe add security.md 2022-05-31 08:29:45 -08:00
Hayden
4c594a48dc wip: pagination-repository (#1316)
* bump mypy

* add pagination + refactor generic repo

* add pagination test

* remove all query object
2022-05-30 10:30:54 -08:00
Hayden
00f144a622 chore: init git-cliff config 2022-05-29 17:34:41 -08:00
Hayden
d2a9f7ca24 fix: consoldate stores to fix mismatched state 2022-05-29 17:34:41 -08:00
Hayden
f831791db2 feat: default unit fractions to True 2022-05-29 17:34:41 -08:00
Hayden
c3bdfe7b3b fix: printer page refs (#1314) 2022-05-29 09:15:38 -08:00
Hayden
3542bb0927 fix: bad import path (#1313)
* fix bad import

* add eslint rule for incorrect imports
2022-05-29 09:09:36 -08:00
Philipp Fischbeck
e898c80f59 fix: update issue links in v1.0.0beta-2 changelog (#1312) 2022-05-29 08:51:11 -08:00
James Addison
27c5cfc56b Fixup: render homepage social media link images at 32x32 size (#1310) 2022-05-29 08:50:44 -08:00
Hayden
369cda0a61 New Crowdin updates (#1308)
* New translations en-US.json (Italian)

* New translations en-US.json (Italian)
2022-05-29 08:50:18 -08:00
Hayden
b981cf62bf chore: bump version (#1307)
* bump version

* add release notes
2022-05-28 17:13:36 -08:00
Hayden
ee93d77ace Update stale.yml
exclude tasks
2022-05-28 17:04:40 -08:00
Hayden
3dcfcc1fa9 feat: rewrite print implementation to support new ing (#1305) 2022-05-28 17:00:37 -08:00
Hayden
80f1a9add8 New Crowdin updates (#1304)
* New translations en-US.json (French)

* New translations en-US.json (German)

* New translations en-US.json (German)

* New translations en-US.json (German)
2022-05-28 16:38:59 -08:00
Hayden
137bf9de91 bump recipe scrapers version (#1303) 2022-05-28 16:38:47 -08:00
Drumstickx
1534f0df77 docs: docker-compose.dev.yml is currently not functional (#1300) 2022-05-27 16:17:47 -08:00
Drumstickx
d751e3b35b docs: add references for VSCode dev containers (#1299) 2022-05-27 16:17:39 -08:00
Hayden
07bf5be3ec improve touch support with icon and handle (#1302) 2022-05-27 16:17:25 -08:00
Hayden
a96f94a149 fix: add touch support for mealplanner delete (#1298)
* add touch support for mealplanner delete

* remove click.stop
2022-05-26 19:31:54 -08:00
Hayden
78a8204b58 New translations en-US.json (German) (#1296) 2022-05-26 09:01:17 -08:00
Hayden
649e34f66e add touch support for mealplanner (#1295) 2022-05-25 20:13:13 -08:00
Hayden
010aafa69b feat: add reports to bulk recipe import (url) (#1294)
* remove unused docker and caddy configs

* add experimental nested configs

* switch to nest under docker-compose

* remove v-card

* bulk parser backend re-implementation

* refactor UI for bulk importer

* remove migration specific report text
2022-05-25 19:33:58 -08:00
Hayden
d66d6c55ae fix recipe assets build (#1286) 2022-05-25 11:50:45 -08:00
Hayden
7609715d9e remove explicity typescript version (#1285)
* remove explicity typescript version

* i hate javascript
2022-05-25 10:14:24 -08:00
Hayden
921fceddea chore: update dev dependencies (#1282)
* update dev dependencies

* upgrade eslint

* resolve several errors

* resolve eslint errors
2022-05-25 09:38:21 -08:00
Hayden
01f3fef21f New Crowdin updates (#1284)
* New translations en-US.json (German)

* New translations en-US.json (German)
2022-05-25 09:08:41 -08:00
Philipp Fischbeck
8f7c7c39bb refactor: split up recipe create page (#1283)
* refactor: split up recipe create page

* add flat card

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-05-25 09:08:32 -08:00
Hayden
30d19c6503 fix: bad dev dependency (#1281)
* revert bad dev dependency

* fix concurrent builds
2022-05-24 21:19:50 -08:00
Hayden
ea503a0235 add conccureny config (#1280) 2022-05-24 20:24:33 -08:00
dependabot[bot]
c05c123880 fix(deps): bump @nuxtjs/auth-next in /frontend (#1265)
Bumps [@nuxtjs/auth-next](https://github.com/nuxt-community/auth-module) from 5.0.0-1624817847.21691f1 to 5.0.0-1648802546.c9880dc.
- [Release notes](https://github.com/nuxt-community/auth-module/releases)
- [Changelog](https://github.com/nuxt-community/auth-module/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/nuxt-community/auth-module/commits)

---
updated-dependencies:
- dependency-name: "@nuxtjs/auth-next"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:02:43 -08:00
dependabot[bot]
6f45de6167 chore(deps-dev): bump vue2-script-setup-transform in /frontend (#1263)
Bumps [vue2-script-setup-transform](https://github.com/antfu/vue2-script-setup-transform) from 0.2.6 to 0.3.5.
- [Release notes](https://github.com/antfu/vue2-script-setup-transform/releases)
- [Commits](https://github.com/antfu/vue2-script-setup-transform/compare/v0.2.6...v0.3.5)

---
updated-dependencies:
- dependency-name: vue2-script-setup-transform
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:02:00 -08:00
dependabot[bot]
d634e2fbe1 chore(deps-dev): bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend (#1260)
Bumps [nuxt-vite](https://github.com/nuxt/vite) from 0.1.3 to 0.3.5.
- [Release notes](https://github.com/nuxt/vite/releases)
- [Changelog](https://github.com/nuxt/vite/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nuxt/vite/compare/v0.1.3...v0.3.5)

---
updated-dependencies:
- dependency-name: nuxt-vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:00:50 -08:00
dependabot[bot]
43a566339a chore(deps-dev): bump @vue/runtime-dom in /frontend (#1259)
Bumps [@vue/runtime-dom](https://github.com/vuejs/core/tree/HEAD/packages/runtime-dom) from 3.2.35 to 3.2.36.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.2.36/packages/runtime-dom)

---
updated-dependencies:
- dependency-name: "@vue/runtime-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:00:16 -08:00
dependabot[bot]
cc284a0ceb chore(deps-dev): bump eslint-plugin-nuxt in /frontend (#1258)
Bumps [eslint-plugin-nuxt](https://github.com/nuxt/eslint-plugin-nuxt) from 2.0.0 to 3.2.0.
- [Release notes](https://github.com/nuxt/eslint-plugin-nuxt/releases)
- [Changelog](https://github.com/nuxt/eslint-plugin-nuxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nuxt/eslint-plugin-nuxt/compare/v2.0.0...v3.2.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-nuxt
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 19:59:49 -08:00
dependabot[bot]
3c19105d8b fix(deps): bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend (#1257)
Bumps [isomorphic-dompurify](https://github.com/kkomelin/isomorphic-dompurify) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/kkomelin/isomorphic-dompurify/releases)
- [Commits](https://github.com/kkomelin/isomorphic-dompurify/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: isomorphic-dompurify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 19:58:14 -08:00
Hayden
b8ee1a4bd8 fix #1270 migration not capture settings (#1272) 2022-05-24 09:55:15 -08:00
Hayden
c30ffbc851 update readme 2022-05-24 09:51:39 -08:00
Hayden
3ddbc033b2 chore: github stalebot changes (#1271)
* add exempt labels

* update tags for docker-compose
2022-05-24 08:30:07 -08:00
142 changed files with 4897 additions and 3863 deletions

6
.github/stale.yml vendored
View File

@@ -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

View File

@@ -5,6 +5,10 @@ on:
branches:
- mealie-next
concurrency:
group: backend-nightly-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -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
View File

@@ -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"
}
}

View File

@@ -5,12 +5,6 @@
[![MIT License][license-shield]][license-url]
[![Docker Pulls][docker-pull]][docker-pull]
[![CodeFactor](https://www.codefactor.io/repository/github/hay-kot/mealie/badge)](https://www.codefactor.io/repository/github/hay-kot/mealie)
[![Docker Build Production](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.release.yml/badge.svg)](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.release.yml)
[![Project Tests Production](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml/badge.svg)](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
[![Docker Build Dev](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml/badge.svg?branch=dev)](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml)
[![Project Tests Dev](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml/badge.svg?branch=dev)](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
View 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.

View File

@@ -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
View 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"

View File

@@ -1,3 +1,4 @@
# WARNING: currently not functional, see #756, #1072
# Use root/example as user/password credentials
version: "3.4"
services:

View File

@@ -1,15 +0,0 @@
{
auto_https off
}
:80 {
root * /srv
encode gzip
uri strip_suffix /
handle {
try_files {path} {path}/ /index.html
file_server
}
}

View File

@@ -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

View File

@@ -1,11 +0,0 @@
version: "3"
services:
wiki:
container_name: mealie-docs
image: mealie-docs
ports:
- 8888:80
build:
context: .
dockerfile: Dockerfile
restart: always

View 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))

View 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))

View File

@@ -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
```

View File

@@ -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>
```
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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/

View File

@@ -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

View File

@@ -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>

View File

@@ -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"

View File

@@ -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",

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
},

View File

@@ -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" });
}

View File

@@ -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";

View File

@@ -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,
};
},
});

View File

@@ -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>

View 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>

View 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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -122,8 +122,5 @@ export default defineComponent({
listItem,
};
},
head: {
title: "vbase-nuxt",
},
});
</script>
</script>

View File

@@ -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

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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";

View File

@@ -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) {

View 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,
};
}

View File

@@ -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";

View File

@@ -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 };
};

View File

@@ -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);
}

View File

@@ -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 };
};

View File

@@ -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),
};
};

View 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";

View 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,
};
}

View 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 };
};

View 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,
};
}

View 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,
};
}

View 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,
};
}

View 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 };
};

View 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",
},
};
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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": {

View File

@@ -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}",

View File

@@ -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 },

View File

@@ -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"
}
}

View File

@@ -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() {

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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: {},

View File

@@ -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,

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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),

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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);
}
});
};

View File

@@ -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"],

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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