mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:38:18 -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.formatting.provider": "black", | ||||||
|   "python.pythonPath": ".venv/bin/python3.9", |   "python.pythonPath": ".venv/bin/python3.9", | ||||||
|   "python.linting.pylintEnabled": true, |   "python.linting.pylintEnabled": false, | ||||||
|   "python.linting.enabled": true, |   "python.linting.enabled": true, | ||||||
|   "python.testing.unittestEnabled": false, |   "python.testing.unittestEnabled": false, | ||||||
|   "python.testing.nosetestsEnabled": false, |   "python.testing.nosetestsEnabled": false, | ||||||
|   "python.testing.pytestEnabled": true, |   "python.testing.pytestEnabled": true, | ||||||
|   "python.testing.autoTestDiscoverOnSaveEnabled": false, |   "python.testing.autoTestDiscoverOnSaveEnabled": false, | ||||||
|   "python.testing.pytestArgs": ["tests"], |   "python.testing.pytestArgs": ["tests"], | ||||||
|   "cSpell.enableFiletypes": [ |   "cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"], | ||||||
|     "!javascript", |  | ||||||
|     "!python", |  | ||||||
|     "!yaml" |  | ||||||
|   ], |  | ||||||
|   "i18n-ally.localesPaths": "frontend/src/locales/messages", |   "i18n-ally.localesPaths": "frontend/src/locales/messages", | ||||||
|   "i18n-ally.sourceLanguage": "en-US", |   "i18n-ally.sourceLanguage": "en-US", | ||||||
|   "i18n-ally.enabledFrameworks": ["vue"], |   "i18n-ally.enabledFrameworks": ["vue"], | ||||||
|   "i18n-ally.keystyle": "nested", |   "i18n-ally.keystyle": "nested", | ||||||
|   "cSpell.words": [ |   "cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"], | ||||||
|     "compression", |   "search.mode": "reuseEditor", | ||||||
|     "hkotel", |   "python.linting.flake8Enabled": true | ||||||
|     "performant", |  | ||||||
|     "postgres", |  | ||||||
|     "webp" |  | ||||||
|   ], |  | ||||||
|   "search.mode": "reuseEditor" |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								Caddyfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Caddyfile
									
									
									
									
									
								
							| @ -6,11 +6,11 @@ | |||||||
| :80 { | :80 { | ||||||
|   @proxied path /api/* /docs /openapi.json |   @proxied path /api/* /docs /openapi.json | ||||||
|    |    | ||||||
|   root * /app/dist |   encode gzip zstd | ||||||
|   encode gzip |  | ||||||
|   uri strip_suffix / |   uri strip_suffix / | ||||||
|    |    | ||||||
|   handle_path /api/recipes/media/* { |   # Handles Recipe Images / Assets | ||||||
|  |   handle_path /api/media/recipes/* { | ||||||
|     root * /app/data/recipes/ |     root * /app/data/recipes/ | ||||||
|     file_server |     file_server | ||||||
|   } |   } | ||||||
| @ -20,8 +20,8 @@ | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handle { |   handle { | ||||||
|     try_files {path}.html {path} / |     root * /app/dist | ||||||
|  |     try_files {path}.html {path} /index.html | ||||||
|     file_server  |     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 |       dockerfile: Dockerfile | ||||||
|     container_name: mealie |     container_name: mealie | ||||||
|     restart: always |     restart: always | ||||||
|  |     depends_on: | ||||||
|  |       - "postgres" | ||||||
|     ports: |     ports: | ||||||
|       - 9090:80 |       - 9090:80 | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -135,14 +135,18 @@ export const recipeAPI = { | |||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   recipeImage(recipeSlug) { |   recipeImage(recipeSlug) { | ||||||
|     return `/api/recipes/media/${recipeSlug}/images/original.webp`; |     return `/api/media/recipes/${recipeSlug}/images/original.webp`; | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   recipeSmallImage(recipeSlug) { |   recipeSmallImage(recipeSlug) { | ||||||
|     return `/api/recipes/media/${recipeSlug}/images/min-original.webp`; |     return `/api/media/recipes/${recipeSlug}/images/min-original.webp`; | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   recipeTinyImage(recipeSlug) { |   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 = { | const settingsURLs = { | ||||||
|   allThemes: `${baseURL}themes`, |   allThemes: `${baseURL}themes`, | ||||||
|   specificTheme: themeName => `${prefix}/${themeName}`, |   specificTheme: id => `${prefix}/${id}`, | ||||||
|   createTheme: `${prefix}/create`, |   createTheme: `${prefix}/create`, | ||||||
|   updateTheme: themeName => `${prefix}/${themeName}`, |   updateTheme: id => `${prefix}/${id}`, | ||||||
|   deleteTheme: themeName => `${prefix}/${themeName}`, |   deleteTheme: id => `${prefix}/${id}`, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const themeAPI = { | export const themeAPI = { | ||||||
| @ -32,22 +32,18 @@ export const themeAPI = { | |||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   update(themeName, colors) { |   update(data) { | ||||||
|     const body = { |  | ||||||
|       name: themeName, |  | ||||||
|       colors: colors, |  | ||||||
|     }; |  | ||||||
|     return apiReq.put( |     return apiReq.put( | ||||||
|       settingsURLs.updateTheme(themeName), |       settingsURLs.updateTheme(data.id), | ||||||
|       body, |       data, | ||||||
|       () => i18n.t("settings.theme.error-updating-theme"), |       () => i18n.t("settings.theme.error-updating-theme"), | ||||||
|       () => i18n.t("settings.theme.theme-updated") |       () => i18n.t("settings.theme.theme-updated") | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   delete(themeName) { |   delete(id) { | ||||||
|     return apiReq.delete( |     return apiReq.delete( | ||||||
|       settingsURLs.deleteTheme(themeName), |       settingsURLs.deleteTheme(id), | ||||||
|       null, |       null, | ||||||
|       () => i18n.t("settings.theme.error-deleting-theme"), |       () => i18n.t("settings.theme.error-deleting-theme"), | ||||||
|       () => i18n.t("settings.theme.theme-deleted") |       () => i18n.t("settings.theme.theme-deleted") | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
|     <div class="text-center"> |     <div class="text-center"> | ||||||
|       <h3>{{ buttonText }}</h3> |       <h3>{{ buttonText }}</h3> | ||||||
|     </div> |     </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> |       <template v-slot:append> | ||||||
|         <v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false"> |         <v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false"> | ||||||
|           <template v-slot:activator="{ on }"> |           <template v-slot:activator="{ on }"> | ||||||
| @ -17,15 +17,7 @@ | |||||||
|         </v-menu> |         </v-menu> | ||||||
|       </template> |       </template> | ||||||
|     </v-text-field> |     </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> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <v-checkbox |     <v-checkbox | ||||||
|       v-for="option in options" |       v-for="(option, index) in options" | ||||||
|       :key="option.text" |       :key="index" | ||||||
|       class="mb-n4 mt-n3" |       class="mb-n4 mt-n3" | ||||||
|       dense |       dense | ||||||
|       :label="option.text" |       :label="option.text" | ||||||
| @ -62,5 +62,3 @@ export default { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </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-title class="pl-2" v-text="item.name"></v-list-item-title> | ||||||
|           </v-list-item-content> |           </v-list-item-content> | ||||||
|           <v-list-item-action> |           <v-list-item-action> | ||||||
|             <v-btn |             <v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top> | ||||||
|               v-if="!edit" |  | ||||||
|               color="primary" |  | ||||||
|               icon |  | ||||||
|               :href="`/api/recipes/media/${slug}/assets/${item.fileName}`" |  | ||||||
|               target="_blank" |  | ||||||
|               top |  | ||||||
|             > |  | ||||||
|               <v-icon> mdi-download</v-icon> |               <v-icon> mdi-download</v-icon> | ||||||
|             </v-btn> |             </v-btn> | ||||||
|             <div v-else> |             <div v-else> | ||||||
| @ -118,6 +111,9 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     assetURL(assetName) { | ||||||
|  |       return api.recipes.recipeAssetPath(this.slug, assetName); | ||||||
|  |     }, | ||||||
|     setFileObject(obj) { |     setFileObject(obj) { | ||||||
|       this.fileObject = obj; |       this.fileObject = obj; | ||||||
|     }, |     }, | ||||||
| @ -135,7 +131,8 @@ export default { | |||||||
|       this.value.splice(index, 1); |       this.value.splice(index, 1); | ||||||
|     }, |     }, | ||||||
|     copyLink(name, fileName) { |     copyLink(name, fileName) { | ||||||
|       const copyText = ``; |       const assetLink = api.recipes.recipeAssetPath(this.slug, fileName); | ||||||
|  |       const copyText = ``; | ||||||
|       navigator.clipboard.writeText(copyText).then( |       navigator.clipboard.writeText(copyText).then( | ||||||
|         () => console.log("Copied", copyText), |         () => console.log("Copied", copyText), | ||||||
|         () => console.log("Copied Failed", copyText) |         () => console.log("Copied Failed", copyText) | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ | |||||||
|             color="secondary darken-1" |             color="secondary darken-1" | ||||||
|             class="rounded-sm static" |             class="rounded-sm static" | ||||||
|           > |           > | ||||||
|             {{ yields }} |             {{ recipe.yields }} | ||||||
|           </v-btn> |           </v-btn> | ||||||
|         </v-col> |         </v-col> | ||||||
|         <Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" /> |         <Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" /> | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|   <v-form ref="file"> |   <v-form ref="file"> | ||||||
|     <input ref="uploader" class="d-none" type="file" @change="onFileChanged" /> |     <input ref="uploader" class="d-none" type="file" @change="onFileChanged" /> | ||||||
|     <slot v-bind="{ isSelecting, onButtonClick }"> |     <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> |         <v-icon left> {{ icon }}</v-icon> | ||||||
|         {{ text ? text : defaultText }} |         {{ text ? text : defaultText }} | ||||||
|       </v-btn> |       </v-btn> | ||||||
| @ -15,6 +15,9 @@ const UPLOAD_EVENT = "uploaded"; | |||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|  |     small: { | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|     post: { |     post: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: true, |       default: true, | ||||||
|  | |||||||
| @ -1,18 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="mt-n5" v-if="recipes"> |   <div v-if="recipes"> | ||||||
|     <v-card flat class="transparent" height="60px"> |     <v-app-bar color="transparent" flat class="mt-n1 rounded" v-if="!disableToolbar"> | ||||||
|       <v-card-text> |       <v-icon large left v-if="title"> | ||||||
|         <v-row v-if="title != null"> |         {{ titleIcon }} | ||||||
|           <v-col> |       </v-icon> | ||||||
|             <v-btn-toggle group> |       <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> | ||||||
|               <v-btn text> |  | ||||||
|                 {{ title.toUpperCase() }} |  | ||||||
|               </v-btn> |  | ||||||
|             </v-btn-toggle> |  | ||||||
|           </v-col> |  | ||||||
|       <v-spacer></v-spacer> |       <v-spacer></v-spacer> | ||||||
|           <v-col align="end"> |       <v-menu offset-y v-if="$listeners.sortRecent || $listeners.sort"> | ||||||
|             <v-menu offset-y v-if="sortable"> |  | ||||||
|         <template v-slot:activator="{ on, attrs }"> |         <template v-slot:activator="{ on, attrs }"> | ||||||
|           <v-btn-toggle group> |           <v-btn-toggle group> | ||||||
|             <v-btn text v-bind="attrs" v-on="on"> |             <v-btn text v-bind="attrs" v-on="on"> | ||||||
| @ -21,7 +15,7 @@ | |||||||
|           </v-btn-toggle> |           </v-btn-toggle> | ||||||
|         </template> |         </template> | ||||||
|         <v-list> |         <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-title>{{ $t("general.recent") }}</v-list-item-title> | ||||||
|           </v-list-item> |           </v-list-item> | ||||||
|           <v-list-item @click="$emit('sort')"> |           <v-list-item @click="$emit('sort')"> | ||||||
| @ -29,11 +23,8 @@ | |||||||
|           </v-list-item> |           </v-list-item> | ||||||
|         </v-list> |         </v-list> | ||||||
|       </v-menu> |       </v-menu> | ||||||
|           </v-col> |     </v-app-bar> | ||||||
|         </v-row> |     <div v-if="recipes" class="mt-2"> | ||||||
|       </v-card-text> |  | ||||||
|     </v-card> |  | ||||||
|     <div v-if="recipes"> |  | ||||||
|       <v-row v-if="!viewScale"> |       <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"> |         <v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name"> | ||||||
|           <RecipeCard |           <RecipeCard | ||||||
| @ -91,9 +82,12 @@ export default { | |||||||
|     MobileRecipeCard, |     MobileRecipeCard, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     sortable: { |     disableToolbar: { | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|  |     titleIcon: { | ||||||
|  |       default: "mdi-tag-multiple-outline", | ||||||
|  |     }, | ||||||
|     title: { |     title: { | ||||||
|       default: null, |       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> |     <slot name="open" v-bind="{ open }"> </slot> | ||||||
|     <v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined"> |     <v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined"> | ||||||
|       <v-card class="pb-10" height="100%"> |       <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> |           <v-icon large left> | ||||||
|             {{ titleIcon }} |             {{ titleIcon }} | ||||||
|           </v-icon> |           </v-icon> | ||||||
|           <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> |           <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> | ||||||
|           <v-spacer></v-spacer> |           <v-spacer></v-spacer> | ||||||
|         </v-app-bar> |         </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> |         <slot> </slot> | ||||||
|         <v-card-actions> |         <v-card-actions> | ||||||
|           <slot name="card-actions"> |           <slot name="card-actions"> | ||||||
| @ -18,8 +18,12 @@ | |||||||
|               {{ $t("general.cancel") }} |               {{ $t("general.cancel") }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|             <v-spacer></v-spacer> |             <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"> |             <v-btn color="success" @click="submitEvent"> | ||||||
|               {{ $t("general.submit") }} |               {{ submitText }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|           </slot> |           </slot> | ||||||
|         </v-card-actions> |         </v-card-actions> | ||||||
| @ -31,6 +35,7 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
|  | import i18n from "@/i18n"; | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     color: { |     color: { | ||||||
| @ -51,16 +56,34 @@ export default { | |||||||
|     top: { |     top: { | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|  |     submitText: { | ||||||
|  |       default: () => i18n.t("general.create"), | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       dialog: false, |       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: { |   methods: { | ||||||
|     submitEvent() { |     submitEvent() { | ||||||
|       this.$emit("submit"); |       this.$emit("submit"); | ||||||
|       this.close(); |       this.submitted = true; | ||||||
|     }, |     }, | ||||||
|     open() { |     open() { | ||||||
|       this.dialog = true; |       this.dialog = true; | ||||||
| @ -68,6 +91,10 @@ export default { | |||||||
|     close() { |     close() { | ||||||
|       this.dialog = false; |       this.dialog = false; | ||||||
|     }, |     }, | ||||||
|  |     deleteEvent() { | ||||||
|  |       this.$emit("delete"); | ||||||
|  |       this.submitted = true; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -48,7 +48,7 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import ImportOptions from "./ImportOptions"; | import ImportOptions from "@/components/FormHelpers/ImportOptions"; | ||||||
| import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue"; | import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue"; | ||||||
| import { backupURLs } from "@/api/backup"; | import { backupURLs } from "@/api/backup"; | ||||||
| export default { | export default { | ||||||
| @ -1,6 +1,7 @@ | |||||||
| w<template> | w<template> | ||||||
|   <v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3"> |   <v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3"> | ||||||
|     <div class="d-flex grow flex-wrap"> |     <div class="d-flex grow flex-wrap"> | ||||||
|  |       <slot name="avatar"> | ||||||
|         <v-sheet |         <v-sheet | ||||||
|           :color="color" |           :color="color" | ||||||
|           :max-height="icon ? 90 : undefined" |           :max-height="icon ? 90 : undefined" | ||||||
| @ -12,6 +13,7 @@ w<template> | |||||||
|           <v-icon v-if="icon" size="40" v-text="icon" /> |           <v-icon v-if="icon" size="40" v-text="icon" /> | ||||||
|           <div v-if="text" class="headline font-weight-thin" v-text="text" /> |           <div v-if="text" class="headline font-weight-thin" v-text="text" /> | ||||||
|         </v-sheet> |         </v-sheet> | ||||||
|  |       </slot> | ||||||
| 
 | 
 | ||||||
|       <div v-if="$slots['after-heading']" class="ml-auto"> |       <div v-if="$slots['after-heading']" class="ml-auto"> | ||||||
|         <slot name="after-heading" /> |         <slot name="after-heading" /> | ||||||
| @ -29,7 +31,7 @@ w<template> | |||||||
|     </template> |     </template> | ||||||
| 
 | 
 | ||||||
|     <template v-if="$slots.bottom"> |     <template v-if="$slots.bottom"> | ||||||
|       <v-divider class="mt-2" /> |       <v-divider class="mt-2" v-if="!$slots.actions" /> | ||||||
| 
 | 
 | ||||||
|       <div class="pb-0"> |       <div class="pb-0"> | ||||||
|         <slot name="bottom" /> |         <slot name="bottom" /> | ||||||
| @ -73,6 +75,7 @@ export default { | |||||||
|     classes() { |     classes() { | ||||||
|       return { |       return { | ||||||
|         "v-card--material--has-heading": this.hasHeading, |         "v-card--material--has-heading": this.hasHeading, | ||||||
|  |         "mt-3": this.$vuetify.breakpoint.name == "xs" || this.$vuetify.breakpoint.name == "sm" | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|     hasHeading() { |     hasHeading() { | ||||||
| @ -133,11 +133,6 @@ export default { | |||||||
|           to: "/admin/profile", |           to: "/admin/profile", | ||||||
|           title: this.$t("settings.profile"), |           title: this.$t("settings.profile"), | ||||||
|         }, |         }, | ||||||
|         { |  | ||||||
|           icon: "mdi-format-color-fill", |  | ||||||
|           to: "/admin/themes", |  | ||||||
|           title: this.$t("general.themes"), |  | ||||||
|         }, |  | ||||||
|         { |         { | ||||||
|           icon: "mdi-food", |           icon: "mdi-food", | ||||||
|           to: "/admin/meal-planner", |           to: "/admin/meal-planner", | ||||||
| @ -167,11 +162,6 @@ export default { | |||||||
|           to: "/admin/manage-users", |           to: "/admin/manage-users", | ||||||
|           title: this.$t("settings.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", |           icon: "mdi-database-import", | ||||||
|           to: "/admin/migrations", |           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> |             </v-btn> | ||||||
|           </template> |           </template> | ||||||
|         </TheUploadBtn> |         </TheUploadBtn> | ||||||
|  |         <BackupDialog :color="color" /> | ||||||
|  | 
 | ||||||
|         <v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup"> |         <v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup"> | ||||||
|           <v-icon left> mdi-plus </v-icon> Create |           <v-icon left> mdi-plus </v-icon> Create | ||||||
|         </v-btn> |         </v-btn> | ||||||
| @ -36,7 +38,7 @@ | |||||||
|             <v-list-item @click.prevent="openDialog(item)"> |             <v-list-item @click.prevent="openDialog(item)"> | ||||||
|               <v-list-item-avatar> |               <v-list-item-avatar> | ||||||
|                 <v-icon large dark :color="color"> |                 <v-icon large dark :color="color"> | ||||||
|                   mdi-backup-restore |                   mdi-database | ||||||
|                 </v-icon> |                 </v-icon> | ||||||
|               </v-list-item-avatar> |               </v-list-item-avatar> | ||||||
| 
 | 
 | ||||||
| @ -65,13 +67,14 @@ | |||||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||||
| import ImportSummaryDialog from "@/components/ImportSummaryDialog"; | import ImportSummaryDialog from "@/components/ImportSummaryDialog"; | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import StatCard from "./StatCard"; | import StatCard from "@/components/UI/StatCard"; | ||||||
| import ImportDialog from "../Backup/ImportDialog"; | import BackupDialog from "@/components/UI/Dialogs/BackupDialog"; | ||||||
|  | import ImportDialog from "@/components/UI/Dialogs/ImportDialog"; | ||||||
| export default { | export default { | ||||||
|   components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog }, |   components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog, BackupDialog }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       color: "secondary", |       color: "accent", | ||||||
|       selectedName: "", |       selectedName: "", | ||||||
|       selectedDate: "", |       selectedDate: "", | ||||||
|       loading: false, |       loading: false, | ||||||
| @ -91,7 +94,6 @@ export default { | |||||||
|     async getAvailableBackups() { |     async getAvailableBackups() { | ||||||
|       const response = await api.backups.requestAvailable(); |       const response = await api.backups.requestAvailable(); | ||||||
|       this.availableBackups = response.imports; |       this.availableBackups = response.imports; | ||||||
|       console.log(this.availableBackups); |  | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     async deleteBackup(name) { |     async deleteBackup(name) { | ||||||
| @ -106,6 +108,7 @@ export default { | |||||||
|       this.selectedName = backup.name; |       this.selectedName = backup.name; | ||||||
|       this.$refs.import_dialog.open(); |       this.$refs.import_dialog.open(); | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|     async importBackup(data) { |     async importBackup(data) { | ||||||
|       this.loading = true; |       this.loading = true; | ||||||
|       const response = await api.backups.import(data.name, data); |       const response = await api.backups.import(data.name, data); | ||||||
|  | |||||||
| @ -49,12 +49,12 @@ | |||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import StatCard from "./StatCard"; | import StatCard from "@/components/UI/StatCard"; | ||||||
| export default { | export default { | ||||||
|   components: { StatCard }, |   components: { StatCard }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       color: "secondary", |       color: "accent", | ||||||
|       total: 0, |       total: 0, | ||||||
|       events: [], |       events: [], | ||||||
|       icons: { |       icons: { | ||||||
|  | |||||||
| @ -82,7 +82,7 @@ | |||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import StatCard from "./StatCard"; | import StatCard from "@/components/UI/StatCard"; | ||||||
| import EventViewer from "./EventViewer"; | import EventViewer from "./EventViewer"; | ||||||
| import BackupViewer from "./BackupViewer"; | import BackupViewer from "./BackupViewer"; | ||||||
| export default { | 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> | <template> | ||||||
|   <v-row dense> |   <div class="mt-10"> | ||||||
|     <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> |  | ||||||
|     <v-row> |     <v-row> | ||||||
|             <v-col cols="12" md="3" align="center" justify="center"> |       <v-col cols="12" sm="12" lg="6"> | ||||||
|               <v-avatar color="accent" size="120" class="white--text headline mr-2"> |         <UserCard /> | ||||||
|                 <img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" /> |       </v-col> | ||||||
|                 <div v-else> |       <v-col cols="12" sm="12" lg="6"> </v-col> | ||||||
|                   {{ initials }} |     </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> |   </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> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| // import AvatarPicker from '@/components/AvatarPicker' | import ThemeCard from "./ThemeCard"; | ||||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | import UserCard from "./UserCard"; | ||||||
| import { api } from "@/api"; |  | ||||||
| import { validators } from "@/mixins/validators"; |  | ||||||
| import { initials } from "@/mixins/initials"; |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     TheUploadBtn, |     UserCard, | ||||||
|   }, |     ThemeCard, | ||||||
|   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> | </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> | <template> | ||||||
|   <v-container> |   <v-container> | ||||||
|     <CardSection |     <CardSection | ||||||
|  |       title-icon="" | ||||||
|       v-if="siteSettings.showRecent" |       v-if="siteSettings.showRecent" | ||||||
|       :title="$t('page.recent')" |       :title="$t('page.recent')" | ||||||
|       :recipes="recentRecipes" |       :recipes="recentRecipes" | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
|   <v-container> |   <v-container> | ||||||
|     <v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear> |     <v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear> | ||||||
|     <CardSection |     <CardSection | ||||||
|  |       title-icon="" | ||||||
|       :sortable="true" |       :sortable="true" | ||||||
|       :title="$t('page.all-recipes')" |       :title="$t('page.all-recipes')" | ||||||
|       :recipes="allRecipes" |       :recipes="allRecipes" | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| import Admin from "@/pages/Admin"; | import Admin from "@/pages/Admin"; | ||||||
| import Backup from "@/pages/Admin/Backup"; |  | ||||||
| import Theme from "@/pages/Admin/Theme"; |  | ||||||
| import MealPlanner from "@/pages/Admin/MealPlanner"; | import MealPlanner from "@/pages/Admin/MealPlanner"; | ||||||
| import Migration from "@/pages/Admin/Migration"; | import Migration from "@/pages/Admin/Migration"; | ||||||
| import Profile from "@/pages/Admin/Profile"; | import Profile from "@/pages/Admin/Profile"; | ||||||
| @ -31,21 +29,6 @@ export const adminRoutes = { | |||||||
|         title: "settings.profile", |         title: "settings.profile", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     { |  | ||||||
|       path: "backups", |  | ||||||
|       component: Backup, |  | ||||||
|       meta: { |  | ||||||
|         title: "settings.backup-and-exports", |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       path: "themes", |  | ||||||
|       component: Theme, |  | ||||||
|       meta: { |  | ||||||
|         title: "general.themes", |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     { |     { | ||||||
|       path: "meal-planner", |       path: "meal-planner", | ||||||
|       component: MealPlanner, |       component: MealPlanner, | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ export const routes = [ | |||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const router = new VueRouter({ | const router = new VueRouter({ | ||||||
|  |   base: process.env.BASE_URL, | ||||||
|   routes, |   routes, | ||||||
|   mode: process.env.NODE_ENV === "production" ? "history" : "hash", |   mode: process.env.NODE_ENV === "production" ? "history" : "hash", | ||||||
|   scrollBehavior() { |   scrollBehavior() { | ||||||
|  | |||||||
| @ -60,7 +60,7 @@ const actions = { | |||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   async resetTheme({ commit }) { |   async resetTheme({ commit }) { | ||||||
|     const defaultTheme = await api.themes.requestByName("default"); |     const defaultTheme = await api.themes.requestByName(1); | ||||||
|     if (defaultTheme.colors) { |     if (defaultTheme.colors) { | ||||||
|       Vuetify.framework.theme.themes.dark = defaultTheme.colors; |       Vuetify.framework.theme.themes.dark = defaultTheme.colors; | ||||||
|       Vuetify.framework.theme.themes.light = defaultTheme.colors; |       Vuetify.framework.theme.themes.light = defaultTheme.colors; | ||||||
|  | |||||||
| @ -1,18 +1,19 @@ | |||||||
| import uvicorn | import uvicorn | ||||||
| from fastapi import FastAPI | from fastapi import FastAPI | ||||||
| 
 | 
 | ||||||
| from mealie.core import root_logger |  | ||||||
| from mealie.core.config import APP_VERSION, settings | 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 import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes | ||||||
| from mealie.routes.about import about_router | from mealie.routes.about import about_router | ||||||
| from mealie.routes.groups import groups | from mealie.routes.groups import groups_router | ||||||
| from mealie.routes.mealplans import mealplans | from mealie.routes.mealplans import meal_plan_router | ||||||
| from mealie.routes.recipe import router as recipe_router | from mealie.routes.media import media_router | ||||||
| from mealie.routes.site_settings import all_settings | from mealie.routes.recipe import recipe_router | ||||||
| from mealie.routes.users import users | from mealie.routes.site_settings import settings_router | ||||||
|  | from mealie.routes.users import user_router | ||||||
| from mealie.services.events import create_general_event | from mealie.services.events import create_general_event | ||||||
| 
 | 
 | ||||||
| logger = root_logger.get_logger() | logger = get_logger() | ||||||
| 
 | 
 | ||||||
| app = FastAPI( | app = FastAPI( | ||||||
|     title="Mealie", |     title="Mealie", | ||||||
| @ -29,15 +30,16 @@ def start_scheduler(): | |||||||
| 
 | 
 | ||||||
| def api_routers(): | def api_routers(): | ||||||
|     # Authentication |     # Authentication | ||||||
|     app.include_router(users.router) |     app.include_router(user_router) | ||||||
|     app.include_router(groups.router) |     app.include_router(groups_router) | ||||||
|     # Recipes |     # Recipes | ||||||
|     app.include_router(recipe_router) |     app.include_router(recipe_router) | ||||||
|  |     app.include_router(media_router) | ||||||
|     app.include_router(about_router) |     app.include_router(about_router) | ||||||
|     # Meal Routes |     # Meal Routes | ||||||
|     app.include_router(mealplans.router) |     app.include_router(meal_plan_router) | ||||||
|     # Settings Routes |     # Settings Routes | ||||||
|     app.include_router(all_settings.router) |     app.include_router(settings_router) | ||||||
|     app.include_router(theme_routes.router) |     app.include_router(theme_routes.router) | ||||||
|     # Backups/Imports Routes |     # Backups/Imports Routes | ||||||
|     app.include_router(backup_routes.router) |     app.include_router(backup_routes.router) | ||||||
|  | |||||||
| @ -93,7 +93,7 @@ class _Settings(BaseDocument): | |||||||
| 
 | 
 | ||||||
| class _Themes(BaseDocument): | class _Themes(BaseDocument): | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|         self.primary_key = "name" |         self.primary_key = "id" | ||||||
|         self.sql_model = SiteThemeModel |         self.sql_model = SiteThemeModel | ||||||
|         self.schema = SiteTheme |         self.schema = SiteTheme | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,23 +1,21 @@ | |||||||
| import sqlalchemy as sa | import sqlalchemy as sa | ||||||
| import sqlalchemy.orm as orm | 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" |     __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") |     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.name = name | ||||||
|         self.colors = ThemeColorsModel(**colors) |         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, BaseMixins): | ||||||
| class ThemeColorsModel(SqlAlchemyBase): |  | ||||||
|     __tablename__ = "theme_colors" |     __tablename__ = "theme_colors" | ||||||
|     id = sa.Column(sa.Integer, primary_key=True) |     id = sa.Column(sa.Integer, primary_key=True) | ||||||
|     parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name")) |     parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name")) | ||||||
| @ -28,21 +26,3 @@ class ThemeColorsModel(SqlAlchemyBase): | |||||||
|     info = sa.Column(sa.String) |     info = sa.Column(sa.String) | ||||||
|     warning = sa.Column(sa.String) |     warning = sa.Column(sa.String) | ||||||
|     error = 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 fastapi import APIRouter | ||||||
| 
 | 
 | ||||||
| from .events import router as events_router | from . import events | ||||||
| 
 | 
 | ||||||
| about_router = APIRouter(prefix="/api/about") | 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 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) | recipe_router.include_router(all_recipe_routes.router) | ||||||
| router.include_router(recipe_crud_routes.router) | recipe_router.include_router(recipe_crud_routes.router) | ||||||
| router.include_router(recipe_media.router) | recipe_router.include_router(category_routes.router) | ||||||
| router.include_router(category_routes.router) | recipe_router.include_router(tag_routes.router) | ||||||
| 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 import APIRouter, Depends, File, Form, HTTPException, status | ||||||
|  | from fastapi.datastructures import UploadFile | ||||||
| from mealie.core.root_logger import get_logger | from mealie.core.root_logger import get_logger | ||||||
| from mealie.db.database import db | from mealie.db.database import db | ||||||
| from mealie.db.db_setup import generate_session | from mealie.db.db_setup import generate_session | ||||||
| from mealie.routes.deps import get_current_user | 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.events import create_recipe_event | ||||||
| from mealie.services.image.image import scrape_image, write_image | from mealie.services.image.image import scrape_image, write_image | ||||||
| from mealie.services.recipe.media import check_assets, delete_assets | from mealie.services.recipe.media import check_assets, delete_assets | ||||||
| from mealie.services.scraper.scraper import create_from_url | from mealie.services.scraper.scraper import create_from_url | ||||||
|  | from slugify import slugify | ||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
| 
 | 
 | ||||||
| router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"]) | 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. """ |     """ Removes an existing image and replaces it with the incoming file. """ | ||||||
| 
 | 
 | ||||||
|     scrape_image(url.url, recipe_slug) |     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.database import db | ||||||
| from mealie.db.db_setup import generate_session | from mealie.db.db_setup import generate_session | ||||||
| from mealie.routes.deps import get_current_user | 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()) |     db.themes.create(session, data.dict()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @router.get("/themes/{theme_name}") | @router.get("/themes/{id}") | ||||||
| def get_single_theme(theme_name: str, session: Session = Depends(generate_session)): | def get_single_theme(id: int, session: Session = Depends(generate_session)): | ||||||
|     """ Returns a named theme """ |     """ 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( | def update_theme( | ||||||
|     theme_name: str, |     id: int, | ||||||
|     data: SiteTheme, |     data: SiteTheme, | ||||||
|     session: Session = Depends(generate_session), |     session: Session = Depends(generate_session), | ||||||
|     current_user=Depends(get_current_user), |     current_user=Depends(get_current_user), | ||||||
| ): | ): | ||||||
|     """ Update a theme database entry """ |     """ 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) | @router.delete("/themes/{id}", status_code=status.HTTP_200_OK) | ||||||
| def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): | def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): | ||||||
|     """ Deletes theme from the database """ |     """ Deletes theme from the database """ | ||||||
|     try: |     try: | ||||||
|         db.themes.delete(session, theme_name) |         db.themes.delete(session, id) | ||||||
|     except Exception: |     except Exception: | ||||||
|         raise HTTPException(status.HTTP_400_BAD_REQUEST) |         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 | from pydantic import BaseModel | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -5,7 +7,7 @@ class Colors(BaseModel): | |||||||
|     primary: str = "#E58325" |     primary: str = "#E58325" | ||||||
|     accent: str = "#00457A" |     accent: str = "#00457A" | ||||||
|     secondary: str = "#973542" |     secondary: str = "#973542" | ||||||
|     success: str = "#4CAF50" |     success: str = "#43A047" | ||||||
|     info: str = "#4990BA" |     info: str = "#4990BA" | ||||||
|     warning: str = "#FF4081" |     warning: str = "#FF4081" | ||||||
|     error: str = "#EF5350" |     error: str = "#EF5350" | ||||||
| @ -15,6 +17,7 @@ class Colors(BaseModel): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SiteTheme(BaseModel): | class SiteTheme(BaseModel): | ||||||
|  |     id: Optional[int] | ||||||
|     name: str = "default" |     name: str = "default" | ||||||
|     colors: Colors = Colors() |     colors: Colors = Colors() | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,18 +14,19 @@ def default_settings(): | |||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope="session") | @pytest.fixture(scope="session") | ||||||
| def default_theme(): | def default_theme(): | ||||||
|     return SiteTheme().dict() |     return SiteTheme(id=1).dict() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope="session") | @pytest.fixture(scope="session") | ||||||
| def new_theme(): | def new_theme(): | ||||||
|     return { |     return { | ||||||
|  |         "id": 2, | ||||||
|         "name": "myTestTheme", |         "name": "myTestTheme", | ||||||
|         "colors": { |         "colors": { | ||||||
|             "primary": "#E58325", |             "primary": "#E58325", | ||||||
|             "accent": "#00457A", |             "accent": "#00457A", | ||||||
|             "secondary": "#973542", |             "secondary": "#973542", | ||||||
|             "success": "#5AB1BB", |             "success": "#43A047", | ||||||
|             "info": "#4990BA", |             "info": "#4990BA", | ||||||
|             "warning": "#FF4081", |             "warning": "#FF4081", | ||||||
|             "error": "#EF5350", |             "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): | 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 response.status_code == 200 | ||||||
|     assert json.loads(response.content) == default_theme |     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) |     response = api_client.post(api_routes.themes_create, json=new_theme, headers=token) | ||||||
|     assert response.status_code == 201 |     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 response.status_code == 200 | ||||||
|     assert json.loads(response.content) == new_theme |     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): | def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme): | ||||||
|     for theme in [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 response.status_code == 200 | ||||||
|         assert json.loads(response.content) == theme |         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 |     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 |     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 |     assert json.loads(response.content) == new_theme | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token): | def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token): | ||||||
|     for theme in [default_theme, new_theme]: |     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 |         assert response.status_code == 200 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user