From fea5f8f3d418ad516ab1fb8c5abf76f227438f02 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 2 Sep 2024 16:50:22 -0500 Subject: [PATCH] Update:Batch edit page show confirmation before navigating away with unsaved changes #3369 --- client/components/widgets/BookDetailsEdit.vue | 39 ++++++---- .../components/widgets/PodcastDetailsEdit.vue | 30 +++++--- client/pages/batch/index.vue | 74 ++++++++++--------- 3 files changed, 80 insertions(+), 63 deletions(-) diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue index b42082b5..5fbcaa20 100644 --- a/client/components/widgets/BookDetailsEdit.vue +++ b/client/components/widgets/BookDetailsEdit.vue @@ -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> \ No newline at end of file +</script> diff --git a/client/components/widgets/PodcastDetailsEdit.vue b/client/components/widgets/PodcastDetailsEdit.vue index 4c2fd739..20513ba5 100644 --- a/client/components/widgets/PodcastDetailsEdit.vue +++ b/client/components/widgets/PodcastDetailsEdit.vue @@ -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() diff --git a/client/pages/batch/index.vue b/client/pages/batch/index.vue index 1f119387..5cc83176 100644 --- a/client/pages/batch/index.vue +++ b/client/pages/batch/index.vue @@ -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>