backend-events + code-cleanup (#395)
* additional server events * sort 'recent recipes' by updated * remove duplicate code * fixes #396 * set color * consolidate tag/category pages * set colors * list unorganized recipes * cleanup old code * remove flash message, switch to global snackbar * cancel to close * cleanup Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
96919319b1
commit
466997febc
31 changed files with 1604 additions and 686 deletions
|
@ -59,7 +59,7 @@
|
|||
- All images are now converted to .webp for better compression
|
||||
|
||||
### General
|
||||
- New 'Dark' Theme Packages with Mealie
|
||||
- New 'Dark' Color Theme Packaged with Mealie
|
||||
- Updated Recipe Card Sections Toolbar
|
||||
- New Sort Options (They work this time!)
|
||||
- Alphabetical
|
||||
|
@ -82,6 +82,7 @@
|
|||
- Improved styling for search bar in desktop
|
||||
- Improved search layout on mobile
|
||||
- Profile image now shown on all sidebars
|
||||
- Switched from Flash Messages to Snackbar (Removed dependency
|
||||
|
||||
### Behind the Scenes
|
||||
- Black and Flake8 now run as CI/CD checks
|
||||
|
|
1884
frontend/package-lock.json
generated
1884
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,7 +10,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@adapttive/vue-markdown": "^4.0.1",
|
||||
"@smartweb/vue-flash-message": "^0.6.10",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.9.1",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
|
@ -31,7 +30,7 @@
|
|||
"@mdi/font": "^5.9.55",
|
||||
"@vue/cli-plugin-babel": "^4.5.11",
|
||||
"@vue/cli-plugin-eslint": "^4.5.11",
|
||||
"@vue/cli-service": "^4.1.1",
|
||||
"@vue/cli-service": "^4.5.12",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
<v-banner v-if="demo" sticky
|
||||
><div class="text-center"><b> This is a Demo</b> | Username: changeme@email.com | Password: demo</div></v-banner
|
||||
>
|
||||
<GlobalSnackbar />
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
<FlashMessage :position="'right bottom'"></FlashMessage>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheAppBar from "@/components/UI/TheAppBar";
|
||||
import GlobalSnackbar from "@/components/UI/GlobalSnackbar";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
import { user } from "@/mixins/user";
|
||||
|
||||
|
@ -22,6 +23,7 @@ export default {
|
|||
|
||||
components: {
|
||||
TheAppBar,
|
||||
GlobalSnackbar,
|
||||
},
|
||||
|
||||
mixins: [user],
|
||||
|
@ -71,38 +73,6 @@ export default {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.notify-info-color {
|
||||
border: 1px, solid, var(--v-info-base) !important;
|
||||
border-left: 3px, solid, var(--v-info-base) !important;
|
||||
background-color: var(--v-info-base) !important;
|
||||
}
|
||||
|
||||
.notify-warning-color {
|
||||
border: 1px, solid, var(--v-warning-base) !important;
|
||||
border-left: 3px, solid, var(--v-warning-base) !important;
|
||||
background-color: var(--v-warning-base) !important;
|
||||
}
|
||||
|
||||
.notify-error-color {
|
||||
border: 1px, solid, var(--v-error-base) !important;
|
||||
border-left: 3px, solid, var(--v-error-base) !important;
|
||||
background-color: var(--v-error-base) !important;
|
||||
}
|
||||
|
||||
.notify-success-color {
|
||||
border: 1px, solid, var(--v-success-base) !important;
|
||||
border-left: 3px, solid, var(--v-success-base) !important;
|
||||
background-color: var(--v-success-base) !important;
|
||||
}
|
||||
|
||||
.notify-base {
|
||||
color: white !important;
|
||||
/* min-height: 50px; */
|
||||
margin-right: 60px;
|
||||
margin-bottom: -5px;
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ const recipeURLs = {
|
|||
createAsset: slug => `${prefix}${slug}/assets`,
|
||||
recipeImage: slug => `${prefix}${slug}/image`,
|
||||
updateImage: slug => `${prefix}${slug}/image`,
|
||||
untagged: prefix + "summary/untagged",
|
||||
uncategorized: prefix + "summary/uncategorized ",
|
||||
};
|
||||
|
||||
export const recipeAPI = {
|
||||
|
@ -134,6 +136,16 @@ export const recipeAPI = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
async allUntagged() {
|
||||
const response = await apiReq.get(recipeURLs.untagged);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async allUnategorized() {
|
||||
const response = await apiReq.get(recipeURLs.uncategorized);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
recipeImage(recipeSlug) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/original.webp`;
|
||||
},
|
||||
|
|
30
frontend/src/components/UI/GlobalSnackbar.vue
Normal file
30
frontend/src/components/UI/GlobalSnackbar.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div class="text-center ma-2">
|
||||
<v-snackbar v-model="snackbar.open" top :color="snackbar.color" timeout="3500">
|
||||
{{ snackbar.title }}
|
||||
{{ snackbar.text }}
|
||||
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn text v-bind="attrs" @click="snackbar.open = false">
|
||||
{{ $t("general.close") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({}),
|
||||
computed: {
|
||||
snackbar: {
|
||||
set(val) {
|
||||
this.$store.commit("setSnackbar", val);
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.getSnackbar;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -106,7 +106,7 @@ export default {
|
|||
this.processing = false;
|
||||
},
|
||||
isValidWebUrl(url) {
|
||||
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||
return regEx.test(url) ? true : "Must be a Valid URL";
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,11 +5,9 @@ import store from "./store";
|
|||
import VueRouter from "vue-router";
|
||||
import { router } from "./routes";
|
||||
import i18n from "./i18n";
|
||||
import FlashMessage from "@smartweb/vue-flash-message";
|
||||
import "@mdi/font/css/materialdesignicons.css";
|
||||
import "typeface-roboto/index.css";
|
||||
|
||||
Vue.use(FlashMessage);
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(VueRouter);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{$t('settings.backup-and-exports')}}
|
||||
{{ $t("settings.backup-and-exports") }}
|
||||
</h2>
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
|
@ -23,15 +23,15 @@
|
|||
<div class="d-flex row py-3 justify-end">
|
||||
<TheUploadBtn url="/api/backups/upload" @uploaded="getAvailableBackups">
|
||||
<template v-slot="{ isSelecting, onButtonClick }">
|
||||
<v-btn :loading="isSelecting" class="mx-2" small :color="color" @click="onButtonClick">
|
||||
<v-icon left> mdi-cloud-upload </v-icon> {{$t('general.upload')}}
|
||||
<v-btn :loading="isSelecting" class="mx-2" small color="info" @click="onButtonClick">
|
||||
<v-icon left> mdi-cloud-upload </v-icon> {{ $t("general.upload") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</TheUploadBtn>
|
||||
<BackupDialog :color="color" />
|
||||
|
||||
<v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup">
|
||||
<v-icon left> mdi-plus </v-icon> {{$t('general.create')}}
|
||||
<v-btn :loading="loading" class="mx-2" small color="success" @click="createBackup">
|
||||
<v-icon left> mdi-plus </v-icon> {{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<template v-slot:bottom>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{$t('settings.events')}}
|
||||
{{ $t("settings.events") }}
|
||||
</h2>
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
|
@ -13,8 +13,8 @@
|
|||
</div>
|
||||
</template>
|
||||
<div class="d-flex row py-3 justify-end">
|
||||
<v-btn class="mx-2" small :color="color" @click="deleteAll">
|
||||
<v-icon left> mdi-notification-clear-all </v-icon> {{$t('general.clear')}}
|
||||
<v-btn class="mx-2" small color="error lighten-1" @click="deleteAll">
|
||||
<v-icon left> mdi-notification-clear-all </v-icon> {{ $t("general.clear") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<template v-slot:bottom>
|
||||
|
@ -69,7 +69,7 @@ export default {
|
|||
color: "primary",
|
||||
},
|
||||
backup: {
|
||||
icon: "mdi-backup-restore",
|
||||
icon: "mdi-database",
|
||||
color: "primary",
|
||||
},
|
||||
schedule: {
|
||||
|
@ -80,9 +80,13 @@ export default {
|
|||
icon: "mdi-database-import",
|
||||
color: "primary",
|
||||
},
|
||||
signup: {
|
||||
user: {
|
||||
icon: "mdi-account",
|
||||
color: "primary",
|
||||
color: "accent",
|
||||
},
|
||||
group: {
|
||||
icon: "mdi-account-group-outline",
|
||||
color: "accent",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer class="mx-2"></v-spacer>
|
||||
<v-btn class="my-1 mb-n1" :color="color" @click="createTheme">
|
||||
<v-btn class="my-1 mb-n1" color="success" @click="createTheme">
|
||||
<v-icon left> mdi-plus </v-icon> {{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
:top="true"
|
||||
>
|
||||
<template v-slot:open="{ open }">
|
||||
<v-btn color="primary" class="mr-1" small @click="open">
|
||||
<v-btn color="info" class="mr-1" small @click="open">
|
||||
<v-icon left>mdi-lock</v-icon>
|
||||
Change Password
|
||||
</v-btn>
|
||||
|
@ -99,7 +99,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<v-card outlined class="mt-n1">
|
||||
<base-dialog
|
||||
<BaseDialog
|
||||
ref="renameDialog"
|
||||
title-icon="mdi-tag"
|
||||
:title="renameTarget.title"
|
||||
:submit-text="$t('general.update')"
|
||||
modal-width="800"
|
||||
@submit="renameFromDialog(renameTarget.slug, renameTarget.newName)"
|
||||
>
|
||||
|
@ -32,7 +33,7 @@
|
|||
:tags="recipe.tags"
|
||||
/>
|
||||
</template>
|
||||
</base-dialog>
|
||||
</BaseDialog>
|
||||
|
||||
<div class="d-flex justify-center align-center pa-2 flex-wrap">
|
||||
<new-category-tag-dialog ref="newDialog" :tag-dialog="isTags">
|
||||
|
|
|
@ -13,38 +13,45 @@
|
|||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-spacer v-if="!isMobile"> </v-spacer>
|
||||
|
||||
<FuseSearchBar :raw-data="allItems" @results="filterItems" :search="searchString">
|
||||
<v-text-field
|
||||
v-model="searchString"
|
||||
clearable
|
||||
solo
|
||||
dense
|
||||
class="mx-2"
|
||||
hide-details
|
||||
single-line
|
||||
:placeholder="$t('search.search')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
>
|
||||
</v-text-field>
|
||||
</FuseSearchBar>
|
||||
</div>
|
||||
<v-card-text>
|
||||
<CardSection :sortable="true" title="Unorganized" :recipes="shownRecipes" @sort="assignSorted" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
|
||||
import { api } from "@/api";
|
||||
import CardSection from "@/components/UI/CardSection";
|
||||
export default {
|
||||
components: { FuseSearchBar },
|
||||
components: {
|
||||
// FuseSearchBar,
|
||||
CardSection,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buttonToggle: 0,
|
||||
allItems: [],
|
||||
tagRecipes: [],
|
||||
categoryRecipes: [],
|
||||
searchString: "",
|
||||
searchResults: [],
|
||||
sortedResults: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shownRecipes() {
|
||||
if (this.sortedResults.length > 0) {
|
||||
return this.sortedResults;
|
||||
} else {
|
||||
switch (this.filter) {
|
||||
case "category":
|
||||
return this.categoryRecipes;
|
||||
case "tag":
|
||||
return this.tagRecipes;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
|
@ -60,10 +67,22 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.refreshUnorganized();
|
||||
},
|
||||
methods: {
|
||||
filterItems(val) {
|
||||
this.searchResults = val.map(x => x.item);
|
||||
},
|
||||
async refreshUnorganized() {
|
||||
this.loading = true;
|
||||
this.tagRecipes = await api.recipes.allUntagged();
|
||||
this.categoryRecipes = await api.recipes.allUnategorized();
|
||||
this.loading = false;
|
||||
},
|
||||
assignSorted(val) {
|
||||
this.sortedResults = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -22,6 +22,12 @@ export default {
|
|||
currentCategory() {
|
||||
return this.$route.params.category;
|
||||
},
|
||||
currentTag() {
|
||||
return this.$route.params.tag;
|
||||
},
|
||||
TagOrCategory() {
|
||||
return this.currentCategory || this.currentTag;
|
||||
},
|
||||
shownRecipes() {
|
||||
if (this.sortedResults.length > 0) {
|
||||
return this.sortedResults;
|
||||
|
@ -31,7 +37,7 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
async currentCategory() {
|
||||
async TagOrCategory() {
|
||||
this.sortedResults = [];
|
||||
this.getRecipes();
|
||||
},
|
||||
|
@ -42,7 +48,14 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async getRecipes() {
|
||||
let data = await api.categories.getRecipesInCategory(this.currentCategory);
|
||||
if (!this.TagOrCategory === null) return;
|
||||
|
||||
let data = {};
|
||||
if (this.currentCategory) {
|
||||
data = await api.categories.getRecipesInCategory(this.TagOrCategory);
|
||||
} else {
|
||||
data = await api.tags.getRecipesInTag(this.TagOrCategory);
|
||||
}
|
||||
this.title = data.name;
|
||||
this.recipes = data.recipes;
|
||||
},
|
|
@ -69,10 +69,7 @@ export default {
|
|||
methods: {
|
||||
async buildPage() {
|
||||
this.page = await api.siteSettings.getPage(this.pageSlug);
|
||||
},
|
||||
filterRecipe(slug) {
|
||||
const storeCategory = this.recipeStore.find(element => element.slug === slug);
|
||||
return storeCategory ? storeCategory.recipes : [];
|
||||
this.tab = this.page.categories[0];
|
||||
},
|
||||
sortRecipes(sortedRecipes, destKey) {
|
||||
this.page.categories[destKey].recipes = sortedRecipes;
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<CardSection :sortable="true" :title="title" :recipes="shownRecipes" @sort="assignSorted" />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import CardSection from "@/components/UI/CardSection";
|
||||
export default {
|
||||
components: {
|
||||
CardSection,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
recipes: [],
|
||||
sortedResults: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentTag() {
|
||||
return this.$route.params.tag;
|
||||
},
|
||||
shownRecipes() {
|
||||
if (this.sortedResults.length > 0) {
|
||||
return this.sortedResults;
|
||||
} else {
|
||||
return this.recipes;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async currentTag() {
|
||||
this.getRecipes();
|
||||
this.sortedResults = [];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getRecipes();
|
||||
this.sortedResults = [];
|
||||
},
|
||||
methods: {
|
||||
async getRecipes() {
|
||||
let data = await api.tags.getRecipesInTag(this.currentTag);
|
||||
this.title = data.name;
|
||||
this.recipes = data.recipes;
|
||||
},
|
||||
assignSorted(val) {
|
||||
console.log(val);
|
||||
this.sortedResults = val.slice();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -2,15 +2,14 @@ import ViewRecipe from "@/pages/Recipe/ViewRecipe";
|
|||
import NewRecipe from "@/pages/Recipe/NewRecipe";
|
||||
import CustomPage from "@/pages/Recipes/CustomPage";
|
||||
import AllRecipes from "@/pages/Recipes/AllRecipes";
|
||||
import CategoryPage from "@/pages/Recipes/CategoryPage";
|
||||
import TagPage from "@/pages/Recipes/TagPage";
|
||||
import CategoryTagPage from "@/pages/Recipes/CategoryTagPage";
|
||||
import { api } from "@/api";
|
||||
|
||||
export const recipeRoutes = [
|
||||
// Recipes
|
||||
{ path: "/recipes/all", component: AllRecipes },
|
||||
{ path: "/recipes/tag/:tag", component: TagPage },
|
||||
{ path: "/recipes/category/:category", component: CategoryPage },
|
||||
{ path: "/recipes/tag/:tag", component: CategoryTagPage },
|
||||
{ path: "/recipes/category/:category", component: CategoryTagPage },
|
||||
// Misc
|
||||
{ path: "/new/", component: NewRecipe },
|
||||
{ path: "/pages/:customPage", component: CustomPage },
|
||||
|
|
|
@ -7,6 +7,7 @@ import language from "./modules/language";
|
|||
import siteSettings from "./modules/siteSettings";
|
||||
import recipes from "./modules/recipes";
|
||||
import groups from "./modules/groups";
|
||||
import snackbar from "./modules/snackbar";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
@ -22,6 +23,7 @@ const store = new Vuex.Store({
|
|||
siteSettings,
|
||||
groups,
|
||||
recipes,
|
||||
snackbar,
|
||||
},
|
||||
state: {
|
||||
// All Recipe Data Store
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { api } from "@/api";
|
||||
import Vue from "vue";
|
||||
import { recipe } from "@/utils/recipe";
|
||||
|
||||
const state = {
|
||||
recentRecipes: [],
|
||||
|
@ -36,7 +37,6 @@ const mutations = {
|
|||
const actions = {
|
||||
async requestRecentRecipes() {
|
||||
const payload = await api.recipes.allSummary(0, 30);
|
||||
payload.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||
const hash = Object.fromEntries(payload.map(e => [e.id, e]));
|
||||
this.commit("setRecentRecipes", hash);
|
||||
},
|
||||
|
@ -60,7 +60,11 @@ const actions = {
|
|||
const getters = {
|
||||
getAllRecipes: state => Object.values(state.allRecipes),
|
||||
getAllRecipesHash: state => state.allRecipes,
|
||||
getRecentRecipes: state => Object.values(state.recentRecipes),
|
||||
getRecentRecipes: state => {
|
||||
let list = Object.values(state.recentRecipes);
|
||||
recipe.sortByUpdated(list);
|
||||
return list;
|
||||
},
|
||||
getRecentRecipesHash: state => state.recentRecipes,
|
||||
};
|
||||
|
||||
|
|
23
frontend/src/store/modules/snackbar.js
Normal file
23
frontend/src/store/modules/snackbar.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
const state = {
|
||||
snackbar: {
|
||||
open: false,
|
||||
text: "Hello From The Store",
|
||||
color: "info",
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
setSnackbar(state, payload) {
|
||||
state.snackbar = payload;
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getSnackbar: state => state.snackbar,
|
||||
};
|
||||
|
||||
export default {
|
||||
state,
|
||||
mutations,
|
||||
getters,
|
||||
};
|
|
@ -1,14 +1,7 @@
|
|||
import { vueApp } from "../main";
|
||||
import { recipe } from "@/utils/recipe";
|
||||
import { store } from "@/store";
|
||||
|
||||
// TODO: Migrate to Mixins
|
||||
const notifyHelpers = {
|
||||
baseCSS: "notify-base",
|
||||
error: "notify-error-color",
|
||||
warning: "notify-warning-color",
|
||||
success: "notify-success-color",
|
||||
info: "notify-info-color",
|
||||
};
|
||||
|
||||
export const utils = {
|
||||
recipe: recipe,
|
||||
|
@ -27,27 +20,37 @@ export const utils = {
|
|||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
notify: {
|
||||
show: function(text, type = "info", title = null) {
|
||||
vueApp.flashMessage.show({
|
||||
status: type,
|
||||
info: function(text, title = null) {
|
||||
store.commit("setSnackbar", {
|
||||
open: true,
|
||||
title: title,
|
||||
message: text,
|
||||
time: 3000,
|
||||
blockClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
|
||||
contentClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
|
||||
text: text,
|
||||
color: "info",
|
||||
});
|
||||
},
|
||||
info: function(text, title = null) {
|
||||
this.show(text, "info", title);
|
||||
},
|
||||
success: function(text, title = null) {
|
||||
this.show(text, "success", title);
|
||||
store.commit("setSnackbar", {
|
||||
open: true,
|
||||
title: title,
|
||||
text: text,
|
||||
color: "success",
|
||||
});
|
||||
},
|
||||
error: function(text, title = null) {
|
||||
this.show(text, "error", title);
|
||||
store.commit("setSnackbar", {
|
||||
open: true,
|
||||
title: title,
|
||||
text: text,
|
||||
color: "error",
|
||||
});
|
||||
},
|
||||
warning: function(text, title = null) {
|
||||
this.show(text, "warning", title);
|
||||
store.commit("setSnackbar", {
|
||||
open: true,
|
||||
title: title,
|
||||
text: text,
|
||||
color: "warning",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -90,7 +90,7 @@ def import_database(file_name: str, import_data: ImportJob, session: Session = D
|
|||
force_import=import_data.force,
|
||||
rebase=import_data.rebase,
|
||||
)
|
||||
create_backup_event("Database Restore", f"Restored Database File {file_name}", session)
|
||||
create_backup_event("Database Restore", f"Restore File: {file_name}", session)
|
||||
return db_import
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from fastapi import APIRouter, Depends, status, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
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.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
|
||||
from mealie.services.events import create_group_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/groups", tags=["Groups"])
|
||||
|
@ -39,6 +40,7 @@ async def create_group(
|
|||
|
||||
try:
|
||||
db.groups.create(session, group_data.dict())
|
||||
create_group_event("Group Created", f"'{group_data.name}' created")
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
@ -68,7 +70,8 @@ async def delete_user_group(
|
|||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_NOT_FOUND")
|
||||
|
||||
if not group.users == []:
|
||||
if group.users != []:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS")
|
||||
|
||||
create_group_event("Group Deleted", f"'{group.name}' Deleted")
|
||||
db.groups.delete(session, id)
|
||||
|
|
|
@ -4,6 +4,7 @@ from mealie.db.db_setup import generate_session
|
|||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.meal import MealPlanIn, MealPlanInDB
|
||||
from mealie.schema.user import GroupInDB, UserInDB
|
||||
from mealie.services.events import create_group_event
|
||||
from mealie.services.image import image
|
||||
from mealie.services.meal_services import get_todays_meal, process_meals
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -24,10 +25,11 @@ def get_all_meals(
|
|||
|
||||
@router.post("/create", status_code=status.HTTP_201_CREATED)
|
||||
def create_meal_plan(
|
||||
data: MealPlanIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
|
||||
data: MealPlanIn, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
""" Creates a meal plan database entry """
|
||||
processed_plan = process_meals(session, data)
|
||||
create_group_event("Meal Plan Created", f"Mealplan Created for '{current_user.group}'")
|
||||
return db.meals.create(session, processed_plan.dict())
|
||||
|
||||
|
||||
|
@ -36,23 +38,29 @@ def update_meal_plan(
|
|||
plan_id: str,
|
||||
meal_plan: MealPlanIn,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Updates a meal plan based off ID """
|
||||
processed_plan = process_meals(session, meal_plan)
|
||||
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
|
||||
try:
|
||||
db.meals.update(session, plan_id, processed_plan.dict())
|
||||
create_group_event("Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'")
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@router.delete("/{plan_id}")
|
||||
def delete_meal_plan(plan_id, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
def delete_meal_plan(
|
||||
plan_id,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Removes a meal plan from the database """
|
||||
|
||||
try:
|
||||
db.meals.delete(session, plan_id)
|
||||
create_group_event("Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'")
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
|
@ -26,17 +26,17 @@ async def get_recipe_summary(
|
|||
|
||||
"""
|
||||
|
||||
return db.recipes.get_all(session, limit=limit, start=start, override_schema=RecipeSummary)
|
||||
return db.recipes.get_all(session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
|
||||
async def get_untagged_recipes(session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_untagged(session, False, override_schema=RecipeSummary)
|
||||
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
|
||||
async def get_uncategorized_recipes(session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_uncategorized(session, False, override_schema=RecipeSummary)
|
||||
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.post("/api/recipes/category")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from mealie.core import security
|
||||
|
@ -6,6 +6,7 @@ from mealie.core.security import authenticate_user
|
|||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.user import UserInDB
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||
|
@ -14,6 +15,7 @@ router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
|||
@router.post("/token/long")
|
||||
@router.post("/token")
|
||||
def get_token(
|
||||
request: Request,
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
@ -23,6 +25,7 @@ def get_token(
|
|||
user = authenticate_user(session, email, password)
|
||||
|
||||
if not user:
|
||||
create_user_event("Failed Login", f"Username: {email}, Source IP: '{request.client.host}'")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
|
|
|
@ -9,7 +9,7 @@ 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.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from mealie.services.events import create_sign_up_event
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
|
@ -23,7 +23,7 @@ async def create_user(
|
|||
):
|
||||
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
create_sign_up_event("User Created", f"Created by {current_user.full_name}", session=session)
|
||||
create_user_event("User Created", f"Created by {current_user.full_name}", session=session)
|
||||
return db.users.create(session, new_user.dict())
|
||||
|
||||
|
||||
|
@ -150,5 +150,6 @@ async def delete_user(
|
|||
if current_user.id == id or current_user.admin:
|
||||
try:
|
||||
db.users.delete(session, id)
|
||||
create_user_event("User Deleted", f"User ID: {id}", session=session)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
|
|
@ -7,7 +7,7 @@ from mealie.db.db_setup import generate_session
|
|||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
|
||||
from mealie.schema.user import UserIn, UserInDB
|
||||
from mealie.services.events import create_sign_up_event
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
||||
|
@ -39,7 +39,7 @@ async def create_user_sign_up_key(
|
|||
"name": key_data.name,
|
||||
"admin": key_data.admin,
|
||||
}
|
||||
create_sign_up_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session)
|
||||
create_user_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session)
|
||||
return db.sign_ups.create(session, sign_up)
|
||||
|
||||
|
||||
|
@ -62,7 +62,7 @@ async def create_user_with_token(
|
|||
db.users.create(session, new_user.dict())
|
||||
|
||||
# DeleteToken
|
||||
create_sign_up_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session)
|
||||
create_user_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session)
|
||||
db.sign_ups.delete(session, token)
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ class EventCategory(str, Enum):
|
|||
backup = "backup"
|
||||
scheduled = "scheduled"
|
||||
migration = "migration"
|
||||
sign_up = "signup"
|
||||
group = "group"
|
||||
user = "user"
|
||||
|
||||
|
||||
class Event(CamelModel):
|
||||
|
|
|
@ -35,6 +35,11 @@ def create_migration_event(title, text, session=None):
|
|||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_sign_up_event(title, text, session=None):
|
||||
category = EventCategory.sign_up
|
||||
def create_group_event(title, text, session=None):
|
||||
category = EventCategory.group
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
||||
|
||||
def create_user_event(title, text, session=None):
|
||||
category = EventCategory.user
|
||||
save_event(title=title, text=text, category=category, session=session)
|
||||
|
|
Loading…
Reference in a new issue