mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05: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()
 | 
			
		||||
 | 
			
		||||
@ -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