mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
General Frontend Bug Fixes for v0.4.0 RC (#233)
* comment * add frontend-build command * address #211 * fix margins * fix import bug * await user updates * fix meal-plan filter * meal-plan search redesign * improve mobile search * fix sidebar update * fix category auto-completes * draft new pages * fix tag auto completes * refactor export const * dispatch evens for CRUD operations * recipe loaders screen * create category dialog Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
6a71161252
commit
c8284eaeee
46 changed files with 440 additions and 165 deletions
36
.gitignore
vendored
36
.gitignore
vendored
|
@ -156,3 +156,39 @@ mealie/data/debug/last_recipe.json
|
|||
*.sqlite
|
||||
dev/data/db/test.db
|
||||
scratch.py
|
||||
frontend/dist/favicon.ico
|
||||
frontend/dist/index.html
|
||||
frontend/dist/css/app.29fe0155.css
|
||||
frontend/dist/css/chunk-vendors.db944396.css
|
||||
frontend/dist/fonts/materialdesignicons-webfont.7a44ea19.woff2
|
||||
frontend/dist/fonts/materialdesignicons-webfont.64d4cf64.eot
|
||||
frontend/dist/fonts/materialdesignicons-webfont.147e3378.woff
|
||||
frontend/dist/fonts/materialdesignicons-webfont.174c02fc.ttf
|
||||
frontend/dist/fonts/roboto-latin-100.5cb7edfc.woff
|
||||
frontend/dist/fonts/roboto-latin-100.7370c367.woff2
|
||||
frontend/dist/fonts/roboto-latin-100italic.f8b1df51.woff2
|
||||
frontend/dist/fonts/roboto-latin-100italic.f9e8e590.woff
|
||||
frontend/dist/fonts/roboto-latin-300.b00849e0.woff
|
||||
frontend/dist/fonts/roboto-latin-300.ef7c6637.woff2
|
||||
frontend/dist/fonts/roboto-latin-300italic.4df32891.woff
|
||||
frontend/dist/fonts/roboto-latin-300italic.14286f3b.woff2
|
||||
frontend/dist/fonts/roboto-latin-400.60fa3c06.woff
|
||||
frontend/dist/fonts/roboto-latin-400.479970ff.woff2
|
||||
frontend/dist/fonts/roboto-latin-400italic.51521a2a.woff2
|
||||
frontend/dist/fonts/roboto-latin-400italic.fe65b833.woff
|
||||
frontend/dist/fonts/roboto-latin-500.020c97dc.woff2
|
||||
frontend/dist/fonts/roboto-latin-500.87284894.woff
|
||||
frontend/dist/fonts/roboto-latin-500italic.288ad9c6.woff
|
||||
frontend/dist/fonts/roboto-latin-500italic.db4a2a23.woff2
|
||||
frontend/dist/fonts/roboto-latin-700.2735a3a6.woff2
|
||||
frontend/dist/fonts/roboto-latin-700.adcde98f.woff
|
||||
frontend/dist/fonts/roboto-latin-700italic.81f57861.woff
|
||||
frontend/dist/fonts/roboto-latin-700italic.da0e7178.woff2
|
||||
frontend/dist/fonts/roboto-latin-900.9b3766ef.woff2
|
||||
frontend/dist/fonts/roboto-latin-900.bb1e4dc6.woff
|
||||
frontend/dist/fonts/roboto-latin-900italic.28f91510.woff
|
||||
frontend/dist/fonts/roboto-latin-900italic.ebf6d164.woff2
|
||||
frontend/dist/js/app.36f2760c.js
|
||||
frontend/dist/js/app.36f2760c.js.map
|
||||
frontend/dist/js/chunk-vendors.c93761e4.js
|
||||
frontend/dist/js/chunk-vendors.c93761e4.js.map
|
||||
|
|
|
@ -55,8 +55,7 @@ services:
|
|||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
|
||||
|
||||
## Deployed as a Python Application
|
||||
Alternatively, this project is built on Python and SQLite. If you are dead set on deploying on a linux machine you can run this in an python virtual env. Provided that you know thats how you want to host the application, I'll assume you know how to do that. I may or may not get around to writing this guide. I'm open to pull requests if anyone has a good guide on it.
|
||||
|
||||
|
||||
## Advanced
|
||||
!!! warning "Not Required"
|
||||
|
@ -88,4 +87,29 @@ The Docker image provided by Mealie contains both the API and the html bundle in
|
|||
}
|
||||
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Deployed without Docker
|
||||
!!! error "Unsupported Deployment"
|
||||
If you are experiencing a problem with manual deployment, please do not submit a github issue unless it is related to an aspect of the application. For deployment help, the [discord server](https://discord.gg/R6QDyJgbD2) is a better place to find support.
|
||||
|
||||
Alternatively, this project is built on Python and SQLite so you may run it as a python application on your server. This is not a supported options for deployment and is only here as a reference for those who would like to do this on their own. To get started you can clone this repository into a directory of your choice and use the instructions below as a reference for how to get started.
|
||||
|
||||
There are three parts to the Mealie application
|
||||
|
||||
- Frontend/Static Files
|
||||
- Backend API
|
||||
- Proxy Server
|
||||
|
||||
### Frontend/ Static Files
|
||||
The frontend static files are generated with `npm run build`. This is done during the build process with docker. If you choose to deploy this as a system application you must do this process yourself. In the project directory run `cd frontend` to change directories into the frontend directory and run `npm install` and then `npm run build`. This will generate the static files in a `dist` folder in the frontend directory.
|
||||
|
||||
### Backend API
|
||||
The backend API is build with Python, FastAPI, and SQLite and requires Python 3.9, and Poetry. Once the requirements are installed, in the project directory you can run the command `poetry install` to create a python virtual environment and install the python dependencies.
|
||||
|
||||
Once the dependencies are installed you should be ready to run the server. To initialize that database you need to first run `python mealie/db/init_db.py`. Then to start The web server, you run the command `uvicorn mealie.app:app --host 0.0.0.0 --port 9000`
|
||||
|
||||
|
||||
### Proxy Server
|
||||
You must use a proxy server to server up the static files created with `npm run build` and proxy requests to the API. In the docker build this is done with Caddy. You can use the CaddyFile in the section above as a reference. One important thing to keep in mind is that you should drop any trailing `/` in the url. Not doing this may result in failed API requests.
|
||||
|
||||
|
|
4
docs/docs/getting-started/organizing-recipes.md
Normal file
4
docs/docs/getting-started/organizing-recipes.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Organizing Recipes
|
||||
|
||||
!!! tip
|
||||
Below is a suggestion of guidelines my wife and I use for organizing our recipes within Mealie. Mealie is fairly flexible, so feel free to utilize how you'd like! 👍
|
|
@ -1,5 +1,8 @@
|
|||
# Building Pages
|
||||
|
||||
!!! warning
|
||||
The page building is still experimental and may change. You can provide feedback on any changes you'd like to see on [github](https://github.com/hay-kot/mealie/discussions/229)
|
||||
|
||||
Custom pages can be created to organize multiple categories into a single page. Links to your custom pages are displayed on the home page sidebar and accessible by all users, however only Administrators can create or update pages.
|
||||
|
||||
To create a new page. Navigate to the settings page at `/admin/settings` and scroll down to the custom pages section. Here you can create, view, and edit your custom pages. To reorder how they are displayed on the sidebar you can drag and drop the pages into the preferred order.
|
||||
|
|
|
@ -39,6 +39,7 @@ nav:
|
|||
- Installation: "getting-started/install.md"
|
||||
- Updating: "getting-started/updating.md"
|
||||
- Working With Recipes: "getting-started/recipes.md"
|
||||
- Organizing Recipes: "getting-started/organizing-recipes.md"
|
||||
- Planning Meals: "getting-started/meal-planner.md"
|
||||
- iOS Shortcuts: "getting-started/ios.md"
|
||||
- API Usage: "getting-started/api-usage.md"
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none">
|
||||
<router-link to="/">
|
||||
<router-link v-if="!(isMobile && search)" to="/">
|
||||
<v-btn icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<div btn class="pl-2">
|
||||
<div v-if="!isMobile" btn class="pl-2">
|
||||
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
|
||||
>Mealie
|
||||
</v-toolbar-title>
|
||||
|
@ -20,6 +20,7 @@
|
|||
v-if="search"
|
||||
:show-results="true"
|
||||
@selected="navigateFromSearch"
|
||||
:max-width="isMobile ? '100%' : '450px'"
|
||||
/>
|
||||
</v-expand-x-transition>
|
||||
<v-btn icon @click="search = !search">
|
||||
|
@ -64,6 +65,12 @@ export default {
|
|||
this.search = false;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener("keyup", e => {
|
||||
if (e.key == "/" && !document.activeElement.id.startsWith("input")) {
|
||||
|
@ -79,6 +86,7 @@ export default {
|
|||
this.$store.dispatch("refreshToken");
|
||||
this.$store.dispatch("requestCurrentGroup");
|
||||
this.$store.dispatch("requestCategories");
|
||||
this.$store.dispatch("requestTags");
|
||||
this.darkModeSystemCheck();
|
||||
this.darkModeAddEventListener();
|
||||
},
|
||||
|
|
|
@ -63,5 +63,7 @@ const apiReq = {
|
|||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
export { apiReq };
|
||||
export { baseURL };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store";
|
||||
import { store } from "@/store";
|
||||
|
||||
const backupBase = baseURL + "backups/";
|
||||
|
||||
|
@ -13,7 +13,7 @@ const backupURLs = {
|
|||
downloadBackup: fileName => `${backupBase}${fileName}/download`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const backupAPI = {
|
||||
/**
|
||||
* Request all backups available on the server
|
||||
* @returns {Array} List of Available Backups
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "@/store";
|
||||
|
||||
const prefix = baseURL + "categories";
|
||||
|
||||
|
@ -9,17 +10,47 @@ const categoryURLs = {
|
|||
delete_category: category => `${prefix}/${category}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const categoryAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(categoryURLs.get_all);
|
||||
return response.data;
|
||||
},
|
||||
async get_recipes_in_category(category) {
|
||||
async create(name) {
|
||||
let response = await apiReq.post(categoryURLs.get_all, { name: name });
|
||||
store.dispatch("requestCategories");
|
||||
return response.data;
|
||||
},
|
||||
async getRecipesInCategory(category) {
|
||||
let response = await apiReq.get(categoryURLs.get_category(category));
|
||||
return response.data;
|
||||
},
|
||||
async delete(category) {
|
||||
let response = await apiReq.delete(categoryURLs.delete_category(category));
|
||||
store.dispatch("requestCategories");
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
const tagPrefix = baseURL + "tags";
|
||||
|
||||
const tagURLs = {
|
||||
getAll: `${tagPrefix}`,
|
||||
getTag: tag => `${tagPrefix}/${tag}`,
|
||||
deleteTag: tag => `${tagPrefix}/${tag}`,
|
||||
};
|
||||
|
||||
export const tagAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(tagURLs.getAll);
|
||||
return response.data;
|
||||
},
|
||||
async getRecipesInTag(tag) {
|
||||
let response = await apiReq.get(tagURLs.getTag(tag));
|
||||
return response.data;
|
||||
},
|
||||
async delete(tag) {
|
||||
let response = await apiReq.delete(tagURLs.deleteTag(tag));
|
||||
store.dispatch("requestTags");
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ const groupsURLs = {
|
|||
update: id => `${groupPrefix}/${id}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const groupAPI = {
|
||||
async allGroups() {
|
||||
let response = await apiReq.get(groupsURLs.groups);
|
||||
return response.data;
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
import backup from "./backup";
|
||||
import recipe from "./recipe";
|
||||
import mealplan from "./mealplan";
|
||||
import settings from "./settings";
|
||||
import themes from "./themes";
|
||||
import migration from "./migration";
|
||||
import myUtils from "./upload";
|
||||
import category from "./category";
|
||||
import meta from "./meta";
|
||||
import users from "./users";
|
||||
import signUps from "./signUps";
|
||||
import groups from "./groups";
|
||||
import siteSettings from "./siteSettings";
|
||||
import { backupAPI } from "./backup";
|
||||
import { recipeAPI } from "./recipe";
|
||||
import { mealplanAPI } from "./mealplan";
|
||||
import { settingsAPI } from "./settings";
|
||||
import { themeAPI } from "./themes";
|
||||
import { migrationAPI } from "./migration";
|
||||
import { utilsAPI } from "./upload";
|
||||
import { categoryAPI, tagAPI } from "./category";
|
||||
import { metaAPI } from "./meta";
|
||||
import { userAPI } from "./users";
|
||||
import { signupAPI } from "./signUps";
|
||||
import { groupAPI } from "./groups";
|
||||
import { siteSettingsAPI } from "./siteSettings";
|
||||
|
||||
/**
|
||||
* The main object namespace for interacting with the backend database
|
||||
*/
|
||||
export const api = {
|
||||
recipes: recipe,
|
||||
siteSettings: siteSettings,
|
||||
backups: backup,
|
||||
mealPlans: mealplan,
|
||||
settings: settings,
|
||||
themes: themes,
|
||||
migrations: migration,
|
||||
utils: myUtils,
|
||||
categories: category,
|
||||
meta: meta,
|
||||
users: users,
|
||||
signUps: signUps,
|
||||
groups: groups,
|
||||
recipes: recipeAPI,
|
||||
siteSettings: siteSettingsAPI,
|
||||
backups: backupAPI,
|
||||
mealPlans: mealplanAPI,
|
||||
settings: settingsAPI,
|
||||
themes: themeAPI,
|
||||
migrations: migrationAPI,
|
||||
utils: utilsAPI,
|
||||
categories: categoryAPI,
|
||||
tags: tagAPI,
|
||||
meta: metaAPI,
|
||||
users: userAPI,
|
||||
signUps: signupAPI,
|
||||
groups: groupAPI,
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ const mealPlanURLs = {
|
|||
shopping: planID => `${prefix}${planID}/shopping-list`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const mealplanAPI = {
|
||||
async create(postBody) {
|
||||
let response = await apiReq.post(mealPlanURLs.create, postBody);
|
||||
return response;
|
||||
|
|
|
@ -8,7 +8,7 @@ const debugURLs = {
|
|||
lastRecipe: `${prefix}/last-recipe-json`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const metaAPI = {
|
||||
async get_version() {
|
||||
let response = await apiReq.get(debugURLs.version);
|
||||
return response.data;
|
||||
|
|
|
@ -11,7 +11,7 @@ const migrationURLs = {
|
|||
import: (folder, file) => `${migrationBase}/${folder}/${file}/import`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const migrationAPI = {
|
||||
async getMigrations() {
|
||||
let response = await apiReq.get(migrationURLs.all);
|
||||
return response.data;
|
||||
|
|
|
@ -18,7 +18,7 @@ const recipeURLs = {
|
|||
updateImage: slug => `${prefix}${slug}/image`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const recipeAPI = {
|
||||
/**
|
||||
* Create a Recipe by URL
|
||||
* @param {string} recipeURL
|
||||
|
@ -43,6 +43,7 @@ export default {
|
|||
|
||||
async create(recipeData) {
|
||||
let response = await apiReq.post(recipeURLs.create, recipeData);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
@ -62,15 +63,13 @@ export default {
|
|||
},
|
||||
|
||||
async update(data) {
|
||||
const recipeSlug = data.slug;
|
||||
|
||||
let response = await apiReq.put(recipeURLs.update(recipeSlug), data);
|
||||
let response = await apiReq.put(recipeURLs.update(data.slug), data);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async delete(recipeSlug) {
|
||||
apiReq.delete(recipeURLs.delete(recipeSlug));
|
||||
await apiReq.delete(recipeURLs.delete(recipeSlug));
|
||||
store.dispatch("requestRecentRecipes");
|
||||
router.push(`/`);
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@ const settingsURLs = {
|
|||
testWebhooks: `${settingsBase}/webhooks/test`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const settingsAPI = {
|
||||
async requestAll() {
|
||||
let response = await apiReq.get(settingsURLs.siteSettings);
|
||||
return response.data;
|
||||
|
|
|
@ -10,7 +10,7 @@ const signUpURLs = {
|
|||
createUser: token => `${signUpPrefix}/${token}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const signupAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(signUpURLs.all);
|
||||
return response.data;
|
||||
|
|
|
@ -11,7 +11,7 @@ const settingsURLs = {
|
|||
customPage: id => `${settingsBase}/custom-pages/${id}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const siteSettingsAPI = {
|
||||
async get() {
|
||||
let response = await apiReq.get(settingsURLs.siteSettings);
|
||||
return response.data;
|
||||
|
|
|
@ -11,7 +11,7 @@ const settingsURLs = {
|
|||
deleteTheme: themeName => `${prefix}/${themeName}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const themeAPI = {
|
||||
async requestAll() {
|
||||
let response = await apiReq.get(settingsURLs.allThemes);
|
||||
return response.data;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
|
||||
export default {
|
||||
export const utilsAPI = {
|
||||
// import { api } from "@/api";
|
||||
async uploadFile(url, fileObject) {
|
||||
let response = await apiReq.post(url, fileObject, {
|
||||
|
|
|
@ -17,7 +17,7 @@ const usersURLs = {
|
|||
resetPassword: id => `${userPrefix}/${id}/reset-password`,
|
||||
};
|
||||
|
||||
export default {
|
||||
export const userAPI = {
|
||||
async login(formData) {
|
||||
let response = await apiReq.post(authURLs.token, formData, {
|
||||
headers: {
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<NewCategoryDialog />
|
||||
</v-app-bar>
|
||||
<v-list height="300" dense style="overflow:auto">
|
||||
<v-list-item-group>
|
||||
|
@ -132,11 +133,13 @@
|
|||
import { api } from "@/api";
|
||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||
import draggable from "vuedraggable";
|
||||
import NewCategoryDialog from "./NewCategoryDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
LanguageMenu,
|
||||
NewCategoryDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
78
frontend/src/components/Admin/General/NewCategoryDialog.vue
Normal file
78
frontend/src/components/Admin/General/NewCategoryDialog.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-btn icon @click="dialog = true">
|
||||
<v-icon color="white">mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<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">
|
||||
mdi-tag
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
Create a Category
|
||||
</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
|
||||
dense
|
||||
label="Category Name"
|
||||
v-model="categoryName"
|
||||
:rules="[rules.required]"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="dialog = false">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text type="submit" :disabled="!categoryName">
|
||||
{{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
props: {
|
||||
buttonText: String,
|
||||
value: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
categoryName: "",
|
||||
rules: {
|
||||
required: val =>
|
||||
!!val || this.$t("settings.theme.theme-name-is-required"),
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dialog(val) {
|
||||
if (!val) this.categoryName = "";
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async select() {
|
||||
await api.categories.create(this.categoryName);
|
||||
this.$emit("new-category", this.categoryName);
|
||||
this.dialog = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -22,7 +22,7 @@
|
|||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-dialog v-model="dialog" max-width="500">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
{{ $t("user.create-link") }}
|
||||
|
@ -42,19 +42,16 @@
|
|||
</v-app-bar>
|
||||
<v-form ref="newUser" @submit.prevent="save">
|
||||
<v-card-text>
|
||||
<v-row class="justify-center mt-3">
|
||||
<v-text-field
|
||||
class="mr-2"
|
||||
v-model="editedItem.name"
|
||||
:label="$t('user.link-name')"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
v-model="editedItem.admin"
|
||||
:label="$t('user.admin')"
|
||||
></v-checkbox>
|
||||
</v-row>
|
||||
<v-text-field
|
||||
v-model="editedItem.name"
|
||||
:label="$t('user.link-name')"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
v-model="editedItem.admin"
|
||||
:label="$t('user.admin')"
|
||||
></v-checkbox>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
|
|
|
@ -264,10 +264,10 @@ export default {
|
|||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.users.update(this.editedItem);
|
||||
await api.users.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
await api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
|
|
|
@ -115,26 +115,17 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
groupSettings() {
|
||||
this.buildMealStore();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
let categories = Array.from(this.groupSettings, x => x.name);
|
||||
this.items = await api.recipes.getAllByCategory(categories);
|
||||
|
||||
if (this.items.length === 0) {
|
||||
const keys = [
|
||||
"name",
|
||||
"slug",
|
||||
"image",
|
||||
"description",
|
||||
"dateAdded",
|
||||
"rating",
|
||||
];
|
||||
this.items = await api.recipes.allByKeys(keys);
|
||||
}
|
||||
this.$store.dispatch("requestCurrentGroup");
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupSettings() {
|
||||
console.log(this.$store.getters.getCurrentGroup);
|
||||
return this.$store.getters.getCurrentGroup;
|
||||
},
|
||||
actualStartDate() {
|
||||
|
@ -164,6 +155,22 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
async buildMealStore() {
|
||||
let categories = Array.from(this.groupSettings.categories, x => x.name);
|
||||
this.items = await api.recipes.getAllByCategory(categories);
|
||||
|
||||
if (this.items.length === 0) {
|
||||
const keys = [
|
||||
"name",
|
||||
"slug",
|
||||
"image",
|
||||
"description",
|
||||
"dateAdded",
|
||||
"rating",
|
||||
];
|
||||
this.items = await api.recipes.allByKeys(keys);
|
||||
}
|
||||
},
|
||||
get_random(list) {
|
||||
const object = list[Math.floor(Math.random() * list.length)];
|
||||
return object;
|
||||
|
|
|
@ -120,17 +120,19 @@
|
|||
deletable-chips
|
||||
v-model="value.recipeCategory"
|
||||
hide-selected
|
||||
:items="categories"
|
||||
:items="allCategories"
|
||||
text="name"
|
||||
:search-input.sync="categoriesSearchInput"
|
||||
@change="categoriesSearchInput = ''"
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="removeCategory(data.index)"
|
||||
color="secondary"
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
>
|
||||
{{ data.item }}
|
||||
|
@ -146,16 +148,18 @@
|
|||
deletable-chips
|
||||
v-model="value.tags"
|
||||
hide-selected
|
||||
:items="tags"
|
||||
:items="allTags"
|
||||
:search-input.sync="tagsSearchInput"
|
||||
@change="tagssSearchInput = ''"
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
label
|
||||
@click:close="removeTags(data.index)"
|
||||
color="secondary"
|
||||
color="accent"
|
||||
dark
|
||||
>
|
||||
{{ data.item }}
|
||||
|
@ -221,7 +225,7 @@
|
|||
elevation="0"
|
||||
@click="removeStep(index)"
|
||||
>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
<v-icon size="24" color="error">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
{{ $t("recipe.step-index", { step: index + 1 }) }}
|
||||
</v-card-title>
|
||||
|
@ -280,18 +284,19 @@ export default {
|
|||
},
|
||||
categoriesSearchInput: "",
|
||||
tagsSearchInput: "",
|
||||
categories: [],
|
||||
tags: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getCategories();
|
||||
computed: {
|
||||
allCategories() {
|
||||
const categories = this.$store.getters.getAllCategories;
|
||||
return categories.map(cat => cat.name);
|
||||
},
|
||||
allTags() {
|
||||
const tags = this.$store.getters.getAllTags;
|
||||
return tags.map(cat => cat.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getCategories() {
|
||||
let response = await api.categories.get_all();
|
||||
this.categories = response.map(cat => cat.name);
|
||||
},
|
||||
uploadImage() {
|
||||
this.$emit("upload", this.fileObject);
|
||||
},
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<div v-if="items && items.length > 0">
|
||||
<h2 class="mt-4">{{ title }}</h2>
|
||||
<v-chip
|
||||
:to="`/recipes/${getSlug(category)}`"
|
||||
label
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
dark
|
||||
|
@ -18,6 +20,21 @@ export default {
|
|||
props: {
|
||||
items: Array,
|
||||
title: String,
|
||||
category: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
allCategories() {
|
||||
return this.$store.getters.getAllCategories;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSlug(name) {
|
||||
if (this.category) {
|
||||
return this.allCategories.filter(x => x.name == name)[0].slug;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,34 +1,38 @@
|
|||
<template>
|
||||
<v-menu v-model="menuModel" offset-y readonly max-width="450">
|
||||
<v-menu v-model="menuModel" offset-y readonly :width="maxWidth">
|
||||
<template #activator="{ attrs }">
|
||||
<v-text-field
|
||||
class="mt-6"
|
||||
v-model="search"
|
||||
v-bind="attrs"
|
||||
dense
|
||||
:dense="dense"
|
||||
light
|
||||
:label="$t('search.search-mealie')"
|
||||
solo
|
||||
autofocus
|
||||
style="max-width: 450px;"
|
||||
:solo="solo"
|
||||
:style="`max-width: ${maxWidth};`"
|
||||
@focus="onFocus"
|
||||
autocomplete="off"
|
||||
>
|
||||
</v-text-field>
|
||||
</template>
|
||||
<v-card v-if="showResults" max-height="500" min-width="98%" class="">
|
||||
<v-card-text class="py-1">Results</v-card-text>
|
||||
<v-card v-if="showResults" max-height="500" :max-width="maxWidth">
|
||||
<v-card-text class="py-1">Results</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list scrollable>
|
||||
<v-list-item
|
||||
v-for="(item, index) in autoResults"
|
||||
:key="index"
|
||||
:to="showResults ? `/recipe/${item.item.slug}` : null"
|
||||
:to="navOnClick ? `/recipe/${item.item.slug}` : null"
|
||||
@click="navOnClick ? null : selected(item.item.slug, item.item.name)"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.image)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content
|
||||
@click="showResults ? null : selected(item.item.slug)"
|
||||
@click="
|
||||
showResults ? null : selected(item.item.slug, item.item.name)
|
||||
"
|
||||
>
|
||||
<v-list-item-title v-html="highlight(item.item.name)">
|
||||
</v-list-item-title>
|
||||
|
@ -57,6 +61,21 @@ export default {
|
|||
showResults: {
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
default: "450px",
|
||||
},
|
||||
dense: {
|
||||
default: true,
|
||||
},
|
||||
navOnClick: {
|
||||
default: true,
|
||||
},
|
||||
resetSearch: {
|
||||
default: false,
|
||||
},
|
||||
solo: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -65,7 +84,7 @@ export default {
|
|||
menuModel: false,
|
||||
data: [],
|
||||
result: [],
|
||||
autoResults: [],
|
||||
fuseResults: [],
|
||||
isDark: false,
|
||||
options: {
|
||||
shouldSort: true,
|
||||
|
@ -84,6 +103,9 @@ export default {
|
|||
this.data = this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
computed: {
|
||||
autoResults() {
|
||||
return this.fuseResults.length > 1 ? this.fuseResults : this.results;
|
||||
},
|
||||
fuse() {
|
||||
return new Fuse(this.data, this.options);
|
||||
},
|
||||
|
@ -96,6 +118,10 @@ export default {
|
|||
val ? (this.menuModel = true) : null;
|
||||
},
|
||||
|
||||
resetSearch(val) {
|
||||
val ? (this.search = "") : null;
|
||||
},
|
||||
|
||||
search() {
|
||||
try {
|
||||
this.result = this.fuse.search(this.search.trim());
|
||||
|
@ -107,7 +133,7 @@ export default {
|
|||
this.$emit("results", this.result);
|
||||
|
||||
if (this.showResults === true) {
|
||||
this.autoResults = this.result;
|
||||
this.fuseResults = this.result;
|
||||
}
|
||||
},
|
||||
searchSlug() {
|
||||
|
@ -127,8 +153,9 @@ export default {
|
|||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
selected(slug) {
|
||||
this.$emit("selected", slug);
|
||||
selected(slug, name) {
|
||||
console.log("Selected", slug, name);
|
||||
this.$emit("selected", slug, name);
|
||||
},
|
||||
async onFocus() {
|
||||
clearTimeout(this.timeout);
|
||||
|
|
|
@ -1,41 +1,21 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" height="100%" max-width="1200">
|
||||
<v-card min-height="725" height="100%">
|
||||
<v-dialog v-model="dialog" width="600px" height="0">
|
||||
<v-card>
|
||||
<v-app-bar dark color="primary">
|
||||
<v-toolbar-title class="headline">Search a Recipe</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
<v-card-text>
|
||||
<v-card-title></v-card-title>
|
||||
<v-row justify="center">
|
||||
<v-col cols="1"> </v-col>
|
||||
<v-col>
|
||||
<SearchBar @results="updateResults" :show-results="false" />
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn icon>
|
||||
<v-icon large> mdi-filter </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="searchResults">
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="item in searchResults.slice(0, 24)"
|
||||
:key="item.item.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:route="false"
|
||||
:name="item.item.name"
|
||||
:description="item.item.description"
|
||||
:slug="item.item.slug"
|
||||
:rating="item.item.rating"
|
||||
:image="item.item.image"
|
||||
@click="emitSelect(item.item.name, item.item.slug)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<SearchBar
|
||||
@results="updateResults"
|
||||
@selected="emitSelect"
|
||||
:show-results="true"
|
||||
max-width="550px"
|
||||
:dense="false"
|
||||
:nav-on-click="false"
|
||||
:reset-search="dialog"
|
||||
:solo="false"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
@ -44,11 +24,9 @@
|
|||
|
||||
<script>
|
||||
import SearchBar from "./SearchBar";
|
||||
import RecipeCard from "../../Recipe/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
RecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -60,7 +38,7 @@ export default {
|
|||
updateResults(results) {
|
||||
this.searchResults = results;
|
||||
},
|
||||
emitSelect(name, slug) {
|
||||
emitSelect(slug, name) {
|
||||
this.$emit("select", name, slug);
|
||||
this.dialog = false;
|
||||
},
|
||||
|
@ -72,4 +50,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.v-dialog__content {
|
||||
margin-top: 10%;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
|
@ -133,7 +133,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
categories() {
|
||||
return this.$store.getters.getCategories;
|
||||
return this.$store.getters.getAllCategories;
|
||||
},
|
||||
isFlat() {
|
||||
return this.groupSettings.categories >= 1 ? true : false;
|
||||
|
|
|
@ -201,6 +201,7 @@ export default {
|
|||
this.$store.commit("setToken", newKey.access_token);
|
||||
this.refreshProfile();
|
||||
this.loading = false;
|
||||
this.$store.dispatch("requestUserData")
|
||||
},
|
||||
async changePassword() {
|
||||
this.paswordLoading = true;
|
||||
|
|
|
@ -59,7 +59,7 @@ export default {
|
|||
});
|
||||
},
|
||||
async getRecipeByCategory(category) {
|
||||
return await api.categories.get_recipes_in_category(category);
|
||||
return await api.categories.getRecipesInCategory(category);
|
||||
},
|
||||
getRecentRecipes() {
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-card id="myRecipe">
|
||||
<v-card
|
||||
v-if="skeleton"
|
||||
:color="`white ${theme.isDark ? 'darken-2' : 'lighten-4'}`"
|
||||
class="pa-3"
|
||||
>
|
||||
<v-skeleton-loader
|
||||
class="mx-auto"
|
||||
height="700px"
|
||||
type="card"
|
||||
></v-skeleton-loader>
|
||||
</v-card>
|
||||
<v-card v-else id="myRecipe">
|
||||
<v-img
|
||||
height="400"
|
||||
:src="getImage(recipeDetails.image)"
|
||||
|
@ -77,8 +88,14 @@ export default {
|
|||
RecipeTimeCard,
|
||||
},
|
||||
mixins: [user],
|
||||
inject: {
|
||||
theme: {
|
||||
default: { isDark: false },
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
skeleton: true,
|
||||
// currentRecipe: this.$route.params.recipe,
|
||||
form: false,
|
||||
jsonEditor: false,
|
||||
|
@ -138,6 +155,7 @@ export default {
|
|||
},
|
||||
async getRecipeDetails() {
|
||||
this.recipeDetails = await api.recipes.requestDetails(this.currentRecipe);
|
||||
this.skeleton = false;
|
||||
this.form = false;
|
||||
},
|
||||
getImage(image) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async getRecipes() {
|
||||
let data = await api.categories.get_recipes_in_category(
|
||||
let data = await api.categories.getRecipesInCategory(
|
||||
this.currentCategory
|
||||
);
|
||||
this.title = data.name;
|
||||
|
|
|
@ -76,7 +76,7 @@ export default {
|
|||
});
|
||||
},
|
||||
async getRecipeByCategory(category) {
|
||||
return await api.categories.get_recipes_in_category(category);
|
||||
return await api.categories.getRecipesInCategory(category);
|
||||
},
|
||||
filterRecipe(slug) {
|
||||
const storeCategory = this.recipeStore.find(
|
||||
|
|
|
@ -27,19 +27,22 @@ const store = new Vuex.Store({
|
|||
allRecipes: [],
|
||||
mealPlanCategories: [],
|
||||
allCategories: [],
|
||||
allTags: [],
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setRecentRecipes(state, payload) {
|
||||
state.recentRecipes = payload;
|
||||
},
|
||||
|
||||
setMealPlanCategories(state, payload) {
|
||||
state.mealPlanCategories = payload;
|
||||
},
|
||||
setAllCategories(state, payload) {
|
||||
state.allCategories = payload;
|
||||
},
|
||||
setAllTags(state, payload) {
|
||||
state.allTags = payload;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -60,12 +63,19 @@ const store = new Vuex.Store({
|
|||
const categories = await api.categories.getAll();
|
||||
commit("setAllCategories", categories);
|
||||
},
|
||||
async requestTags({ commit }) {
|
||||
const tags = await api.tags.getAll();
|
||||
commit("setAllTags", tags);
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
getRecentRecipes: state => state.recentRecipes,
|
||||
getMealPlanCategories: state => state.mealPlanCategories,
|
||||
getAllCategories: state => state.allCategories,
|
||||
getAllCategories: state =>
|
||||
state.allCategories.sort((a, b) => (a.slug > b.slug ? 1 : -1)),
|
||||
getAllTags: state =>
|
||||
state.allTags.sort((a, b) => (a.slug > b.slug ? 1 : -1)),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -54,6 +54,11 @@ const mutations = {
|
|||
};
|
||||
|
||||
const actions = {
|
||||
async requestUserData({ commit }) {
|
||||
const userData = await api.users.self();
|
||||
commit("setUserData", userData);
|
||||
},
|
||||
|
||||
async resetTheme({ commit }) {
|
||||
const defaultTheme = await api.themes.requestByName("default");
|
||||
if (defaultTheme.colors) {
|
||||
|
|
5
makefile
5
makefile
|
@ -56,10 +56,14 @@ backend: ## Start Mealie Backend Development Server
|
|||
poetry run python mealie/db/init_db.py && \
|
||||
poetry run python mealie/app.py
|
||||
|
||||
|
||||
.PHONY: frontend
|
||||
frontend: ## Start Mealie Frontend Development Server
|
||||
cd frontend && npm run serve
|
||||
|
||||
frontend-build: ## Build Frontend in frontend/dist
|
||||
cd frontned && npm run build
|
||||
|
||||
.PHONY: docs
|
||||
docs: ## Start Mkdocs Development Server
|
||||
poetry run python dev/scripts/api_docs_gen.py && \
|
||||
|
@ -71,7 +75,6 @@ docker-dev: ## Build and Start Docker Development Stack
|
|||
docker-prod: ## Build and Start Docker Production Stack
|
||||
docker-compose -p mealie up --build -d
|
||||
|
||||
|
||||
code-gen: ## Run Code-Gen Scripts
|
||||
poetry run python dev/scripts/app_routes_gen.py
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from mealie.db.models.model_base import SqlAlchemyBase
|
||||
from fastapi.logger import logger
|
||||
from mealie.db.models.model_base import SqlAlchemyBase
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm import validates
|
||||
|
||||
|
@ -46,7 +46,7 @@ class Category(SqlAlchemyBase):
|
|||
assert name != ""
|
||||
return name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
def __init__(self, name, session=None) -> None:
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(name)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depend
|
|||
def available_imports():
|
||||
"""Returns a list of avaiable .zip files for import into Mealie."""
|
||||
imports = []
|
||||
for archive in app_dirs.app_dirs.BACKUP_DIR.glob("*.zip"):
|
||||
for archive in app_dirs.BACKUP_DIR.glob("*.zip"):
|
||||
backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
|
||||
imports.append(backup)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from mealie.schema.recipe import AllRecipeRequest
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -75,7 +75,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
|
|||
""" pass a list of categories and get a list of recipes associated with those categories """
|
||||
# ! This should be refactored into a single database call, but I couldn't figure it out
|
||||
in_category = [db.categories.get(session, slugify(cat), limit=1) for cat in categories]
|
||||
in_category = [cat.get("recipes") for cat in in_category if cat]
|
||||
in_category = [cat.recipes for cat in in_category if cat]
|
||||
in_category = [item for sublist in in_category for item in sublist]
|
||||
return in_category
|
||||
|
||||
|
@ -85,6 +85,6 @@ async def filter_by_tags(tags: list, session: Session = Depends(generate_session
|
|||
""" pass a list of tags and get a list of recipes associated with those tags"""
|
||||
# ! This should be refactored into a single database call, but I couldn't figure it out
|
||||
in_tags = [db.tags.get(session, slugify(tag), limit=1) for tag in tags]
|
||||
in_tags = [tag.get("recipes") for tag in in_tags]
|
||||
in_tags = [tag.recipes for tag in in_tags]
|
||||
in_tags = [item for sublist in in_tags for item in sublist]
|
||||
return in_tags
|
||||
|
|
|
@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends
|
|||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.category import RecipeCategoryResponse
|
||||
from mealie.schema.category import CategoryIn, RecipeCategoryResponse
|
||||
from mealie.schema.snackbar import SnackResponse
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
@ -18,6 +18,15 @@ async def get_all_recipe_categories(session: Session = Depends(generate_session)
|
|||
return db.categories.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_recipe_category(
|
||||
category: CategoryIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
|
||||
):
|
||||
""" Creates a Category in the database """
|
||||
|
||||
return db.categories.create(session, category.dict())
|
||||
|
||||
|
||||
@router.get("/{category}", response_model=RecipeCategoryResponse)
|
||||
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)):
|
||||
""" Returns a list of recipes associated with the provided category. """
|
||||
|
|
|
@ -8,15 +8,15 @@ from sqlalchemy.orm.session import Session
|
|||
router = APIRouter(tags=["Recipes"])
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/recipes/tags",
|
||||
prefix="/api/tags",
|
||||
tags=["Recipe Tags"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("")
|
||||
async def get_all_recipe_tags(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of available tags in the database """
|
||||
return db.tags.get_all_primary_keys(session)
|
||||
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@router.get("/{tag}")
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
from mealie.schema.recipe import Recipe
|
||||
|
||||
|
||||
class CategoryBase(CamelModel):
|
||||
id: int
|
||||
class CategoryIn(CamelModel):
|
||||
name: str
|
||||
|
||||
|
||||
class CategoryBase(CategoryIn):
|
||||
id: int
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# Make .env in this folder if needed.
|
||||
DEFAULT_GROUP=Home
|
||||
ENV=False
|
||||
API_PORT=9000
|
||||
|
@ -5,4 +6,4 @@ API_DOCS=True
|
|||
DB_TYPE='sqlite'
|
||||
DEFAULT_PASSWORD=MyPassword
|
||||
SFTP_USERNAME=None
|
||||
SFTP_PASSWORD=None
|
||||
SFTP_PASSWORD=None
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue