mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:28:00 -04:00 
			
		
		
		
	Refactor/backend routers (#388)
* update router * update caddy file * setup depends in docker-fole * make changes for serving on subpath * set dev config * fix router signups * consolidate links * backup-functionality to dashboard * new user card * consolidate theme into profile * fix theme tests * fix pg tests * fix pg tests * remove unused import * mobile margin Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		
							parent
							
								
									be5ac7a17a
								
							
						
					
					
						commit
						c1370afb16
					
				
							
								
								
									
										19
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -1,28 +1,19 @@ | ||||
| { | ||||
|   "python.formatting.provider": "black", | ||||
|   "python.pythonPath": ".venv/bin/python3.9", | ||||
|   "python.linting.pylintEnabled": true, | ||||
|   "python.linting.pylintEnabled": false, | ||||
|   "python.linting.enabled": true, | ||||
|   "python.testing.unittestEnabled": false, | ||||
|   "python.testing.nosetestsEnabled": false, | ||||
|   "python.testing.pytestEnabled": true, | ||||
|   "python.testing.autoTestDiscoverOnSaveEnabled": false, | ||||
|   "python.testing.pytestArgs": ["tests"], | ||||
|   "cSpell.enableFiletypes": [ | ||||
|     "!javascript", | ||||
|     "!python", | ||||
|     "!yaml" | ||||
|   ], | ||||
|   "cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"], | ||||
|   "i18n-ally.localesPaths": "frontend/src/locales/messages", | ||||
|   "i18n-ally.sourceLanguage": "en-US", | ||||
|   "i18n-ally.enabledFrameworks": ["vue"], | ||||
|   "i18n-ally.keystyle": "nested", | ||||
|   "cSpell.words": [ | ||||
|     "compression", | ||||
|     "hkotel", | ||||
|     "performant", | ||||
|     "postgres", | ||||
|     "webp" | ||||
|   ], | ||||
|   "search.mode": "reuseEditor" | ||||
|   "cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"], | ||||
|   "search.mode": "reuseEditor", | ||||
|   "python.linting.flake8Enabled": true | ||||
| } | ||||
|  | ||||
							
								
								
									
										10
									
								
								Caddyfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Caddyfile
									
									
									
									
									
								
							| @ -6,11 +6,11 @@ | ||||
| :80 { | ||||
|   @proxied path /api/* /docs /openapi.json | ||||
|    | ||||
|   root * /app/dist | ||||
|   encode gzip | ||||
|   encode gzip zstd | ||||
|   uri strip_suffix / | ||||
|    | ||||
|   handle_path /api/recipes/media/* { | ||||
|   # Handles Recipe Images / Assets | ||||
|   handle_path /api/media/recipes/* { | ||||
|     root * /app/data/recipes/ | ||||
|     file_server | ||||
|   } | ||||
| @ -20,8 +20,8 @@ | ||||
|   } | ||||
| 
 | ||||
|   handle { | ||||
|     try_files {path}.html {path} / | ||||
|     root * /app/dist | ||||
|     try_files {path}.html {path} /index.html | ||||
|     file_server  | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										9
									
								
								Caddyfile.dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Caddyfile.dev
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| { | ||||
|   admin off | ||||
| } | ||||
| 
 | ||||
| localhost { | ||||
|   handle /mealie/* { | ||||
|     reverse_proxy http://127.0.0.1:9090  | ||||
|   } | ||||
| } | ||||
| @ -6,6 +6,8 @@ services: | ||||
|       dockerfile: Dockerfile | ||||
|     container_name: mealie | ||||
|     restart: always | ||||
|     depends_on: | ||||
|       - "postgres" | ||||
|     ports: | ||||
|       - 9090:80 | ||||
|     environment: | ||||
|  | ||||
| @ -135,14 +135,18 @@ export const recipeAPI = { | ||||
|   }, | ||||
| 
 | ||||
|   recipeImage(recipeSlug) { | ||||
|     return `/api/recipes/media/${recipeSlug}/images/original.webp`; | ||||
|     return `/api/media/recipes/${recipeSlug}/images/original.webp`; | ||||
|   }, | ||||
| 
 | ||||
|   recipeSmallImage(recipeSlug) { | ||||
|     return `/api/recipes/media/${recipeSlug}/images/min-original.webp`; | ||||
|     return `/api/media/recipes/${recipeSlug}/images/min-original.webp`; | ||||
|   }, | ||||
| 
 | ||||
|   recipeTinyImage(recipeSlug) { | ||||
|     return `/api/recipes/media/${recipeSlug}/images/tiny-original.webp`; | ||||
|     return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp`; | ||||
|   }, | ||||
| 
 | ||||
|   recipeAssetPath(recipeSlug, assetName) { | ||||
|     return `api/media/recipes/${recipeSlug}/assets/${assetName}`; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| @ -6,10 +6,10 @@ const prefix = baseURL + "themes"; | ||||
| 
 | ||||
| const settingsURLs = { | ||||
|   allThemes: `${baseURL}themes`, | ||||
|   specificTheme: themeName => `${prefix}/${themeName}`, | ||||
|   specificTheme: id => `${prefix}/${id}`, | ||||
|   createTheme: `${prefix}/create`, | ||||
|   updateTheme: themeName => `${prefix}/${themeName}`, | ||||
|   deleteTheme: themeName => `${prefix}/${themeName}`, | ||||
|   updateTheme: id => `${prefix}/${id}`, | ||||
|   deleteTheme: id => `${prefix}/${id}`, | ||||
| }; | ||||
| 
 | ||||
| export const themeAPI = { | ||||
| @ -32,22 +32,18 @@ export const themeAPI = { | ||||
|     ); | ||||
|   }, | ||||
| 
 | ||||
|   update(themeName, colors) { | ||||
|     const body = { | ||||
|       name: themeName, | ||||
|       colors: colors, | ||||
|     }; | ||||
|   update(data) { | ||||
|     return apiReq.put( | ||||
|       settingsURLs.updateTheme(themeName), | ||||
|       body, | ||||
|       settingsURLs.updateTheme(data.id), | ||||
|       data, | ||||
|       () => i18n.t("settings.theme.error-updating-theme"), | ||||
|       () => i18n.t("settings.theme.theme-updated") | ||||
|     ); | ||||
|   }, | ||||
| 
 | ||||
|   delete(themeName) { | ||||
|   delete(id) { | ||||
|     return apiReq.delete( | ||||
|       settingsURLs.deleteTheme(themeName), | ||||
|       settingsURLs.deleteTheme(id), | ||||
|       null, | ||||
|       () => i18n.t("settings.theme.error-deleting-theme"), | ||||
|       () => i18n.t("settings.theme.theme-deleted") | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
|     <div class="text-center"> | ||||
|       <h3>{{ buttonText }}</h3> | ||||
|     </div> | ||||
|     <v-text-field v-model="color" hide-details class="ma-0 pa-0" solo v-show="$vuetify.breakpoint.mdAndUp"> | ||||
|     <v-text-field v-model="color" hide-details class="ma-0 pa-0" solo > | ||||
|       <template v-slot:append> | ||||
|         <v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false"> | ||||
|           <template v-slot:activator="{ on }"> | ||||
| @ -17,15 +17,7 @@ | ||||
|         </v-menu> | ||||
|       </template> | ||||
|     </v-text-field> | ||||
|     <div class="text-center" v-show="$vuetify.breakpoint.smAndDown"> | ||||
|       <v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false"> | ||||
|         <template v-slot:activator="{ on, attrs }"> | ||||
|           <v-chip label :color="`${color}`" dark v-bind="attrs" v-on="on"> | ||||
|             {{ color }} | ||||
|           </v-chip> | ||||
|         </template> | ||||
|       </v-menu> | ||||
|     </div> | ||||
| 
 | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-checkbox | ||||
|       v-for="option in options" | ||||
|       :key="option.text" | ||||
|       v-for="(option, index) in options" | ||||
|       :key="index" | ||||
|       class="mb-n4 mt-n3" | ||||
|       dense | ||||
|       :label="option.text" | ||||
| @ -62,5 +62,3 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
| @ -14,14 +14,7 @@ | ||||
|             <v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title> | ||||
|           </v-list-item-content> | ||||
|           <v-list-item-action> | ||||
|             <v-btn | ||||
|               v-if="!edit" | ||||
|               color="primary" | ||||
|               icon | ||||
|               :href="`/api/recipes/media/${slug}/assets/${item.fileName}`" | ||||
|               target="_blank" | ||||
|               top | ||||
|             > | ||||
|             <v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top> | ||||
|               <v-icon> mdi-download</v-icon> | ||||
|             </v-btn> | ||||
|             <div v-else> | ||||
| @ -118,6 +111,9 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     assetURL(assetName) { | ||||
|       return api.recipes.recipeAssetPath(this.slug, assetName); | ||||
|     }, | ||||
|     setFileObject(obj) { | ||||
|       this.fileObject = obj; | ||||
|     }, | ||||
| @ -135,7 +131,8 @@ export default { | ||||
|       this.value.splice(index, 1); | ||||
|     }, | ||||
|     copyLink(name, fileName) { | ||||
|       const copyText = ``; | ||||
|       const assetLink = api.recipes.recipeAssetPath(this.slug, fileName); | ||||
|       const copyText = ``; | ||||
|       navigator.clipboard.writeText(copyText).then( | ||||
|         () => console.log("Copied", copyText), | ||||
|         () => console.log("Copied Failed", copyText) | ||||
|  | ||||
| @ -18,7 +18,7 @@ | ||||
|             color="secondary darken-1" | ||||
|             class="rounded-sm static" | ||||
|           > | ||||
|             {{ yields }} | ||||
|             {{ recipe.yields }} | ||||
|           </v-btn> | ||||
|         </v-col> | ||||
|         <Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" /> | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   <v-form ref="file"> | ||||
|     <input ref="uploader" class="d-none" type="file" @change="onFileChanged" /> | ||||
|     <slot v-bind="{ isSelecting, onButtonClick }"> | ||||
|       <v-btn :loading="isSelecting" @click="onButtonClick" color="accent" :text="textBtn"> | ||||
|       <v-btn :loading="isSelecting" @click="onButtonClick" :small="small" color="accent" :text="textBtn"> | ||||
|         <v-icon left> {{ icon }}</v-icon> | ||||
|         {{ text ? text : defaultText }} | ||||
|       </v-btn> | ||||
| @ -15,6 +15,9 @@ const UPLOAD_EVENT = "uploaded"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   props: { | ||||
|     small: { | ||||
|       default: false, | ||||
|     }, | ||||
|     post: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|  | ||||
| @ -1,18 +1,12 @@ | ||||
| <template> | ||||
|   <div class="mt-n5" v-if="recipes"> | ||||
|     <v-card flat class="transparent" height="60px"> | ||||
|       <v-card-text> | ||||
|         <v-row v-if="title != null"> | ||||
|           <v-col> | ||||
|             <v-btn-toggle group> | ||||
|               <v-btn text> | ||||
|                 {{ title.toUpperCase() }} | ||||
|               </v-btn> | ||||
|             </v-btn-toggle> | ||||
|           </v-col> | ||||
|   <div v-if="recipes"> | ||||
|     <v-app-bar color="transparent" flat class="mt-n1 rounded" v-if="!disableToolbar"> | ||||
|       <v-icon large left v-if="title"> | ||||
|         {{ titleIcon }} | ||||
|       </v-icon> | ||||
|       <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> | ||||
|       <v-spacer></v-spacer> | ||||
|           <v-col align="end"> | ||||
|             <v-menu offset-y v-if="sortable"> | ||||
|       <v-menu offset-y v-if="$listeners.sortRecent || $listeners.sort"> | ||||
|         <template v-slot:activator="{ on, attrs }"> | ||||
|           <v-btn-toggle group> | ||||
|             <v-btn text v-bind="attrs" v-on="on"> | ||||
| @ -21,7 +15,7 @@ | ||||
|           </v-btn-toggle> | ||||
|         </template> | ||||
|         <v-list> | ||||
|                 <v-list-item @click="$emit('sort-recent')"> | ||||
|           <v-list-item @click="$emit('sortRecent')"> | ||||
|             <v-list-item-title>{{ $t("general.recent") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|           <v-list-item @click="$emit('sort')"> | ||||
| @ -29,11 +23,8 @@ | ||||
|           </v-list-item> | ||||
|         </v-list> | ||||
|       </v-menu> | ||||
|           </v-col> | ||||
|         </v-row> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|     <div v-if="recipes"> | ||||
|     </v-app-bar> | ||||
|     <div v-if="recipes" class="mt-2"> | ||||
|       <v-row v-if="!viewScale"> | ||||
|         <v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name"> | ||||
|           <RecipeCard | ||||
| @ -91,9 +82,12 @@ export default { | ||||
|     MobileRecipeCard, | ||||
|   }, | ||||
|   props: { | ||||
|     sortable: { | ||||
|     disableToolbar: { | ||||
|       default: false, | ||||
|     }, | ||||
|     titleIcon: { | ||||
|       default: "mdi-tag-multiple-outline", | ||||
|     }, | ||||
|     title: { | ||||
|       default: null, | ||||
|     }, | ||||
|  | ||||
							
								
								
									
										142
									
								
								frontend/src/components/UI/Dialogs/BackupDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								frontend/src/components/UI/Dialogs/BackupDialog.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,142 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <BaseDialog | ||||
|       :title="$t('settings.backup.create-heading')" | ||||
|       titleIcon="mdi-database" | ||||
|       @submit="createBackup" | ||||
|       :submit-text="$t('general.create')" | ||||
|       :loading="loading" | ||||
|     > | ||||
|       <template v-slot:open="{ open }"> | ||||
|         <v-btn @click="open" class="mx-2" small :color="color"> <v-icon left> mdi-plus </v-icon> Custom </v-btn> | ||||
|       </template> | ||||
|       <v-card-text class="mt-6"> | ||||
|         <v-text-field dense :label="$t('settings.backup.backup-tag')" v-model="tag"></v-text-field> | ||||
|       </v-card-text> | ||||
|       <v-card-actions class="mt-n9 flex-wrap"> | ||||
|         <v-switch v-model="fullBackup" :label="switchLabel"></v-switch> | ||||
|         <v-spacer></v-spacer> | ||||
|       </v-card-actions> | ||||
|       <v-expand-transition> | ||||
|         <div v-if="!fullBackup"> | ||||
|           <v-card-text class="mt-n4"> | ||||
|             <v-row> | ||||
|               <v-col sm="4"> | ||||
|                 <p>{{ $t("general.options") }}</p> | ||||
|                 <ImportOptions @update-options="updateOptions" class="mt-5" /> | ||||
|               </v-col> | ||||
|               <v-col> | ||||
|                 <p>{{ $t("general.templates") }}</p> | ||||
|                 <v-checkbox | ||||
|                   v-for="template in availableTemplates" | ||||
|                   :key="template" | ||||
|                   class="mb-n4 mt-n3" | ||||
|                   dense | ||||
|                   :label="template" | ||||
|                   @click="appendTemplate(template)" | ||||
|                 ></v-checkbox> | ||||
|               </v-col> | ||||
|             </v-row> | ||||
|           </v-card-text> | ||||
|         </div> | ||||
|       </v-expand-transition> | ||||
|     </BaseDialog> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import BaseDialog from "./BaseDialog"; | ||||
| import ImportOptions from "@/components/FormHelpers/ImportOptions"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   props: { | ||||
|     color: { default: "primary" }, | ||||
|   }, | ||||
|   components: { | ||||
|     BaseDialog, | ||||
|     ImportOptions, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       tag: null, | ||||
|       fullBackup: true, | ||||
|       loading: false, | ||||
|       options: { | ||||
|         recipes: true, | ||||
|         settings: true, | ||||
|         themes: true, | ||||
|         pages: true, | ||||
|         users: true, | ||||
|         groups: true, | ||||
|       }, | ||||
|       availableTemplates: [], | ||||
|       selectedTemplates: [], | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     switchLabel() { | ||||
|       if (this.fullBackup) { | ||||
|         return this.$t("settings.backup.full-backup"); | ||||
|       } else return this.$t("settings.backup.partial-backup"); | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.resetData(); | ||||
|     this.getAvailableBackups(); | ||||
|   }, | ||||
|   methods: { | ||||
|     resetData() { | ||||
|       this.tag = null; | ||||
|       this.fullBackup = true; | ||||
|       this.loading = false; | ||||
|       this.options = { | ||||
|         recipes: true, | ||||
|         settings: true, | ||||
|         themes: true, | ||||
|         pages: true, | ||||
|         users: true, | ||||
|         groups: true, | ||||
|       }; | ||||
|       this.availableTemplates = []; | ||||
|       this.selectedTemplates = []; | ||||
|     }, | ||||
|     updateOptions(options) { | ||||
|       this.options = options; | ||||
|     }, | ||||
|     async getAvailableBackups() { | ||||
|       const response = await api.backups.requestAvailable(); | ||||
|       response.templates.forEach(element => { | ||||
|         this.availableTemplates.push(element); | ||||
|       }); | ||||
|     }, | ||||
|     async createBackup() { | ||||
|       this.loading = true; | ||||
|       const data = { | ||||
|         tag: this.tag, | ||||
|         options: { | ||||
|           recipes: this.options.recipes, | ||||
|           settings: this.options.settings, | ||||
|           pages: this.options.pages, | ||||
|           themes: this.options.themes, | ||||
|           users: this.options.users, | ||||
|           groups: this.options.groups, | ||||
|         }, | ||||
|         templates: this.selectedTemplates, | ||||
|       }; | ||||
| 
 | ||||
|       if (await api.backups.create(data)) { | ||||
|         this.$emit("created"); | ||||
|       } | ||||
|       this.loading = false; | ||||
|     }, | ||||
|     appendTemplate(templateName) { | ||||
|       if (this.selectedTemplates.includes(templateName)) { | ||||
|         let index = this.selectedTemplates.indexOf(templateName); | ||||
|         if (index !== -1) { | ||||
|           this.selectedTemplates.splice(index, 1); | ||||
|         } | ||||
|       } else this.selectedTemplates.push(templateName); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @ -3,14 +3,14 @@ | ||||
|     <slot name="open" v-bind="{ open }"> </slot> | ||||
|     <v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined"> | ||||
|       <v-card class="pb-10" height="100%"> | ||||
|         <v-app-bar dark :color="color" class="mt-n1 mb-2"> | ||||
|         <v-app-bar dark :color="color" class="mt-n1 mb-0"> | ||||
|           <v-icon large left> | ||||
|             {{ titleIcon }} | ||||
|           </v-icon> | ||||
|           <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> | ||||
|           <v-spacer></v-spacer> | ||||
|         </v-app-bar> | ||||
|         <v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear> | ||||
|         <v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear> | ||||
|         <slot> </slot> | ||||
|         <v-card-actions> | ||||
|           <slot name="card-actions"> | ||||
| @ -18,8 +18,12 @@ | ||||
|               {{ $t("general.cancel") }} | ||||
|             </v-btn> | ||||
|             <v-spacer></v-spacer> | ||||
| 
 | ||||
|             <v-btn color="error" text @click="deleteEvent" v-if="$listeners.delete"> | ||||
|               {{ $t("general.delete") }} | ||||
|             </v-btn> | ||||
|             <v-btn color="success" @click="submitEvent"> | ||||
|               {{ $t("general.submit") }} | ||||
|               {{ submitText }} | ||||
|             </v-btn> | ||||
|           </slot> | ||||
|         </v-card-actions> | ||||
| @ -31,6 +35,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import i18n from "@/i18n"; | ||||
| export default { | ||||
|   props: { | ||||
|     color: { | ||||
| @ -51,16 +56,34 @@ export default { | ||||
|     top: { | ||||
|       default: false, | ||||
|     }, | ||||
|     submitText: { | ||||
|       default: () => i18n.t("general.create"), | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       dialog: false, | ||||
|       submitted: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     determineClose() { | ||||
|       return this.submitted && !this.loading; | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     determineClose() { | ||||
|       this.submitted = false; | ||||
|       this.dialog = false; | ||||
|     }, | ||||
|     dialog(val) { | ||||
|       if (val) this.submitted = false; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     submitEvent() { | ||||
|       this.$emit("submit"); | ||||
|       this.close(); | ||||
|       this.submitted = true; | ||||
|     }, | ||||
|     open() { | ||||
|       this.dialog = true; | ||||
| @ -68,6 +91,10 @@ export default { | ||||
|     close() { | ||||
|       this.dialog = false; | ||||
|     }, | ||||
|     deleteEvent() { | ||||
|       this.$emit("delete"); | ||||
|       this.submitted = true; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| @ -48,7 +48,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ImportOptions from "./ImportOptions"; | ||||
| import ImportOptions from "@/components/FormHelpers/ImportOptions"; | ||||
| import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue"; | ||||
| import { backupURLs } from "@/api/backup"; | ||||
| export default { | ||||
| @ -1,6 +1,7 @@ | ||||
| w<template> | ||||
|   <v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3"> | ||||
|     <div class="d-flex grow flex-wrap"> | ||||
|       <slot name="avatar"> | ||||
|         <v-sheet | ||||
|           :color="color" | ||||
|           :max-height="icon ? 90 : undefined" | ||||
| @ -12,6 +13,7 @@ w<template> | ||||
|           <v-icon v-if="icon" size="40" v-text="icon" /> | ||||
|           <div v-if="text" class="headline font-weight-thin" v-text="text" /> | ||||
|         </v-sheet> | ||||
|       </slot> | ||||
| 
 | ||||
|       <div v-if="$slots['after-heading']" class="ml-auto"> | ||||
|         <slot name="after-heading" /> | ||||
| @ -29,7 +31,7 @@ w<template> | ||||
|     </template> | ||||
| 
 | ||||
|     <template v-if="$slots.bottom"> | ||||
|       <v-divider class="mt-2" /> | ||||
|       <v-divider class="mt-2" v-if="!$slots.actions" /> | ||||
| 
 | ||||
|       <div class="pb-0"> | ||||
|         <slot name="bottom" /> | ||||
| @ -73,6 +75,7 @@ export default { | ||||
|     classes() { | ||||
|       return { | ||||
|         "v-card--material--has-heading": this.hasHeading, | ||||
|         "mt-3": this.$vuetify.breakpoint.name == "xs" || this.$vuetify.breakpoint.name == "sm" | ||||
|       }; | ||||
|     }, | ||||
|     hasHeading() { | ||||
| @ -133,11 +133,6 @@ export default { | ||||
|           to: "/admin/profile", | ||||
|           title: this.$t("settings.profile"), | ||||
|         }, | ||||
|         { | ||||
|           icon: "mdi-format-color-fill", | ||||
|           to: "/admin/themes", | ||||
|           title: this.$t("general.themes"), | ||||
|         }, | ||||
|         { | ||||
|           icon: "mdi-food", | ||||
|           to: "/admin/meal-planner", | ||||
| @ -167,11 +162,6 @@ export default { | ||||
|           to: "/admin/manage-users", | ||||
|           title: this.$t("settings.manage-users"), | ||||
|         }, | ||||
|         { | ||||
|           icon: "mdi-backup-restore", | ||||
|           to: "/admin/backups", | ||||
|           title: this.$t("settings.backup-and-exports"), | ||||
|         }, | ||||
|         { | ||||
|           icon: "mdi-database-import", | ||||
|           to: "/admin/migrations", | ||||
|  | ||||
| @ -1,79 +0,0 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <ImportDialog | ||||
|       :name="selectedName" | ||||
|       :date="selectedDate" | ||||
|       ref="import_dialog" | ||||
|       @import="importBackup" | ||||
|       @delete="deleteBackup" | ||||
|     /> | ||||
|     <v-row> | ||||
|       <v-col :cols="12" :sm="6" :md="6" :lg="4" :xl="4" v-for="backup in backups" :key="backup.name"> | ||||
|         <v-card hover outlined @click="openDialog(backup)"> | ||||
|           <v-card-text> | ||||
|             <v-row align="center"> | ||||
|               <v-col cols="2"> | ||||
|                 <v-icon large color="primary">mdi-backup-restore</v-icon> | ||||
|               </v-col> | ||||
|               <v-col cols="10"> | ||||
|                 <div class="text-truncate"> | ||||
|                   <strong>{{ backup.name }}</strong> | ||||
|                 </div> | ||||
|                 <div class="text-truncate">{{ $d(Date.parse(backup.date), "medium") }}</div> | ||||
|               </v-col> | ||||
|             </v-row> | ||||
|           </v-card-text> | ||||
|         </v-card> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ImportDialog from "./ImportDialog"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   props: { | ||||
|     backups: Array, | ||||
|   }, | ||||
|   components: { | ||||
|     ImportDialog, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       selectedName: "", | ||||
|       selectedDate: "", | ||||
|       loading: false, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     openDialog(backup) { | ||||
|       this.selectedDate = backup.date; | ||||
|       this.selectedName = backup.name; | ||||
|       this.$refs.import_dialog.open(); | ||||
|     }, | ||||
|     async importBackup(data) { | ||||
|       this.$emit("loading"); | ||||
|       const response = await api.backups.import(data.name, data); | ||||
|       if (response) { | ||||
|         let importData = response.data; | ||||
|         this.$emit("finished", importData); | ||||
|       } else { | ||||
|         this.$emit("finished"); | ||||
|       } | ||||
|     }, | ||||
|     async deleteBackup(data) { | ||||
|       this.$emit("loading"); | ||||
| 
 | ||||
|       if (await api.backups.delete(data.name)) { | ||||
|         this.selectedBackup = null; | ||||
|       } | ||||
|       this.backupLoading = false; | ||||
| 
 | ||||
|       this.$emit("finished"); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -1,113 +0,0 @@ | ||||
| <template> | ||||
|   <v-card :loading="loading"> | ||||
|     <v-card-title> {{ $t("settings.backup.create-heading") }} </v-card-title> | ||||
|     <v-card-text class="mt-n3"> | ||||
|       <v-text-field dense :label="$t('settings.backup.backup-tag')" v-model="tag"></v-text-field> | ||||
|     </v-card-text> | ||||
|     <v-card-actions class="mt-n9 flex-wrap"> | ||||
|       <v-switch v-model="fullBackup" :label="switchLabel"></v-switch> | ||||
|       <v-spacer></v-spacer> | ||||
|       <v-btn color="success" text @click="createBackup()"> | ||||
|         {{ $t("general.create") }} | ||||
|       </v-btn> | ||||
|     </v-card-actions> | ||||
|     <v-expand-transition> | ||||
|       <div v-if="!fullBackup"> | ||||
|         <v-card-text class="mt-n4"> | ||||
|           <v-row> | ||||
|             <v-col sm="4"> | ||||
|               <p>{{ $t("general.options") }}</p> | ||||
|               <ImportOptions @update-options="updateOptions" class="mt-5" /> | ||||
|             </v-col> | ||||
|             <v-col> | ||||
|               <p>{{ $t("general.templates") }}</p> | ||||
|               <v-checkbox | ||||
|                 v-for="template in availableTemplates" | ||||
|                 :key="template" | ||||
|                 class="mb-n4 mt-n3" | ||||
|                 dense | ||||
|                 :label="template" | ||||
|                 @click="appendTemplate(template)" | ||||
|               ></v-checkbox> | ||||
|             </v-col> | ||||
|           </v-row> | ||||
|         </v-card-text> | ||||
|       </div> | ||||
|     </v-expand-transition> | ||||
|   </v-card> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ImportOptions from "./ImportOptions"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { ImportOptions }, | ||||
|   data() { | ||||
|     return { | ||||
|       tag: null, | ||||
|       fullBackup: true, | ||||
|       loading: false, | ||||
|       options: { | ||||
|         recipes: true, | ||||
|         settings: true, | ||||
|         themes: true, | ||||
|         users: true, | ||||
|         groups: true, | ||||
|       }, | ||||
|       availableTemplates: [], | ||||
|       selectedTemplates: [], | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getAvailableBackups(); | ||||
|   }, | ||||
|   computed: { | ||||
|     switchLabel() { | ||||
|       if (this.fullBackup) { | ||||
|         return this.$t("settings.backup.full-backup"); | ||||
|       } else return this.$t("settings.backup.partial-backup"); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     updateOptions(options) { | ||||
|       this.options = options; | ||||
|     }, | ||||
|     async getAvailableBackups() { | ||||
|       let response = await api.backups.requestAvailable(); | ||||
|       response.templates.forEach(element => { | ||||
|         this.availableTemplates.push(element); | ||||
|       }); | ||||
|     }, | ||||
|     async createBackup() { | ||||
|       this.loading = true; | ||||
| 
 | ||||
|       let data = { | ||||
|         tag: this.tag, | ||||
|         options: { | ||||
|           recipes: this.options.recipes, | ||||
|           settings: this.options.settings, | ||||
|           themes: this.options.themes, | ||||
|           users: this.options.users, | ||||
|           groups: this.options.groups, | ||||
|         }, | ||||
|         templates: this.selectedTemplates, | ||||
|       }; | ||||
| 
 | ||||
|       if (await api.backups.create(data)) { | ||||
|         this.$emit("created"); | ||||
|       } | ||||
|       this.loading = false; | ||||
|     }, | ||||
|     appendTemplate(templateName) { | ||||
|       if (this.selectedTemplates.includes(templateName)) { | ||||
|         let index = this.selectedTemplates.indexOf(templateName); | ||||
|         if (index !== -1) { | ||||
|           this.selectedTemplates.splice(index, 1); | ||||
|         } | ||||
|       } else this.selectedTemplates.push(templateName); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -1,75 +0,0 @@ | ||||
| <template> | ||||
|   <v-card :loading="backupLoading" class="mt-3"> | ||||
|     <v-card-title class="headline"> | ||||
|       {{ $t("settings.backup-and-exports") }} | ||||
|     </v-card-title> | ||||
|     <v-divider></v-divider> | ||||
| 
 | ||||
|     <v-card-text> | ||||
|       <v-row> | ||||
|         <v-col cols="12" md="6" sm="12"> | ||||
|           <NewBackupCard @created="processFinished" /> | ||||
|         </v-col> | ||||
|         <v-col cols="12" md="6" sm="12"> | ||||
|           <p> | ||||
|             {{ $t("settings.backup-info") }} | ||||
|           </p> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|       <v-divider class="my-3"></v-divider> | ||||
|       <v-card-title class="mt-n6"> | ||||
|         {{ $t("settings.available-backups") }} | ||||
|         <span> | ||||
|           <TheUploadBtn class="mt-1" url="/api/backups/upload" @uploaded="getAvailableBackups" /> | ||||
|         </span> | ||||
|         <v-spacer></v-spacer> | ||||
|       </v-card-title> | ||||
|       <AvailableBackupCard @loading="backupLoading = true" @finished="processFinished" :backups="availableBackups" /> | ||||
| 
 | ||||
|       <ImportSummaryDialog ref="report" :import-data="importData" /> | ||||
|     </v-card-text> | ||||
|   </v-card> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import ImportSummaryDialog from "@/components/ImportSummaryDialog"; | ||||
| import AvailableBackupCard from "@/pages/Admin/Backup/AvailableBackupCard"; | ||||
| import NewBackupCard from "@/pages/Admin/Backup/NewBackupCard"; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     TheUploadBtn, | ||||
|     AvailableBackupCard, | ||||
|     NewBackupCard, | ||||
|     ImportSummaryDialog, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       failedImports: [], | ||||
|       successfulImports: [], | ||||
|       backupLoading: false, | ||||
|       availableBackups: [], | ||||
|       importData: [], | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getAvailableBackups(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async getAvailableBackups() { | ||||
|       let response = await api.backups.requestAvailable(); | ||||
|       this.availableBackups = response.imports; | ||||
|       this.availableTemplates = response.templates; | ||||
|     }, | ||||
|     processFinished(data) { | ||||
|       this.getAvailableBackups(); | ||||
|       this.backupLoading = false; | ||||
|       this.$refs.report.open(data); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -26,6 +26,8 @@ | ||||
|             </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> Create | ||||
|         </v-btn> | ||||
| @ -36,7 +38,7 @@ | ||||
|             <v-list-item @click.prevent="openDialog(item)"> | ||||
|               <v-list-item-avatar> | ||||
|                 <v-icon large dark :color="color"> | ||||
|                   mdi-backup-restore | ||||
|                   mdi-database | ||||
|                 </v-icon> | ||||
|               </v-list-item-avatar> | ||||
| 
 | ||||
| @ -65,13 +67,14 @@ | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import ImportSummaryDialog from "@/components/ImportSummaryDialog"; | ||||
| import { api } from "@/api"; | ||||
| import StatCard from "./StatCard"; | ||||
| import ImportDialog from "../Backup/ImportDialog"; | ||||
| import StatCard from "@/components/UI/StatCard"; | ||||
| import BackupDialog from "@/components/UI/Dialogs/BackupDialog"; | ||||
| import ImportDialog from "@/components/UI/Dialogs/ImportDialog"; | ||||
| export default { | ||||
|   components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog }, | ||||
|   components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog, BackupDialog }, | ||||
|   data() { | ||||
|     return { | ||||
|       color: "secondary", | ||||
|       color: "accent", | ||||
|       selectedName: "", | ||||
|       selectedDate: "", | ||||
|       loading: false, | ||||
| @ -91,7 +94,6 @@ export default { | ||||
|     async getAvailableBackups() { | ||||
|       const response = await api.backups.requestAvailable(); | ||||
|       this.availableBackups = response.imports; | ||||
|       console.log(this.availableBackups); | ||||
|     }, | ||||
| 
 | ||||
|     async deleteBackup(name) { | ||||
| @ -106,6 +108,7 @@ export default { | ||||
|       this.selectedName = backup.name; | ||||
|       this.$refs.import_dialog.open(); | ||||
|     }, | ||||
| 
 | ||||
|     async importBackup(data) { | ||||
|       this.loading = true; | ||||
|       const response = await api.backups.import(data.name, data); | ||||
|  | ||||
| @ -49,12 +49,12 @@ | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import StatCard from "./StatCard"; | ||||
| import StatCard from "@/components/UI/StatCard"; | ||||
| export default { | ||||
|   components: { StatCard }, | ||||
|   data() { | ||||
|     return { | ||||
|       color: "secondary", | ||||
|       color: "accent", | ||||
|       total: 0, | ||||
|       events: [], | ||||
|       icons: { | ||||
|  | ||||
| @ -82,7 +82,7 @@ | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import StatCard from "./StatCard"; | ||||
| import StatCard from "@/components/UI/StatCard"; | ||||
| import EventViewer from "./EventViewer"; | ||||
| import BackupViewer from "./BackupViewer"; | ||||
| export default { | ||||
|  | ||||
							
								
								
									
										215
									
								
								frontend/src/pages/Admin/Profile/ThemeCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								frontend/src/pages/Admin/Profile/ThemeCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,215 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <StatCard icon="mdi-format-color-fill" :color="color"> | ||||
|       <template v-slot:after-heading> | ||||
|         <div class="ml-auto text-right"> | ||||
|           <div class="body-3 grey--text font-weight-light" v-text="$t('general.themes')" /> | ||||
| 
 | ||||
|           <h3 class="display-2 font-weight-light text--primary"> | ||||
|             <small> {{ selectedTheme.name }} </small> | ||||
|           </h3> | ||||
|         </div> | ||||
|       </template> | ||||
| 
 | ||||
|       <template v-slot:actions> | ||||
|         <v-btn-toggle v-model="darkMode" color="primary " mandatory> | ||||
|           <v-btn small value="system"> | ||||
|             <v-icon>mdi-desktop-tower-monitor</v-icon> | ||||
|             <span class="ml-1" v-show="$vuetify.breakpoint.smAndUp"> | ||||
|               {{ $t("settings.theme.default-to-system") }} | ||||
|             </span> | ||||
|           </v-btn> | ||||
| 
 | ||||
|           <v-btn small value="light"> | ||||
|             <v-icon>mdi-white-balance-sunny</v-icon> | ||||
|             <span class="ml-1" v-show="$vuetify.breakpoint.smAndUp"> | ||||
|               {{ $t("settings.theme.light") }} | ||||
|             </span> | ||||
|           </v-btn> | ||||
| 
 | ||||
|           <v-btn small value="dark"> | ||||
|             <v-icon>mdi-weather-night</v-icon> | ||||
|             <span class="ml-1" v-show="$vuetify.breakpoint.smAndUp"> | ||||
|               {{ $t("settings.theme.dark") }} | ||||
|             </span> | ||||
|           </v-btn> | ||||
|         </v-btn-toggle> | ||||
|       </template> | ||||
|       <template v-slot:bottom> | ||||
|         <v-virtual-scroll height="290" item-height="70" :items="availableThemes" class="mt-2"> | ||||
|           <template v-slot:default="{ item }"> | ||||
|             <v-list-item @click="selectedTheme = item"> | ||||
|               <v-list-item-avatar> | ||||
|                 <v-icon large dark :color="item.colors.primary"> | ||||
|                   mdi-format-color-fill | ||||
|                 </v-icon> | ||||
|               </v-list-item-avatar> | ||||
| 
 | ||||
|               <v-list-item-content> | ||||
|                 <v-list-item-title v-text="item.name"></v-list-item-title> | ||||
| 
 | ||||
|                 <v-row flex align-center class="mt-2 justify-space-around px-4 pb-2"> | ||||
|                   <v-sheet | ||||
|                     class="rounded flex mx-1" | ||||
|                     v-for="(item, index) in item.colors" | ||||
|                     :key="index" | ||||
|                     :color="item" | ||||
|                     height="20" | ||||
|                   > | ||||
|                   </v-sheet> | ||||
|                 </v-row> | ||||
|               </v-list-item-content> | ||||
| 
 | ||||
|               <v-list-item-action class="ml-auto"> | ||||
|                 <v-btn large icon @click.stop="editTheme(item)"> | ||||
|                   <v-icon color="accent">mdi-square-edit-outline</v-icon> | ||||
|                 </v-btn> | ||||
|               </v-list-item-action> | ||||
|             </v-list-item> | ||||
|           </template> | ||||
|         </v-virtual-scroll> | ||||
|         <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-icon left> mdi-plus </v-icon> {{ $t("general.create") }} | ||||
|           </v-btn> | ||||
|         </v-card-actions> | ||||
|       </template> | ||||
|     </StatCard> | ||||
|     <BaseDialog | ||||
|       :loading="loading" | ||||
|       :title="modalLabel.title" | ||||
|       title-icon="mdi-format-color-fill" | ||||
|       modal-width="700" | ||||
|       ref="themeDialog" | ||||
|       :submit-text="modalLabel.button" | ||||
|       @submit="processSubmit" | ||||
|       @delete="deleteTheme" | ||||
|     > | ||||
|       <v-card-text class="mt-3"> | ||||
|         <v-text-field | ||||
|           :label="$t('settings.theme.theme-name')" | ||||
|           v-model="defaultData.name" | ||||
|           :rules="[rules.required]" | ||||
|         ></v-text-field> | ||||
|         <v-row dense dflex wrap justify-content-center v-if="defaultData.colors"> | ||||
|           <v-col cols="12" sm="6" v-for="(_, key) in defaultData.colors" :key="key"> | ||||
|             <ColorPickerDialog :button-text="labels[key]" v-model="defaultData.colors[key]" /> | ||||
|           </v-col> | ||||
|         </v-row> | ||||
|       </v-card-text> | ||||
|     </BaseDialog> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog"; | ||||
| import BaseDialog from "@/components/UI/Dialogs/BaseDialog"; | ||||
| import StatCard from "@/components/UI/StatCard"; | ||||
| export default { | ||||
|   components: { StatCard, BaseDialog, ColorPickerDialog }, | ||||
|   data() { | ||||
|     return { | ||||
|       availableThemes: [], | ||||
|       color: "accent", | ||||
|       newTheme: false, | ||||
|       loading: false, | ||||
|       defaultData: { | ||||
|         name: "", | ||||
|         colors: { | ||||
|           primary: "#E58325", | ||||
|           accent: "#00457A", | ||||
|           secondary: "#973542", | ||||
|           success: "#43A047", | ||||
|           info: "#4990BA", | ||||
|           warning: "#FF4081", | ||||
|           error: "#EF5350", | ||||
|         }, | ||||
|       }, | ||||
|       rules: { | ||||
|         required: val => !!val || this.$t("settings.theme.theme-name-is-required"), | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     labels() { | ||||
|       return { | ||||
|         primary: this.$t("settings.theme.primary"), | ||||
|         secondary: this.$t("settings.theme.secondary"), | ||||
|         accent: this.$t("settings.theme.accent"), | ||||
|         success: this.$t("settings.theme.success"), | ||||
|         info: this.$t("settings.theme.info"), | ||||
|         warning: this.$t("settings.theme.warning"), | ||||
|         error: this.$t("settings.theme.error"), | ||||
|       }; | ||||
|     }, | ||||
|     modalLabel() { | ||||
|       if (this.newTheme) { | ||||
|         return { | ||||
|           title: this.$t("settings.add-a-new-theme"), | ||||
|           button: this.$t("general.create"), | ||||
|         }; | ||||
|       } else { | ||||
|         return { | ||||
|           title: "Update Theme", | ||||
|           button: this.$t("general.update"), | ||||
|         }; | ||||
|       } | ||||
|     }, | ||||
|     selectedTheme: { | ||||
|       set(val) { | ||||
|         this.$store.commit("setTheme", val); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.$store.getters.getActiveTheme; | ||||
|       }, | ||||
|     }, | ||||
|     darkMode: { | ||||
|       set(val) { | ||||
|         this.$store.commit("setDarkMode", val); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.$store.getters.getDarkMode; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   async mounted() { | ||||
|     await this.getAllThemes(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async getAllThemes() { | ||||
|       this.availableThemes = await api.themes.requestAll(); | ||||
|     }, | ||||
|     editTheme(theme) { | ||||
|       console.log(theme); | ||||
|       this.defaultData = theme; | ||||
|       this.newTheme = false; | ||||
|       this.$refs.themeDialog.open(); | ||||
|     }, | ||||
|     createTheme() { | ||||
|       this.newTheme = true; | ||||
|       this.$refs.themeDialog.open(); | ||||
|       console.log("Create Theme"); | ||||
|     }, | ||||
|     async processSubmit() { | ||||
|       if (this.newTheme) { | ||||
|         console.log("New Theme"); | ||||
|         await api.themes.create(this.defaultData); | ||||
|       } else { | ||||
|         await api.themes.update(this.defaultData); | ||||
|       } | ||||
|       this.getAllThemes(); | ||||
|     }, | ||||
|     async deleteTheme() { | ||||
|       console.log(this.defaultData); | ||||
|       await api.themes.delete(this.defaultData.id); | ||||
|       this.getAllThemes(); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
							
								
								
									
										190
									
								
								frontend/src/pages/Admin/Profile/UserCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								frontend/src/pages/Admin/Profile/UserCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,190 @@ | ||||
| <template> | ||||
|   <StatCard icon="mdi-account"> | ||||
|     <template v-slot:avatar> | ||||
|       <v-avatar color="accent" size="120" class="white--text headline mt-n16"> | ||||
|         <img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" /> | ||||
|         <div v-else> | ||||
|           {{ initials }} | ||||
|         </div> | ||||
|       </v-avatar> | ||||
|     </template> | ||||
|     <template v-slot:after-heading> | ||||
|       <div class="ml-auto text-right"> | ||||
|         <div class="body-3 grey--text font-weight-light" v-text="$t('user.user-id-with-value', { id: user.id })" /> | ||||
| 
 | ||||
|         <h3 class="display-2 font-weight-light text--primary"> | ||||
|           <small> {{ $t("group.group") }}: {{ user.group }} </small> | ||||
|         </h3> | ||||
|       </div> | ||||
|     </template> | ||||
|     <template v-slot:actions> | ||||
|       <BaseDialog | ||||
|         :title="$t('user.reset-password')" | ||||
|         title-icon="mdi-lock" | ||||
|         :submit-text="$t('settings.change-password')" | ||||
|         @submit="changePassword" | ||||
|         :loading="loading" | ||||
|         :top="true" | ||||
|       > | ||||
|         <template v-slot:open="{ open }"> | ||||
|           <v-btn color="primary" class="mr-1" small @click="open"> | ||||
|             <v-icon left>mdi-lock</v-icon> | ||||
|             Change Password | ||||
|           </v-btn> | ||||
|         </template> | ||||
| 
 | ||||
|         <v-card-text> | ||||
|           <v-form ref="passChange"> | ||||
|             <v-text-field | ||||
|               v-model="password.current" | ||||
|               prepend-icon="mdi-lock" | ||||
|               :label="$t('user.current-password')" | ||||
|               :rules="[existsRule]" | ||||
|               validate-on-blur | ||||
|               :type="showPassword ? 'text' : 'password'" | ||||
|               @click:append="showPassword.current = !showPassword.current" | ||||
|             ></v-text-field> | ||||
|             <v-text-field | ||||
|               v-model="password.newOne" | ||||
|               prepend-icon="mdi-lock" | ||||
|               :label="$t('user.new-password')" | ||||
|               :rules="[minRule]" | ||||
|               :type="showPassword ? 'text' : 'password'" | ||||
|               @click:append="showPassword.newOne = !showPassword.newOne" | ||||
|             ></v-text-field> | ||||
|             <v-text-field | ||||
|               v-model="password.newTwo" | ||||
|               prepend-icon="mdi-lock" | ||||
|               :label="$t('user.confirm-password')" | ||||
|               :rules="[password.newOne === password.newTwo || $t('user.password-must-match')]" | ||||
|               validate-on-blur | ||||
|               :type="showPassword ? 'text' : 'password'" | ||||
|               @click:append="showPassword.newTwo = !showPassword.newTwo" | ||||
|             ></v-text-field> | ||||
|           </v-form> | ||||
|         </v-card-text> | ||||
|       </BaseDialog> | ||||
|     </template> | ||||
|     <template v-slot:bottom> | ||||
|       <v-card-text> | ||||
|         <v-form> | ||||
|           <v-text-field | ||||
|             :label="$t('user.full-name')" | ||||
|             required | ||||
|             v-model="user.fullName" | ||||
|             :rules="[existsRule]" | ||||
|             validate-on-blur | ||||
|           > | ||||
|           </v-text-field> | ||||
|           <v-text-field :label="$t('user.email')" :rules="[emailRule]" validate-on-blur required v-model="user.email"> | ||||
|           </v-text-field> | ||||
|         </v-form> | ||||
|       </v-card-text> | ||||
|       <v-divider></v-divider> | ||||
|       <v-card-actions class="pb-1 pt-3"> | ||||
|         <TheUploadBtn | ||||
|           icon="mdi-image-area" | ||||
|           :text="$t('user.upload-photo')" | ||||
|           :url="userProfileImage" | ||||
|           file-name="profile_image" | ||||
|         /> | ||||
|         <v-spacer></v-spacer> | ||||
|         <v-btn color="success" @click="updateUser"> | ||||
|           <v-icon left> mdi-content-save </v-icon> | ||||
|           {{ $t("general.update") }} | ||||
|         </v-btn> | ||||
|       </v-card-actions> | ||||
|     </template> | ||||
|   </StatCard> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| 
 | ||||
| import BaseDialog from "@/components/UI/Dialogs/BaseDialog"; | ||||
| import StatCard from "@/components/UI/StatCard"; | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import { api } from "@/api"; | ||||
| import { validators } from "@/mixins/validators"; | ||||
| import { initials } from "@/mixins/initials"; | ||||
| export default { | ||||
|   components: { | ||||
|     BaseDialog, | ||||
|     TheUploadBtn, | ||||
|     StatCard, | ||||
|   }, | ||||
|   mixins: [validators, initials], | ||||
|   data() { | ||||
|     return { | ||||
|       hideImage: false, | ||||
|       passwordLoading: false, | ||||
|       password: { | ||||
|         current: "", | ||||
|         newOne: "", | ||||
|         newTwo: "", | ||||
|       }, | ||||
|       showPassword: false, | ||||
|       loading: false, | ||||
|       user: { | ||||
|         fullName: "", | ||||
|         email: "", | ||||
|         group: "", | ||||
|         admin: false, | ||||
|         id: 0, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
| 
 | ||||
|   computed: { | ||||
|     userProfileImage() { | ||||
|       this.resetImage(); | ||||
|       return `api/users/${this.user.id}/image`; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   async mounted() { | ||||
|     this.refreshProfile(); | ||||
|   }, | ||||
| 
 | ||||
|   methods: { | ||||
|     resetImage() { | ||||
|       this.hideImage = false; | ||||
|     }, | ||||
|     async refreshProfile() { | ||||
|       this.user = await api.users.self(); | ||||
|     }, | ||||
|     openAvatarPicker() { | ||||
|       this.showAvatarPicker = true; | ||||
|     }, | ||||
|     selectAvatar(avatar) { | ||||
|       this.user.avatar = avatar; | ||||
|     }, | ||||
|     async updateUser() { | ||||
|       this.loading = true; | ||||
| 
 | ||||
|       const response = await api.users.update(this.user); | ||||
|       if (response) { | ||||
|         this.$store.commit("setToken", response.data.access_token); | ||||
|         this.refreshProfile(); | ||||
|         this.loading = false; | ||||
|         this.$store.dispatch("requestUserData"); | ||||
|       } | ||||
|     }, | ||||
|     async changePassword() { | ||||
|       this.paswordLoading = true; | ||||
|       let data = { | ||||
|         currentPassword: this.password.current, | ||||
|         newPassword: this.password.newOne, | ||||
|       }; | ||||
| 
 | ||||
|       if (this.$refs.passChange.validate()) { | ||||
|         if (await api.users.changePassword(this.user.id, data)) { | ||||
|           this.$emit("refresh"); | ||||
|         } | ||||
|       } | ||||
|       this.paswordLoading = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -1,206 +1,27 @@ | ||||
| <template> | ||||
|   <v-row dense> | ||||
|     <v-col cols="12" md="8" sm="12"> | ||||
|       <v-card> | ||||
|         <v-card-title class="headline"> | ||||
|           <span> | ||||
|             <v-progress-circular v-if="loading" indeterminate color="primary" large class="mr-2"> </v-progress-circular> | ||||
|           </span> | ||||
|           {{ $t("settings.profile") }} | ||||
|           <v-spacer></v-spacer> | ||||
|           {{ $t("user.user-id-with-value", { id: user.id }) }} | ||||
|         </v-card-title> | ||||
|         <v-divider></v-divider> | ||||
|         <v-card-text> | ||||
|   <div class="mt-10"> | ||||
|     <v-row> | ||||
|             <v-col cols="12" md="3" align="center" justify="center"> | ||||
|               <v-avatar color="accent" size="120" class="white--text headline mr-2"> | ||||
|                 <img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" /> | ||||
|                 <div v-else> | ||||
|                   {{ initials }} | ||||
|       <v-col cols="12" sm="12" lg="6"> | ||||
|         <UserCard /> | ||||
|       </v-col> | ||||
|       <v-col cols="12" sm="12" lg="6"> </v-col> | ||||
|     </v-row> | ||||
|     <v-row class="mt-7"> | ||||
|       <v-col cols="12" sm="12" lg="6"> | ||||
|         <ThemeCard /> | ||||
|       </v-col> | ||||
|       <v-col cols="12" sm="12" lg="6"> </v-col> | ||||
|     </v-row> | ||||
|   </div> | ||||
|               </v-avatar> | ||||
|             </v-col> | ||||
|             <v-col cols="12" md="9"> | ||||
|               <v-form> | ||||
|                 <v-text-field | ||||
|                   :label="$t('user.full-name')" | ||||
|                   required | ||||
|                   v-model="user.fullName" | ||||
|                   :rules="[existsRule]" | ||||
|                   validate-on-blur | ||||
|                 > | ||||
|                 </v-text-field> | ||||
|                 <v-text-field | ||||
|                   :label="$t('user.email')" | ||||
|                   :rules="[emailRule]" | ||||
|                   validate-on-blur | ||||
|                   required | ||||
|                   v-model="user.email" | ||||
|                 > | ||||
|                 </v-text-field> | ||||
|                 <v-text-field | ||||
|                   :label="$t('group.group')" | ||||
|                   readonly | ||||
|                   v-model="user.group" | ||||
|                   persistent-hint | ||||
|                   :hint="$t('group.groups-can-only-be-set-by-administrators')" | ||||
|                 > | ||||
|                 </v-text-field> | ||||
|               </v-form> | ||||
|             </v-col> | ||||
|           </v-row> | ||||
|         </v-card-text> | ||||
| 
 | ||||
|         <v-card-actions> | ||||
|           <TheUploadBtn | ||||
|             icon="mdi-image-area" | ||||
|             :text="$t('user.upload-photo')" | ||||
|             :url="userProfileImage" | ||||
|             file-name="profile_image" | ||||
|           /> | ||||
| 
 | ||||
|           <v-spacer></v-spacer> | ||||
|           <v-btn color="success" class="mr-2" @click="updateUser"> | ||||
|             <v-icon left> mdi-content-save </v-icon> | ||||
|             {{ $t("general.save") }} | ||||
|           </v-btn> | ||||
|         </v-card-actions> | ||||
|       </v-card> | ||||
|     </v-col> | ||||
|     <v-col cols="12" md="4" sm="12"> | ||||
|       <v-card height="100%"> | ||||
|         <v-card-title class="headline"> | ||||
|           {{ $t("user.reset-password") }} | ||||
|           <v-spacer></v-spacer> | ||||
|         </v-card-title> | ||||
|         <v-divider></v-divider> | ||||
|         <v-card-text> | ||||
|           <v-form ref="passChange"> | ||||
|             <v-text-field | ||||
|               v-model="password.current" | ||||
|               prepend-icon="mdi-lock" | ||||
|               :label="$t('user.current-password')" | ||||
|               :rules="[existsRule]" | ||||
|               validate-on-blur | ||||
|               :type="showPassword ? 'text' : 'password'" | ||||
|               @click:append="showPassword.current = !showPassword.current" | ||||
|             ></v-text-field> | ||||
|             <v-text-field | ||||
|               v-model="password.newOne" | ||||
|               prepend-icon="mdi-lock" | ||||
|               :label="$t('user.new-password')" | ||||
|               :rules="[minRule]" | ||||
|               :type="showPassword ? 'text' : 'password'" | ||||
|               @click:append="showPassword.newOne = !showPassword.newOne" | ||||
|             ></v-text-field> | ||||
|             <v-text-field | ||||
|               v-model="password.newTwo" | ||||
|               prepend-icon="mdi-lock" | ||||
|               :label="$t('user.confirm-password')" | ||||
|               :rules="[password.newOne === password.newTwo || $t('user.password-must-match')]" | ||||
|               validate-on-blur | ||||
|               :type="showPassword ? 'text' : 'password'" | ||||
|               @click:append="showPassword.newTwo = !showPassword.newTwo" | ||||
|             ></v-text-field> | ||||
|           </v-form> | ||||
|         </v-card-text> | ||||
|         <v-card-actions> | ||||
|           <v-btn icon @click="showPassword = !showPassword" :loading="passwordLoading"> | ||||
|             <v-icon v-if="!showPassword">mdi-eye-off</v-icon> | ||||
|             <v-icon v-else> mdi-eye </v-icon> | ||||
|           </v-btn> | ||||
|           <v-spacer></v-spacer> | ||||
|           <v-btn color="accent" class="mr-2" @click="changePassword"> | ||||
|             <v-icon left> mdi-lock </v-icon> | ||||
|             {{ $t("settings.change-password") }} | ||||
|           </v-btn> | ||||
|         </v-card-actions> | ||||
|       </v-card> | ||||
|     </v-col> | ||||
|   </v-row> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| // import AvatarPicker from '@/components/AvatarPicker' | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import { api } from "@/api"; | ||||
| import { validators } from "@/mixins/validators"; | ||||
| import { initials } from "@/mixins/initials"; | ||||
| import ThemeCard from "./ThemeCard"; | ||||
| import UserCard from "./UserCard"; | ||||
| export default { | ||||
|   components: { | ||||
|     TheUploadBtn, | ||||
|   }, | ||||
|   mixins: [validators, initials], | ||||
|   data() { | ||||
|     return { | ||||
|       hideImage: false, | ||||
|       passwordLoading: false, | ||||
|       password: { | ||||
|         current: "", | ||||
|         newOne: "", | ||||
|         newTwo: "", | ||||
|       }, | ||||
|       showPassword: false, | ||||
|       loading: false, | ||||
|       user: { | ||||
|         fullName: "", | ||||
|         email: "", | ||||
|         group: "", | ||||
|         admin: false, | ||||
|         id: 0, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
| 
 | ||||
|   computed: { | ||||
|     userProfileImage() { | ||||
|       this.resetImage(); | ||||
|       return `api/users/${this.user.id}/image`; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   async mounted() { | ||||
|     this.refreshProfile(); | ||||
|   }, | ||||
| 
 | ||||
|   methods: { | ||||
|     resetImage() { | ||||
|       this.hideImage = false; | ||||
|     }, | ||||
|     async refreshProfile() { | ||||
|       this.user = await api.users.self(); | ||||
|     }, | ||||
|     openAvatarPicker() { | ||||
|       this.showAvatarPicker = true; | ||||
|     }, | ||||
|     selectAvatar(avatar) { | ||||
|       this.user.avatar = avatar; | ||||
|     }, | ||||
|     async updateUser() { | ||||
|       this.loading = true; | ||||
|       const response = await api.users.update(this.user); | ||||
|       if (response) { | ||||
|         this.$store.commit("setToken", response.data.access_token); | ||||
|         this.refreshProfile(); | ||||
|         this.loading = false; | ||||
|         this.$store.dispatch("requestUserData"); | ||||
|       } | ||||
|     }, | ||||
|     async changePassword() { | ||||
|       this.paswordLoading = true; | ||||
|       let data = { | ||||
|         currentPassword: this.password.current, | ||||
|         newPassword: this.password.newOne, | ||||
|       }; | ||||
| 
 | ||||
|       if (this.$refs.passChange.validate()) { | ||||
|         if (await api.users.changePassword(this.user.id, data)) { | ||||
|           this.$emit("refresh"); | ||||
|         } | ||||
|       } | ||||
|       this.paswordLoading = false; | ||||
|     }, | ||||
|     UserCard, | ||||
|     ThemeCard, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| @ -1,89 +0,0 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-btn text color="info" @click="dialog = true"> | ||||
|       {{ $t("settings.add-a-new-theme") }} | ||||
|     </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-format-color-fill | ||||
|           </v-icon> | ||||
| 
 | ||||
|           <v-toolbar-title class="headline"> | ||||
|             {{ $t("settings.add-a-new-theme") }} | ||||
|           </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 | ||||
|               :label="$t('settings.theme.theme-name')" | ||||
|               v-model="themeName" | ||||
|               :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="!themeName"> | ||||
|               {{ $t("general.create") }} | ||||
|             </v-btn> | ||||
|           </v-card-actions> | ||||
|         </v-form> | ||||
|       </v-card> | ||||
|     </v-dialog> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     buttonText: String, | ||||
|     value: String, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       dialog: false, | ||||
|       themeName: "", | ||||
|       rules: { | ||||
|         required: val => !!val || this.$t("settings.theme.theme-name-is-required"), | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
| 
 | ||||
|   watch: { | ||||
|     color() { | ||||
|       this.updateColor(); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     randomColor() { | ||||
|       return "#" + Math.floor(Math.random() * 16777215).toString(16); | ||||
|     }, | ||||
|     select() { | ||||
|       const newTheme = { | ||||
|         name: this.themeName, | ||||
|         colors: { | ||||
|           primary: "#E58325", | ||||
|           accent: "#00457A", | ||||
|           secondary: "#973542", | ||||
|           success: "#5AB1BB", | ||||
|           info: "#4990BA", | ||||
|           warning: "#FF4081", | ||||
|           error: "#EF5350", | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       this.$emit("new-theme", newTheme); | ||||
|       this.dialog = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -1,88 +0,0 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <ConfirmationDialog | ||||
|       :title="$t('settings.theme.delete-theme')" | ||||
|       :message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')" | ||||
|       color="error" | ||||
|       icon="mdi-alert-circle" | ||||
|       ref="deleteThemeConfirm" | ||||
|       v-on:confirm="deleteSelectedTheme()" | ||||
|     /> | ||||
|     <v-card flat outlined class="ma-2"> | ||||
|       <v-card-text class="mb-n5 mt-n2"> | ||||
|         <h3> | ||||
|           {{ theme.name }} | ||||
|           {{ current ? $t("general.current-parenthesis") : "" }} | ||||
|         </h3> | ||||
|       </v-card-text> | ||||
|       <v-card-text> | ||||
|         <v-row flex align-center> | ||||
|           <v-card | ||||
|             v-for="(color, index) in theme.colors" | ||||
|             :key="index" | ||||
|             class="ma-1 mx-auto" | ||||
|             height="34" | ||||
|             width="36" | ||||
|             :color="color" | ||||
|           > | ||||
|           </v-card> | ||||
|         </v-row> | ||||
|       </v-card-text> | ||||
|       <v-divider></v-divider> | ||||
|       <v-card-actions> | ||||
|         <v-btn text color="error" @click="confirmDelete"> | ||||
|           {{ $t("general.delete") }} | ||||
|         </v-btn> | ||||
|         <v-spacer></v-spacer> | ||||
|         <!-- <v-btn text color="accent" @click="editTheme">Edit</v-btn> --> | ||||
|         <v-btn text color="success" @click="saveThemes">{{ $t("general.apply") }}</v-btn> | ||||
|       </v-card-actions> | ||||
|     </v-card> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog"; | ||||
| import { api } from "@/api"; | ||||
| 
 | ||||
| const DELETE_EVENT = "delete"; | ||||
| const APPLY_EVENT = "apply"; | ||||
| const EDIT_EVENT = "edit"; | ||||
| export default { | ||||
|   components: { | ||||
|     ConfirmationDialog, | ||||
|   }, | ||||
|   props: { | ||||
|     theme: Object, | ||||
|     current: { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     confirmDelete() { | ||||
|       if (this.theme.name === "default") { | ||||
|         // Notify User Can't Delete Default | ||||
|       } else if (this.theme !== {}) { | ||||
|         this.$refs.deleteThemeConfirm.open(); | ||||
|       } | ||||
|     }, | ||||
|     async deleteSelectedTheme() { | ||||
|       //Delete Theme from DB | ||||
|       if (await api.themes.delete(this.theme.name)) { | ||||
|         //Get the new list of available from DB | ||||
|         this.availableThemes = await api.themes.requestAll(); | ||||
|         this.$emit(DELETE_EVENT); | ||||
|       } | ||||
|     }, | ||||
|     async saveThemes() { | ||||
|       this.$store.commit("setTheme", this.theme); | ||||
|       this.$emit(APPLY_EVENT, this.theme); | ||||
|     }, | ||||
|     editTheme() { | ||||
|       this.$emit(EDIT_EVENT); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -1,155 +0,0 @@ | ||||
| <template> | ||||
|   <v-card> | ||||
|     <v-card-title class="headline"> | ||||
|       {{ $t("settings.theme.theme-settings") }} | ||||
|     </v-card-title> | ||||
|     <v-divider></v-divider> | ||||
|     <v-card-text> | ||||
|       <h2 class="mt-4 mb-1">{{ $t("settings.theme.dark-mode") }}</h2> | ||||
|       <p> | ||||
|         {{ | ||||
|           $t( | ||||
|             "settings.theme.choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme" | ||||
|           ) | ||||
|         }} | ||||
|       </p> | ||||
|       <v-row dense align="center"> | ||||
|         <v-col cols="6"> | ||||
|           <v-btn-toggle v-model="selectedDarkMode" color="primary " mandatory @change="setStoresDarkMode"> | ||||
|             <v-btn value="system"> | ||||
|               <v-icon>mdi-desktop-tower-monitor</v-icon> | ||||
|               <span class="ml-1" v-show="$vuetify.breakpoint.smAndUp"> | ||||
|                 {{ $t("settings.theme.default-to-system") }} | ||||
|               </span> | ||||
|             </v-btn> | ||||
| 
 | ||||
|             <v-btn value="light"> | ||||
|               <v-icon>mdi-white-balance-sunny</v-icon> | ||||
|               <span class="ml-1" v-show="$vuetify.breakpoint.smAndUp"> | ||||
|                 {{ $t("settings.theme.light") }} | ||||
|               </span> | ||||
|             </v-btn> | ||||
| 
 | ||||
|             <v-btn value="dark"> | ||||
|               <v-icon>mdi-weather-night</v-icon> | ||||
|               <span class="ml-1" v-show="$vuetify.breakpoint.smAndUp"> | ||||
|                 {{ $t("settings.theme.dark") }} | ||||
|               </span> | ||||
|             </v-btn> | ||||
|           </v-btn-toggle> | ||||
|         </v-col> | ||||
|       </v-row></v-card-text | ||||
|     > | ||||
|     <v-divider></v-divider> | ||||
|     <v-card-text> | ||||
|       <h2 class="mt-1 mb-1">{{ $t("settings.theme.theme") }}</h2> | ||||
|       <p> | ||||
|         {{ | ||||
|           $t( | ||||
|             "settings.theme.select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference" | ||||
|           ) | ||||
|         }} | ||||
|       </p> | ||||
| 
 | ||||
|       <v-row dense align-content="center" v-if="selectedTheme.colors"> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.primary')" v-model="selectedTheme.colors.primary" /> | ||||
|         </v-col> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.secondary')" v-model="selectedTheme.colors.secondary" /> | ||||
|         </v-col> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.accent')" v-model="selectedTheme.colors.accent" /> | ||||
|         </v-col> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.success')" v-model="selectedTheme.colors.success" /> | ||||
|         </v-col> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.info')" v-model="selectedTheme.colors.info" /> | ||||
|         </v-col> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.warning')" v-model="selectedTheme.colors.warning" /> | ||||
|         </v-col> | ||||
|         <v-col> | ||||
|           <ColorPickerDialog :button-text="$t('settings.theme.error')" v-model="selectedTheme.colors.error" /> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </v-card-text> | ||||
| 
 | ||||
|     <v-card-text> | ||||
|       <v-row> | ||||
|         <v-col cols="12" sm="12" md="6" lg="4" xl="3" v-for="theme in availableThemes" :key="theme.name"> | ||||
|           <ThemeCard :theme="theme" :current="selectedTheme.name == theme.name ? true : false" @delete="getAllThemes" /> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </v-card-text> | ||||
| 
 | ||||
|     <v-card-actions> | ||||
|       <NewThemeDialog @new-theme="appendTheme" class="mt-1" /> | ||||
|       <v-spacer></v-spacer> | ||||
|       <v-btn color="success" @click="saveThemes" class="mr-2"> | ||||
|         <v-icon left> mdi-content-save </v-icon> | ||||
|         {{ $t("general.save") }} | ||||
|       </v-btn> | ||||
|     </v-card-actions> | ||||
|   </v-card> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog"; | ||||
| import NewThemeDialog from "./NewThemeDialog"; | ||||
| import ThemeCard from "./ThemeCard"; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     ColorPickerDialog, | ||||
|     NewThemeDialog, | ||||
|     ThemeCard, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       selectedDarkMode: "system", | ||||
|       availableThemes: [], | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     await this.getAllThemes(); | ||||
| 
 | ||||
|     this.selectedDarkMode = this.$store.getters.getDarkMode; | ||||
|   }, | ||||
| 
 | ||||
|   computed: { | ||||
|     selectedTheme() { | ||||
|       return this.$store.getters.getActiveTheme; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   methods: { | ||||
|     async getAllThemes() { | ||||
|       this.availableThemes = await api.themes.requestAll(); | ||||
|     }, | ||||
|     /** | ||||
|      * Create the new Theme and select it. | ||||
|      */ | ||||
|     async appendTheme(NewThemeDialog) { | ||||
|       const response = await api.themes.create(NewThemeDialog); | ||||
|       if (response) { | ||||
|         this.availableThemes.push(NewThemeDialog); | ||||
|         this.$store.commit("setTheme", NewThemeDialog); | ||||
|       } | ||||
|     }, | ||||
|     setStoresDarkMode() { | ||||
|       this.$store.commit("setDarkMode", this.selectedDarkMode); | ||||
|     }, | ||||
|     /** | ||||
|      * This will save the current colors and make the selected theme live. | ||||
|      */ | ||||
|     saveThemes() { | ||||
|       api.themes.update(this.selectedTheme.name, this.selectedTheme.colors); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
| @ -1,6 +1,7 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CardSection | ||||
|       title-icon="" | ||||
|       v-if="siteSettings.showRecent" | ||||
|       :title="$t('page.recent')" | ||||
|       :recipes="recentRecipes" | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|   <v-container> | ||||
|     <v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear> | ||||
|     <CardSection | ||||
|       title-icon="" | ||||
|       :sortable="true" | ||||
|       :title="$t('page.all-recipes')" | ||||
|       :recipes="allRecipes" | ||||
|  | ||||
| @ -1,6 +1,4 @@ | ||||
| import Admin from "@/pages/Admin"; | ||||
| import Backup from "@/pages/Admin/Backup"; | ||||
| import Theme from "@/pages/Admin/Theme"; | ||||
| import MealPlanner from "@/pages/Admin/MealPlanner"; | ||||
| import Migration from "@/pages/Admin/Migration"; | ||||
| import Profile from "@/pages/Admin/Profile"; | ||||
| @ -31,21 +29,6 @@ export const adminRoutes = { | ||||
|         title: "settings.profile", | ||||
|       }, | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|       path: "backups", | ||||
|       component: Backup, | ||||
|       meta: { | ||||
|         title: "settings.backup-and-exports", | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       path: "themes", | ||||
|       component: Theme, | ||||
|       meta: { | ||||
|         title: "general.themes", | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       path: "meal-planner", | ||||
|       component: MealPlanner, | ||||
|  | ||||
| @ -22,6 +22,7 @@ export const routes = [ | ||||
| ]; | ||||
| 
 | ||||
| const router = new VueRouter({ | ||||
|   base: process.env.BASE_URL, | ||||
|   routes, | ||||
|   mode: process.env.NODE_ENV === "production" ? "history" : "hash", | ||||
|   scrollBehavior() { | ||||
|  | ||||
| @ -60,7 +60,7 @@ const actions = { | ||||
|   }, | ||||
| 
 | ||||
|   async resetTheme({ commit }) { | ||||
|     const defaultTheme = await api.themes.requestByName("default"); | ||||
|     const defaultTheme = await api.themes.requestByName(1); | ||||
|     if (defaultTheme.colors) { | ||||
|       Vuetify.framework.theme.themes.dark = defaultTheme.colors; | ||||
|       Vuetify.framework.theme.themes.light = defaultTheme.colors; | ||||
|  | ||||
| @ -1,18 +1,19 @@ | ||||
| import uvicorn | ||||
| from fastapi import FastAPI | ||||
| 
 | ||||
| from mealie.core import root_logger | ||||
| from mealie.core.config import APP_VERSION, settings | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes | ||||
| from mealie.routes.about import about_router | ||||
| from mealie.routes.groups import groups | ||||
| from mealie.routes.mealplans import mealplans | ||||
| from mealie.routes.recipe import router as recipe_router | ||||
| from mealie.routes.site_settings import all_settings | ||||
| from mealie.routes.users import users | ||||
| from mealie.routes.groups import groups_router | ||||
| from mealie.routes.mealplans import meal_plan_router | ||||
| from mealie.routes.media import media_router | ||||
| from mealie.routes.recipe import recipe_router | ||||
| from mealie.routes.site_settings import settings_router | ||||
| from mealie.routes.users import user_router | ||||
| from mealie.services.events import create_general_event | ||||
| 
 | ||||
| logger = root_logger.get_logger() | ||||
| logger = get_logger() | ||||
| 
 | ||||
| app = FastAPI( | ||||
|     title="Mealie", | ||||
| @ -29,15 +30,16 @@ def start_scheduler(): | ||||
| 
 | ||||
| def api_routers(): | ||||
|     # Authentication | ||||
|     app.include_router(users.router) | ||||
|     app.include_router(groups.router) | ||||
|     app.include_router(user_router) | ||||
|     app.include_router(groups_router) | ||||
|     # Recipes | ||||
|     app.include_router(recipe_router) | ||||
|     app.include_router(media_router) | ||||
|     app.include_router(about_router) | ||||
|     # Meal Routes | ||||
|     app.include_router(mealplans.router) | ||||
|     app.include_router(meal_plan_router) | ||||
|     # Settings Routes | ||||
|     app.include_router(all_settings.router) | ||||
|     app.include_router(settings_router) | ||||
|     app.include_router(theme_routes.router) | ||||
|     # Backups/Imports Routes | ||||
|     app.include_router(backup_routes.router) | ||||
|  | ||||
| @ -93,7 +93,7 @@ class _Settings(BaseDocument): | ||||
| 
 | ||||
| class _Themes(BaseDocument): | ||||
|     def __init__(self) -> None: | ||||
|         self.primary_key = "name" | ||||
|         self.primary_key = "id" | ||||
|         self.sql_model = SiteThemeModel | ||||
|         self.schema = SiteTheme | ||||
| 
 | ||||
|  | ||||
| @ -1,23 +1,21 @@ | ||||
| import sqlalchemy as sa | ||||
| import sqlalchemy.orm as orm | ||||
| from mealie.db.models.model_base import SqlAlchemyBase | ||||
| from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase | ||||
| from sqlalchemy.sql.sqltypes import Integer | ||||
| 
 | ||||
| 
 | ||||
| class SiteThemeModel(SqlAlchemyBase): | ||||
| class SiteThemeModel(SqlAlchemyBase, BaseMixins): | ||||
|     __tablename__ = "site_theme" | ||||
|     name = sa.Column(sa.String, primary_key=True) | ||||
|     id = sa.Column(Integer, primary_key=True, unique=True) | ||||
|     name = sa.Column(sa.String, nullable=False, unique=True) | ||||
|     colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete") | ||||
| 
 | ||||
|     def __init__(self, name: str, colors: dict, session=None) -> None: | ||||
|     def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None: | ||||
|         self.name = name | ||||
|         self.colors = ThemeColorsModel(**colors) | ||||
| 
 | ||||
|     def update(self, session=None, name: str = None, colors: dict = None) -> dict: | ||||
|         self.colors.update(**colors) | ||||
|         return self | ||||
| 
 | ||||
| 
 | ||||
| class ThemeColorsModel(SqlAlchemyBase): | ||||
| class ThemeColorsModel(SqlAlchemyBase, BaseMixins): | ||||
|     __tablename__ = "theme_colors" | ||||
|     id = sa.Column(sa.Integer, primary_key=True) | ||||
|     parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name")) | ||||
| @ -28,21 +26,3 @@ class ThemeColorsModel(SqlAlchemyBase): | ||||
|     info = sa.Column(sa.String) | ||||
|     warning = sa.Column(sa.String) | ||||
|     error = sa.Column(sa.String) | ||||
| 
 | ||||
|     def update( | ||||
|         self, | ||||
|         primary: str = None, | ||||
|         accent: str = None, | ||||
|         secondary: str = None, | ||||
|         success: str = None, | ||||
|         info: str = None, | ||||
|         warning: str = None, | ||||
|         error: str = None, | ||||
|     ) -> None: | ||||
|         self.primary = primary | ||||
|         self.accent = accent | ||||
|         self.secondary = secondary | ||||
|         self.success = success | ||||
|         self.info = info | ||||
|         self.warning = warning | ||||
|         self.error = error | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| from fastapi import APIRouter | ||||
| 
 | ||||
| from .events import router as events_router | ||||
| from . import events | ||||
| 
 | ||||
| about_router = APIRouter(prefix="/api/about") | ||||
| 
 | ||||
| about_router.include_router(events_router) | ||||
| about_router.include_router(events.router) | ||||
|  | ||||
| @ -0,0 +1,8 @@ | ||||
| from fastapi import APIRouter | ||||
| 
 | ||||
| from . import crud, groups | ||||
| 
 | ||||
| groups_router = APIRouter() | ||||
| 
 | ||||
| groups_router.include_router(crud.router) | ||||
| groups_router.include_router(groups.router) | ||||
| @ -0,0 +1,9 @@ | ||||
| from fastapi import APIRouter | ||||
| 
 | ||||
| from . import crud, helpers, mealplans | ||||
| 
 | ||||
| meal_plan_router = APIRouter() | ||||
| 
 | ||||
| meal_plan_router.include_router(crud.router) | ||||
| meal_plan_router.include_router(helpers.router) | ||||
| meal_plan_router.include_router(mealplans.router) | ||||
							
								
								
									
										7
									
								
								mealie/routes/media/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								mealie/routes/media/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| from fastapi import APIRouter | ||||
| 
 | ||||
| from . import recipe | ||||
| 
 | ||||
| media_router = APIRouter(prefix="/api/media", tags=["Site Media"]) | ||||
| 
 | ||||
| media_router.include_router(recipe.router) | ||||
							
								
								
									
										41
									
								
								mealie/routes/media/recipe.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								mealie/routes/media/recipe.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| from enum import Enum | ||||
| 
 | ||||
| from fastapi import APIRouter, HTTPException, status | ||||
| from mealie.schema.recipe import Recipe | ||||
| from starlette.responses import FileResponse | ||||
| 
 | ||||
| """ | ||||
| These routes are for development only! These assets are served by Caddy when not | ||||
| in development mode. If you make changes, be sure to test the production container. | ||||
| """ | ||||
| 
 | ||||
| router = APIRouter(prefix="/recipes") | ||||
| 
 | ||||
| 
 | ||||
| class ImageType(str, Enum): | ||||
|     original = "original.webp" | ||||
|     small = "min-original.webp" | ||||
|     tiny = "tiny-original.webp" | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/{recipe_slug}/images/{file_name}") | ||||
| async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original): | ||||
|     """Takes in a recipe slug, returns the static image. This route is proxied in the docker image | ||||
|     and should not hit the API in production""" | ||||
|     recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value) | ||||
| 
 | ||||
|     if recipe_image: | ||||
|         return FileResponse(recipe_image) | ||||
|     else: | ||||
|         raise HTTPException(status.HTTP_404_NOT_FOUND) | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/{recipe_slug}/assets/{file_name}") | ||||
| async def get_recipe_asset(recipe_slug: str, file_name: str): | ||||
|     """ Returns a recipe asset """ | ||||
|     file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name) | ||||
| 
 | ||||
|     try: | ||||
|         return FileResponse(file) | ||||
|     except Exception: | ||||
|         raise HTTPException(status.HTTP_404_NOT_FOUND) | ||||
							
								
								
									
										0
									
								
								mealie/routes/media/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								mealie/routes/media/user.py
									
									
									
									
									
										Normal file
									
								
							| @ -1,10 +1,9 @@ | ||||
| from fastapi import APIRouter | ||||
| from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, recipe_media, tag_routes | ||||
| from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes | ||||
| 
 | ||||
| router = APIRouter() | ||||
| recipe_router = APIRouter() | ||||
| 
 | ||||
| router.include_router(all_recipe_routes.router) | ||||
| router.include_router(recipe_crud_routes.router) | ||||
| router.include_router(recipe_media.router) | ||||
| router.include_router(category_routes.router) | ||||
| router.include_router(tag_routes.router) | ||||
| recipe_router.include_router(all_recipe_routes.router) | ||||
| recipe_router.include_router(recipe_crud_routes.router) | ||||
| recipe_router.include_router(category_routes.router) | ||||
| recipe_router.include_router(tag_routes.router) | ||||
|  | ||||
| @ -1,13 +1,17 @@ | ||||
| from shutil import copyfileobj | ||||
| 
 | ||||
| from fastapi import APIRouter, Depends, File, Form, HTTPException, status | ||||
| from fastapi.datastructures import UploadFile | ||||
| from mealie.core.root_logger import get_logger | ||||
| 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.recipe import Recipe, RecipeURLIn | ||||
| from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn | ||||
| from mealie.services.events import create_recipe_event | ||||
| from mealie.services.image.image import scrape_image, write_image | ||||
| from mealie.services.recipe.media import check_assets, delete_assets | ||||
| from mealie.services.scraper.scraper import create_from_url | ||||
| from slugify import slugify | ||||
| from sqlalchemy.orm.session import Session | ||||
| 
 | ||||
| router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"]) | ||||
| @ -126,3 +130,30 @@ def scrape_image_url( | ||||
|     """ Removes an existing image and replaces it with the incoming file. """ | ||||
| 
 | ||||
|     scrape_image(url.url, recipe_slug) | ||||
| 
 | ||||
| 
 | ||||
| @router.post("/{recipe_slug}/assets", response_model=RecipeAsset) | ||||
| def upload_recipe_asset( | ||||
|     recipe_slug: str, | ||||
|     name: str = Form(...), | ||||
|     icon: str = Form(...), | ||||
|     extension: str = Form(...), | ||||
|     file: UploadFile = File(...), | ||||
|     session: Session = Depends(generate_session), | ||||
|     current_user=Depends(get_current_user), | ||||
| ): | ||||
|     """ Upload a file to store as a recipe asset """ | ||||
|     file_name = slugify(name) + "." + extension | ||||
|     asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) | ||||
|     dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name) | ||||
| 
 | ||||
|     with dest.open("wb") as buffer: | ||||
|         copyfileobj(file.file, buffer) | ||||
| 
 | ||||
|     if not dest.is_file(): | ||||
|         raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) | ||||
| 
 | ||||
|     recipe: Recipe = db.recipes.get(session, recipe_slug) | ||||
|     recipe.assets.append(asset_in) | ||||
|     db.recipes.update(session, recipe_slug, recipe.dict()) | ||||
|     return asset_in | ||||
|  | ||||
| @ -1,70 +0,0 @@ | ||||
| import shutil | ||||
| from enum import Enum | ||||
| 
 | ||||
| from fastapi import APIRouter, Depends, File, Form, HTTPException, status | ||||
| from fastapi.datastructures import UploadFile | ||||
| 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.recipe import Recipe, RecipeAsset | ||||
| from slugify import slugify | ||||
| from sqlalchemy.orm.session import Session | ||||
| from starlette.responses import FileResponse | ||||
| 
 | ||||
| router = APIRouter(prefix="/api/recipes/media", tags=["Recipe Media"]) | ||||
| 
 | ||||
| 
 | ||||
| class ImageType(str, Enum): | ||||
|     original = "original.webp" | ||||
|     small = "min-original.webp" | ||||
|     tiny = "tiny-original.webp" | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/{recipe_slug}/images/{file_name}") | ||||
| async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original): | ||||
|     """Takes in a recipe slug, returns the static image. This route is proxied in the docker image | ||||
|     and should not hit the API in production""" | ||||
|     recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value) | ||||
| 
 | ||||
|     if recipe_image: | ||||
|         return FileResponse(recipe_image) | ||||
|     else: | ||||
|         raise HTTPException(status.HTTP_404_NOT_FOUND) | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/{recipe_slug}/assets/{file_name}") | ||||
| async def get_recipe_asset(recipe_slug: str, file_name: str): | ||||
|     """ Returns a recipe asset """ | ||||
|     file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name) | ||||
| 
 | ||||
|     try: | ||||
|         return FileResponse(file) | ||||
|     except Exception: | ||||
|         raise HTTPException(status.HTTP_404_NOT_FOUND) | ||||
| 
 | ||||
| 
 | ||||
| @router.post("/{recipe_slug}/assets", response_model=RecipeAsset) | ||||
| def upload_recipe_asset( | ||||
|     recipe_slug: str, | ||||
|     name: str = Form(...), | ||||
|     icon: str = Form(...), | ||||
|     extension: str = Form(...), | ||||
|     file: UploadFile = File(...), | ||||
|     session: Session = Depends(generate_session), | ||||
|     current_user=Depends(get_current_user), | ||||
| ): | ||||
|     """ Upload a file to store as a recipe asset """ | ||||
|     file_name = slugify(name) + "." + extension | ||||
|     asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) | ||||
|     dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name) | ||||
| 
 | ||||
|     with dest.open("wb") as buffer: | ||||
|         shutil.copyfileobj(file.file, buffer) | ||||
| 
 | ||||
|     if not dest.is_file(): | ||||
|         raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) | ||||
| 
 | ||||
|     recipe: Recipe = db.recipes.get(session, recipe_slug) | ||||
|     recipe.assets.append(asset_in) | ||||
|     db.recipes.update(session, recipe_slug, recipe.dict()) | ||||
|     return asset_in | ||||
| @ -0,0 +1,9 @@ | ||||
| from fastapi import APIRouter | ||||
| 
 | ||||
| from . import all_settings, custom_pages, site_settings | ||||
| 
 | ||||
| settings_router = APIRouter() | ||||
| 
 | ||||
| settings_router.include_router(all_settings.router) | ||||
| settings_router.include_router(custom_pages.router) | ||||
| settings_router.include_router(site_settings.router) | ||||
| @ -1,4 +1,4 @@ | ||||
| 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 | ||||
| @ -21,27 +21,27 @@ def create_theme(data: SiteTheme, session: Session = Depends(generate_session), | ||||
|     db.themes.create(session, data.dict()) | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/themes/{theme_name}") | ||||
| def get_single_theme(theme_name: str, session: Session = Depends(generate_session)): | ||||
| @router.get("/themes/{id}") | ||||
| def get_single_theme(id: int, session: Session = Depends(generate_session)): | ||||
|     """ Returns a named theme """ | ||||
|     return db.themes.get(session, theme_name) | ||||
|     return db.themes.get(session, id) | ||||
| 
 | ||||
| 
 | ||||
| @router.put("/themes/{theme_name}", status_code=status.HTTP_200_OK) | ||||
| @router.put("/themes/{id}", status_code=status.HTTP_200_OK) | ||||
| def update_theme( | ||||
|     theme_name: str, | ||||
|     id: int, | ||||
|     data: SiteTheme, | ||||
|     session: Session = Depends(generate_session), | ||||
|     current_user=Depends(get_current_user), | ||||
| ): | ||||
|     """ Update a theme database entry """ | ||||
|     db.themes.update(session, theme_name, data.dict()) | ||||
|     db.themes.update(session, id, data.dict()) | ||||
| 
 | ||||
| 
 | ||||
| @router.delete("/themes/{theme_name}", status_code=status.HTTP_200_OK) | ||||
| def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): | ||||
| @router.delete("/themes/{id}", status_code=status.HTTP_200_OK) | ||||
| def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): | ||||
|     """ Deletes theme from the database """ | ||||
|     try: | ||||
|         db.themes.delete(session, theme_name) | ||||
|         db.themes.delete(session, id) | ||||
|     except Exception: | ||||
|         raise HTTPException(status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
| @ -0,0 +1,9 @@ | ||||
| from fastapi import APIRouter | ||||
| 
 | ||||
| from . import auth, crud, sign_up | ||||
| 
 | ||||
| user_router = APIRouter() | ||||
| 
 | ||||
| user_router.include_router(auth.router) | ||||
| user_router.include_router(sign_up.router) | ||||
| user_router.include_router(crud.router) | ||||
| @ -1,9 +0,0 @@ | ||||
| from fastapi import APIRouter | ||||
| from mealie.routes.users import auth, crud, sign_up | ||||
| 
 | ||||
| router = APIRouter() | ||||
| 
 | ||||
| router.include_router(sign_up.router) | ||||
| router.include_router(auth.router) | ||||
| router.include_router(sign_up.router) | ||||
| router.include_router(crud.router) | ||||
| @ -1,3 +1,5 @@ | ||||
| from typing import Optional | ||||
| 
 | ||||
| from pydantic import BaseModel | ||||
| 
 | ||||
| 
 | ||||
| @ -5,7 +7,7 @@ class Colors(BaseModel): | ||||
|     primary: str = "#E58325" | ||||
|     accent: str = "#00457A" | ||||
|     secondary: str = "#973542" | ||||
|     success: str = "#4CAF50" | ||||
|     success: str = "#43A047" | ||||
|     info: str = "#4990BA" | ||||
|     warning: str = "#FF4081" | ||||
|     error: str = "#EF5350" | ||||
| @ -15,6 +17,7 @@ class Colors(BaseModel): | ||||
| 
 | ||||
| 
 | ||||
| class SiteTheme(BaseModel): | ||||
|     id: Optional[int] | ||||
|     name: str = "default" | ||||
|     colors: Colors = Colors() | ||||
| 
 | ||||
|  | ||||
| @ -14,18 +14,19 @@ def default_settings(): | ||||
| 
 | ||||
| @pytest.fixture(scope="session") | ||||
| def default_theme(): | ||||
|     return SiteTheme().dict() | ||||
|     return SiteTheme(id=1).dict() | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture(scope="session") | ||||
| def new_theme(): | ||||
|     return { | ||||
|         "id": 2, | ||||
|         "name": "myTestTheme", | ||||
|         "colors": { | ||||
|             "primary": "#E58325", | ||||
|             "accent": "#00457A", | ||||
|             "secondary": "#973542", | ||||
|             "success": "#5AB1BB", | ||||
|             "success": "#43A047", | ||||
|             "info": "#4990BA", | ||||
|             "warning": "#FF4081", | ||||
|             "error": "#EF5350", | ||||
| @ -54,7 +55,7 @@ def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_ | ||||
| 
 | ||||
| 
 | ||||
| def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme): | ||||
|     response = api_client.get(api_routes.themes_theme_name("default")) | ||||
|     response = api_client.get(api_routes.themes_theme_name(1)) | ||||
|     assert response.status_code == 200 | ||||
|     assert json.loads(response.content) == default_theme | ||||
| 
 | ||||
| @ -64,7 +65,7 @@ def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, | ||||
|     response = api_client.post(api_routes.themes_create, json=new_theme, headers=token) | ||||
|     assert response.status_code == 201 | ||||
| 
 | ||||
|     response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")), headers=token) | ||||
|     response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")), headers=token) | ||||
|     assert response.status_code == 200 | ||||
|     assert json.loads(response.content) == new_theme | ||||
| 
 | ||||
| @ -77,7 +78,7 @@ def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_ | ||||
| 
 | ||||
| def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme): | ||||
|     for theme in [default_theme, new_theme]: | ||||
|         response = api_client.get(api_routes.themes_theme_name(theme.get("name"))) | ||||
|         response = api_client.get(api_routes.themes_theme_name(theme.get("id"))) | ||||
|         assert response.status_code == 200 | ||||
|         assert json.loads(response.content) == theme | ||||
| 
 | ||||
| @ -94,14 +95,14 @@ def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, defa | ||||
|     } | ||||
| 
 | ||||
|     new_theme["colors"] = theme_colors | ||||
|     response = api_client.put(api_routes.themes_theme_name(new_theme.get("name")), json=new_theme, headers=token) | ||||
|     response = api_client.put(api_routes.themes_theme_name(new_theme.get("id")), json=new_theme, headers=token) | ||||
|     assert response.status_code == 200 | ||||
|     response = api_client.get(api_routes.themes_theme_name(new_theme.get("name"))) | ||||
|     response = api_client.get(api_routes.themes_theme_name(new_theme.get("id"))) | ||||
|     assert json.loads(response.content) == new_theme | ||||
| 
 | ||||
| 
 | ||||
| def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token): | ||||
|     for theme in [default_theme, new_theme]: | ||||
|         response = api_client.delete(api_routes.themes_theme_name(theme.get("name")), headers=token) | ||||
|         response = api_client.delete(api_routes.themes_theme_name(theme.get("id")), headers=token) | ||||
| 
 | ||||
|         assert response.status_code == 200 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user