mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 10:27:01 -04:00 
			
		
		
		
	Update:Batch edit page show confirmation before navigating away with unsaved changes #3369
This commit is contained in:
		
							parent
							
								
									f9bb529b85
								
							
						
					
					
						commit
						fea5f8f3d4
					
				| @ -3,67 +3,67 @@ | ||||
|     <form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm"> | ||||
|       <div class="flex flex-wrap -mx-1"> | ||||
|         <div class="w-full md:w-1/2 px-1"> | ||||
|           <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" /> | ||||
|           <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1 mt-2 md:mt-0"> | ||||
|           <ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" /> | ||||
|           <ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex flex-wrap mt-2 -mx-1"> | ||||
|         <div class="w-full md:w-3/4 px-1"> | ||||
|           <!-- Authors filter only contains authors in this library, uses filter data --> | ||||
|           <ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" /> | ||||
|           <ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1 mt-2 md:mt-0"> | ||||
|           <ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" /> | ||||
|           <ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex mt-2 -mx-1"> | ||||
|         <div class="flex-grow px-1"> | ||||
|           <widgets-series-input-widget v-model="details.series" /> | ||||
|           <widgets-series-input-widget v-model="details.series" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" /> | ||||
|       <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" /> | ||||
| 
 | ||||
|       <div class="flex flex-wrap mt-2 -mx-1"> | ||||
|         <div class="w-full md:w-1/2 px-1"> | ||||
|           <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" /> | ||||
|           <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1 mt-2 md:mt-0"> | ||||
|           <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" /> | ||||
|           <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex flex-wrap mt-2 -mx-1"> | ||||
|         <div class="w-full md:w-1/2 px-1"> | ||||
|           <ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" /> | ||||
|           <ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> | ||||
|           <ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" /> | ||||
|           <ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> | ||||
|           <ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" /> | ||||
|           <ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex flex-wrap mt-2 -mx-1"> | ||||
|         <div class="w-full md:w-1/4 px-1"> | ||||
|           <ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" /> | ||||
|           <ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> | ||||
|           <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" /> | ||||
|           <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> | ||||
|           <div class="flex justify-center"> | ||||
|             <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> | ||||
|             <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> | ||||
|           <div class="flex justify-center"> | ||||
|             <ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> | ||||
|             <ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -132,6 +132,12 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleInputChange() { | ||||
|       this.$emit('change', { | ||||
|         libraryItemId: this.libraryItem.id, | ||||
|         hasChanges: this.checkForChanges().hasChanges | ||||
|       }) | ||||
|     }, | ||||
|     getDetails() { | ||||
|       this.forceBlur() | ||||
|       return this.checkForChanges() | ||||
| @ -172,6 +178,7 @@ export default { | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       this.handleInputChange() | ||||
|     }, | ||||
|     forceBlur() { | ||||
|       if (this.$refs.titleInput) this.$refs.titleInput.blur() | ||||
| @ -286,4 +293,4 @@ export default { | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| </script> | ||||
|  | ||||
| @ -3,45 +3,45 @@ | ||||
|     <form class="w-full h-full px-4 py-6" @submit.prevent="submitForm"> | ||||
|       <div class="flex -mx-1"> | ||||
|         <div class="w-1/2 px-1"> | ||||
|           <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" /> | ||||
|           <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1"> | ||||
|           <ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" /> | ||||
|           <ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" /> | ||||
|       <ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" /> | ||||
| 
 | ||||
|       <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" /> | ||||
|       <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" /> | ||||
| 
 | ||||
|       <div class="flex mt-2 -mx-1"> | ||||
|         <div class="w-1/2 px-1"> | ||||
|           <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" /> | ||||
|           <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1"> | ||||
|           <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" /> | ||||
|           <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex mt-2 -mx-1"> | ||||
|         <div class="w-1/4 px-1"> | ||||
|           <ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" /> | ||||
|           <ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="w-1/4 px-1"> | ||||
|           <ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" /> | ||||
|           <ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="w-1/4 px-1"> | ||||
|           <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" /> | ||||
|           <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" /> | ||||
|         </div> | ||||
|         <div class="flex-grow px-1 pt-6"> | ||||
|           <div class="flex justify-center"> | ||||
|             <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> | ||||
|             <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex mt-2 -mx-1"> | ||||
|         <div class="w-1/4 px-1"> | ||||
|           <ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" /> | ||||
|           <ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" @input="handleInputChange" /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
| @ -105,6 +105,12 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleInputChange() { | ||||
|       this.$emit('change', { | ||||
|         libraryItemId: this.libraryItem.id, | ||||
|         hasChanges: this.checkForChanges().hasChanges | ||||
|       }) | ||||
|     }, | ||||
|     getDetails() { | ||||
|       this.forceBlur() | ||||
|       return this.checkForChanges() | ||||
| @ -136,6 +142,8 @@ export default { | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.handleInputChange() | ||||
|     }, | ||||
|     forceBlur() { | ||||
|       if (this.$refs.titleInput) this.$refs.titleInput.blur() | ||||
|  | ||||
| @ -97,8 +97,8 @@ | ||||
|     <div class="flex justify-center flex-wrap"> | ||||
|       <template v-for="libraryItem in libraryItemCopies"> | ||||
|         <div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px"> | ||||
|           <widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" /> | ||||
|           <widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" /> | ||||
|           <widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" /> | ||||
|           <widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" /> | ||||
|         </div> | ||||
|       </template> | ||||
|     </div> | ||||
| @ -108,7 +108,7 @@ | ||||
| 
 | ||||
|     <div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }"> | ||||
|       <div class="flex-grow" /> | ||||
|       <ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn> | ||||
|       <ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" :disabled="!hasChanges" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @ -170,7 +170,8 @@ export default { | ||||
|         abridged: false | ||||
|       }, | ||||
|       appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'], | ||||
|       openMapOptions: false | ||||
|       openMapOptions: false, | ||||
|       itemsWithChanges: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -221,9 +222,19 @@ export default { | ||||
|     }, | ||||
|     hasSelectedBatchUsage() { | ||||
|       return Object.values(this.selectedBatchUsage).some((b) => !!b) | ||||
|     }, | ||||
|     hasChanges() { | ||||
|       return this.itemsWithChanges.length > 0 | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleItemChange(itemChange) { | ||||
|       if (!itemChange.hasChanges) { | ||||
|         this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId) | ||||
|       } else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) { | ||||
|         this.itemsWithChanges.push(itemChange.libraryItemId) | ||||
|       } | ||||
|     }, | ||||
|     blurBatchForm() { | ||||
|       if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) { | ||||
|         this.$refs.seriesSelect.forceBlur() | ||||
| @ -283,38 +294,10 @@ export default { | ||||
|     removedSeriesItem(item) {}, | ||||
|     newNarratorItem(item) {}, | ||||
|     removedNarratorItem(item) {}, | ||||
|     newTagItem(item) { | ||||
|       // if (item && !this.newTagItems.includes(item)) { | ||||
|       //   this.newTagItems.push(item) | ||||
|       // } | ||||
|     }, | ||||
|     removedTagItem(item) { | ||||
|       // If newly added, remove if not used on any other items | ||||
|       // if (item && this.newTagItems.includes(item)) { | ||||
|       //   var usedByOtherAb = this.libraryItemCopies.find((ab) => { | ||||
|       //     return ab.tags && ab.tags.includes(item) | ||||
|       //   }) | ||||
|       //   if (!usedByOtherAb) { | ||||
|       //     this.newTagItems = this.newTagItems.filter((t) => t !== item) | ||||
|       //   } | ||||
|       // } | ||||
|     }, | ||||
|     newGenreItem(item) { | ||||
|       // if (item && !this.newGenreItems.includes(item)) { | ||||
|       //   this.newGenreItems.push(item) | ||||
|       // } | ||||
|     }, | ||||
|     removedGenreItem(item) { | ||||
|       // If newly added, remove if not used on any other items | ||||
|       // if (item && this.newGenreItems.includes(item)) { | ||||
|       //   var usedByOtherAb = this.libraryItemCopies.find((ab) => { | ||||
|       //     return ab.book.genres && ab.book.genres.includes(item) | ||||
|       //   }) | ||||
|       //   if (!usedByOtherAb) { | ||||
|       //     this.newGenreItems = this.newGenreItems.filter((t) => t !== item) | ||||
|       //   } | ||||
|       // } | ||||
|     }, | ||||
|     newTagItem(item) {}, | ||||
|     removedTagItem(item) {}, | ||||
|     newGenreItem(item) {}, | ||||
|     removedGenreItem(item) {}, | ||||
|     init() { | ||||
|       // TODO: Better deep cloning of library items | ||||
|       this.libraryItemCopies = this.libraryItems.map((li) => { | ||||
| @ -376,6 +359,7 @@ export default { | ||||
|         .then((data) => { | ||||
|           this.isProcessing = false | ||||
|           if (data.updates) { | ||||
|             this.itemsWithChanges = [] | ||||
|             this.$toast.success(`Successfully updated ${data.updates} items`) | ||||
|             this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`) | ||||
|           } else { | ||||
| @ -387,10 +371,28 @@ export default { | ||||
|           this.$toast.error('Failed to batch update') | ||||
|           this.isProcessing = false | ||||
|         }) | ||||
|     }, | ||||
|     beforeUnload(e) { | ||||
|       if (!e || !this.hasChanges) return | ||||
|       e.preventDefault() | ||||
|       e.returnValue = '' | ||||
|     } | ||||
|   }, | ||||
|   beforeRouteLeave(to, from, next) { | ||||
|     if (this.hasChanges) { | ||||
|       next(false) | ||||
|       window.location = to.path | ||||
|     } else { | ||||
|       next() | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init() | ||||
| 
 | ||||
|     window.addEventListener('beforeunload', this.beforeUnload) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     window.removeEventListener('beforeunload', this.beforeUnload) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user