mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Notification create/update events UI
This commit is contained in:
		
							parent
							
								
									ff04eb8d5e
								
							
						
					
					
						commit
						b08ad8785e
					
				
							
								
								
									
										253
									
								
								client/components/modals/notification/NotificationEditModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								client/components/modals/notification/NotificationEditModal.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,253 @@ | ||||
| <template> | ||||
|   <modals-modal ref="modal" v-model="show" name="notification-edit" :width="800" :height="'unset'" :processing="processing"> | ||||
|     <template #outer> | ||||
|       <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> | ||||
|         <p class="font-book text-3xl text-white truncate">{{ title }}</p> | ||||
|       </div> | ||||
|     </template> | ||||
|     <form @submit.prevent="submitForm"> | ||||
|       <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> | ||||
|         <div class="w-full p-8"> | ||||
|           <ui-dropdown v-model="newNotification.eventName" label="Notification Event" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" /> | ||||
| 
 | ||||
|           <ui-multi-select v-model="newNotification.urls" label="Apprise URL(s)" class="mb-2" /> | ||||
| 
 | ||||
|           <ui-text-input-with-label v-model="newNotification.titleTemplate" label="Title Template" class="mb-2" /> | ||||
| 
 | ||||
|           <ui-textarea-with-label v-model="newNotification.bodyTemplate" label="Body Template" class="mb-2" /> | ||||
| 
 | ||||
|           <div class="flex pt-4"> | ||||
|             <div class="flex items-center"> | ||||
|               <ui-toggle-switch v-model="newNotification.enabled" /> | ||||
|               <p class="text-lg pl-2">Enabled</p> | ||||
|             </div> | ||||
|             <div class="flex-grow" /> | ||||
|             <ui-btn color="success" type="submit">Submit</ui-btn> | ||||
|           </div> | ||||
|         </div> | ||||
|         <!-- <div class="w-full p-8"> | ||||
|           <div class="flex py-2"> | ||||
|             <div class="w-1/2 px-2"> | ||||
|               <ui-text-input-with-label v-model="newUser.username" label="Username" /> | ||||
|             </div> | ||||
|             <div class="w-1/2 px-2"> | ||||
|               <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div v-show="!isEditingRoot" class="flex py-2"> | ||||
|             <div class="px-2 w-52"> | ||||
|               <ui-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" /> | ||||
|             </div> | ||||
|             <div class="flex-grow" /> | ||||
|             <div class="flex items-center pt-4 px-2"> | ||||
|               <p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p> | ||||
|               <ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 px-3 mt-4"> | ||||
|             <p class="text-lg mb-2 font-semibold">Permissions</p> | ||||
|             <div class="flex items-center my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Download</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.download" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Update</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.update" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Delete</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.delete" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Upload</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.upload" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Access Explicit Content</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.accessExplicitContent" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Access All Libraries</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div v-if="!newUser.permissions.accessAllLibraries" class="my-4"> | ||||
|               <ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" label="Libraries Accessible to User" /> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-cen~ter my-2 max-w-md"> | ||||
|               <div class="w-1/2"> | ||||
|                 <p>Can Access All Tags</p> | ||||
|               </div> | ||||
|               <div class="w-1/2"> | ||||
|                 <ui-toggle-switch v-model="newUser.permissions.accessAllTags" @input="accessAllTagsToggled" /> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div v-if="!newUser.permissions.accessAllTags" class="my-4"> | ||||
|               <ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" label="Tags Accessible to User" /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="flex pt-4 px-2"> | ||||
|             <ui-btn v-if="isEditingRoot" to="/account">Change Root Password</ui-btn> | ||||
|             <div class="flex-grow" /> | ||||
|             <ui-btn color="success" type="submit">Submit</ui-btn> | ||||
|           </div> | ||||
|         </div> --> | ||||
|       </div> | ||||
|     </form> | ||||
|   </modals-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean, | ||||
|     notification: { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     }, | ||||
|     notificationData: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       processing: false, | ||||
|       newNotification: {}, | ||||
|       isNew: true | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     show: { | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           this.init() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     show: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     notificationEvents() { | ||||
|       if (!this.notificationData) return [] | ||||
|       return this.notificationData.events || [] | ||||
|     }, | ||||
|     eventOptions() { | ||||
|       return this.notificationEvents.map((e) => ({ value: e.name, text: e.name })) | ||||
|     }, | ||||
|     selectedEventData() { | ||||
|       return this.notificationEvents.find((e) => e.name === this.newNotification.eventName) | ||||
|     }, | ||||
|     showLibrarySelectInput() { | ||||
|       return this.selectedEventData && this.selectedEventData.requiresLibrary | ||||
|     }, | ||||
|     title() { | ||||
|       return this.isNew ? 'Create Notification' : 'Update Notification' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     eventOptionUpdated() { | ||||
|       if (!this.selectedEventData) return | ||||
|       this.newNotification.titleTemplate = this.selectedEventData.defaults.title || '' | ||||
|       this.newNotification.bodyTemplate = this.selectedEventData.defaults.body || '' | ||||
|     }, | ||||
|     close() { | ||||
|       // Force close when navigating - used in UsersTable | ||||
|       if (this.$refs.modal) this.$refs.modal.setHide() | ||||
|     }, | ||||
|     submitForm() { | ||||
|       if (this.isNew) { | ||||
|         this.submitCreate() | ||||
|       } else { | ||||
|         this.submitUpdate() | ||||
|       } | ||||
|     }, | ||||
|     submitUpdate() {}, | ||||
|     submitCreate() { | ||||
|       this.processing = true | ||||
| 
 | ||||
|       const payload = { | ||||
|         ...this.newNotification | ||||
|       } | ||||
|       console.log('Sending create notification', payload) | ||||
|       this.$axios | ||||
|         .$post('/api/notifications/event', payload) | ||||
|         .then(() => { | ||||
|           this.$toast.success('Notification created') | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to create notification', error) | ||||
|           this.$toast.error('Failed to create notification') | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           this.processing = false | ||||
|         }) | ||||
|     }, | ||||
|     init() { | ||||
|       this.isNew = !this.notification | ||||
|       if (this.notification) { | ||||
|         this.newNotification = { | ||||
|           id: this.notification.id, | ||||
|           libraryId: this.notification.libraryId, | ||||
|           eventName: this.notification.eventName, | ||||
|           urls: [...this.notification.urls], | ||||
|           titleTemplate: this.notification.titleTemplate, | ||||
|           bodyTemplate: this.notification.bodyTemplate, | ||||
|           enabled: this.notification.enabled, | ||||
|           type: this.notification.type | ||||
|         } | ||||
|       } else { | ||||
|         this.newNotification = { | ||||
|           libraryId: null, | ||||
|           eventName: 'onTest', | ||||
|           urls: [], | ||||
|           titleTemplate: 'Test Title', | ||||
|           bodyTemplate: 'Test Body', | ||||
|           enabled: true, | ||||
|           type: null | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -10,7 +10,25 @@ | ||||
|           <ui-btn type="submit">Save</ui-btn> | ||||
|         </div> | ||||
|       </form> | ||||
| 
 | ||||
|       <div class="w-full h-px bg-white bg-opacity-10 my-6" /> | ||||
| 
 | ||||
|       <div class="flex items-center justify-between mb-6"> | ||||
|         <h2 class="text-xl font-semibold">Notifications</h2> | ||||
|         <ui-btn small color="success" class="flex items-center" @click="clickCreate">Create <span class="material-icons text-lg pl-2">add</span></ui-btn> | ||||
|       </div> | ||||
| 
 | ||||
|       <div v-if="!notifications.length" class="flex justify-center text-center"> | ||||
|         <p class="text-lg text-gray-200">No notifications</p> | ||||
|       </div> | ||||
|       <template v-for="notification in notifications"> | ||||
|         <div :key="notification.id" class="w-full bg-primary rounded-xl p-4"> | ||||
|           <p>{{ notification.eventName }}</p> | ||||
|         </div> | ||||
|       </template> | ||||
|     </div> | ||||
| 
 | ||||
|     <modals-notification-edit-modal v-model="showEditModal" :notification="selectedNotification" :notification-data="notificationData" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -21,11 +39,18 @@ export default { | ||||
|       loading: false, | ||||
|       appriseApiUrl: null, | ||||
|       notifications: [], | ||||
|       notificationSettings: null | ||||
|       notificationSettings: null, | ||||
|       notificationData: null, | ||||
|       showEditModal: false, | ||||
|       selectedNotification: null | ||||
|     } | ||||
|   }, | ||||
|   computed: {}, | ||||
|   methods: { | ||||
|     clickCreate() { | ||||
|       this.selectedNotification = null | ||||
|       this.showEditModal = true | ||||
|     }, | ||||
|     submitForm() { | ||||
|       if (this.notificationSettings && this.notificationSettings.appriseApiUrl == this.appriseApiUrl) { | ||||
|         return | ||||
| @ -52,18 +77,20 @@ export default { | ||||
|     }, | ||||
|     async init() { | ||||
|       this.loading = true | ||||
|       const notificationSettings = await this.$axios.$get('/api/notifications').catch((error) => { | ||||
|       const notificationResponse = await this.$axios.$get('/api/notifications').catch((error) => { | ||||
|         console.error('Failed to get notification settings', error) | ||||
|         this.$toast.error('Failed to load notification settings') | ||||
|         return null | ||||
|       }) | ||||
|       this.loading = false | ||||
|       if (!notificationSettings) { | ||||
|       if (!notificationResponse) { | ||||
|         return | ||||
|       } | ||||
|       this.notificationSettings = notificationSettings | ||||
|       this.appriseApiUrl = notificationSettings.appriseApiUrl | ||||
|       this.notifications = notificationSettings.notifications || [] | ||||
|       this.notificationSettings = notificationResponse.settings | ||||
|       this.notificationData = notificationResponse.data | ||||
|       console.log('Notification response', notificationResponse) | ||||
|       this.appriseApiUrl = this.notificationSettings.appriseApiUrl | ||||
|       this.notifications = this.notificationSettings.notifications || [] | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|  | ||||
| @ -80,7 +80,7 @@ class Server { | ||||
|     this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager) | ||||
| 
 | ||||
|     // Routers
 | ||||
|     this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.emitter.bind(this), this.clientEmitter.bind(this)) | ||||
|     this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.emitter.bind(this), this.clientEmitter.bind(this)) | ||||
|     this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this)) | ||||
|     this.staticRouter = new StaticRouter(this.db) | ||||
| 
 | ||||
|  | ||||
| @ -4,7 +4,10 @@ class NotificationController { | ||||
|   constructor() { } | ||||
| 
 | ||||
|   get(req, res) { | ||||
|     res.json(this.db.notificationSettings) | ||||
|     res.json({ | ||||
|       data: this.notificationManager.getData(), | ||||
|       settings: this.db.notificationSettings | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async update(req, res) { | ||||
| @ -15,6 +18,28 @@ class NotificationController { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async createEvent(req, res) { | ||||
|     const success = this.db.notificationSettings.addNewEvent(req.body) | ||||
| 
 | ||||
|     if (success) { | ||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async updateEvent(req, res) { | ||||
|     const success = this.db.notificationSettings.updateEvent(req.body) | ||||
| 
 | ||||
|     if (success) { | ||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   getData(req, res) { | ||||
|     res.json(this.notificationManager.getData()) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       return res.sendStatus(404) | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const axios = require('axios') | ||||
| const Logger = require("../Logger") | ||||
| const { notificationData } = require('../utils/notifications') | ||||
| 
 | ||||
| class NotificationManager { | ||||
|   constructor(db) { | ||||
| @ -8,6 +9,10 @@ class NotificationManager { | ||||
|     this.notificationFailedMap = {} | ||||
|   } | ||||
| 
 | ||||
|   getData() { | ||||
|     return notificationData | ||||
|   } | ||||
| 
 | ||||
|   onPodcastEpisodeDownloaded(libraryItem, episode) { | ||||
|     if (!this.db.notificationSettings.isUseable) return | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| const { getId } = require('../utils/index') | ||||
| 
 | ||||
| class Notification { | ||||
|   constructor(notification = null) { | ||||
|     this.id = null | ||||
| @ -42,6 +44,37 @@ class Notification { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setData(payload) { | ||||
|     this.id = getId('noti') | ||||
|     this.libraryId = payload.libraryId || null | ||||
|     this.eventName = payload.eventName | ||||
|     this.urls = payload.urls | ||||
|     this.titleTemplate = payload.titleTemplate | ||||
|     this.bodyTemplate = payload.bodyTemplate | ||||
|     this.enabled = !!payload.enabled | ||||
|     this.type = payload.type || null | ||||
|     this.createdAt = Date.now() | ||||
|   } | ||||
| 
 | ||||
|   update(payload) { | ||||
|     const keysToUpdate = ['libraryId', 'eventName', 'urls', 'titleTemplate', 'bodyTemplate', 'enabled', 'type'] | ||||
|     var hasUpdated = false | ||||
|     for (const key of keysToUpdate) { | ||||
|       if (payload[key]) { | ||||
|         if (key === 'urls') { | ||||
|           if (payload[key].join(',') !== this.urls.join(',')) { | ||||
|             this.urls = [...payload[key]] | ||||
|             hasUpdated = true | ||||
|           } | ||||
|         } else if (payload[key] !== this[key]) { | ||||
|           this[key] = payload[key] | ||||
|           hasUpdated = true | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return hasUpdated | ||||
|   } | ||||
| 
 | ||||
|   parseTitleTemplate(data) { | ||||
|     // TODO: Implement template parsing
 | ||||
|     return 'Test Title' | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| const Notification = require('../Notification') | ||||
| 
 | ||||
| class NotificationSettings { | ||||
|   constructor(settings = null) { | ||||
|     this.id = 'notification-settings' | ||||
| @ -41,5 +43,23 @@ class NotificationSettings { | ||||
|     } | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   addNewEvent(payload) { | ||||
|     if (!payload) return false | ||||
|     // TODO: validate
 | ||||
| 
 | ||||
|     const notification = new Notification() | ||||
|     notification.setData(payload) | ||||
|     this.notifications.push(notification) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   updateEvent(payload) { | ||||
|     if (!payload) return false | ||||
|     const notification = this.notifications.find(n => n.id === payload.id) | ||||
|     if (!notification) return false | ||||
| 
 | ||||
|     return notification.update(payload) | ||||
|   } | ||||
| } | ||||
| module.exports = NotificationSettings | ||||
| @ -26,7 +26,7 @@ const Series = require('../objects/entities/Series') | ||||
| const FileSystemController = require('../controllers/FileSystemController') | ||||
| 
 | ||||
| class ApiRouter { | ||||
|   constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, emitter, clientEmitter) { | ||||
|   constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, emitter, clientEmitter) { | ||||
|     this.db = db | ||||
|     this.auth = auth | ||||
|     this.scanner = scanner | ||||
| @ -40,6 +40,7 @@ class ApiRouter { | ||||
|     this.audioMetadataManager = audioMetadataManager | ||||
|     this.rssFeedManager = rssFeedManager | ||||
|     this.cronManager = cronManager | ||||
|     this.notificationManager = notificationManager | ||||
|     this.emitter = emitter | ||||
|     this.clientEmitter = clientEmitter | ||||
| 
 | ||||
| @ -205,6 +206,9 @@ class ApiRouter { | ||||
|     //
 | ||||
|     this.router.get('/notifications', NotificationController.middleware.bind(this), NotificationController.get.bind(this)) | ||||
|     this.router.patch('/notifications', NotificationController.middleware.bind(this), NotificationController.update.bind(this)) | ||||
|     this.router.post('/notifications/event', NotificationController.middleware.bind(this), NotificationController.createEvent.bind(this)) | ||||
|     this.router.patch('/notifications/event', NotificationController.middleware.bind(this), NotificationController.updateEvent.bind(this)) | ||||
|     this.router.get('/notificationdata', NotificationController.middleware.bind(this), NotificationController.getData.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Misc Routes
 | ||||
|  | ||||
							
								
								
									
										13
									
								
								server/utils/notifications.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/utils/notifications.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| module.exports.notificationData = { | ||||
|   events: [ | ||||
|     { | ||||
|       name: 'onTest', | ||||
|       requiresLibrary: false, | ||||
|       description: 'Notification for testing', | ||||
|       defaults: { | ||||
|         title: 'Test Title', | ||||
|         body: 'Test Body' | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user