feat: Enable series name editing with duplicate validation

- Remove disabled state from series name input in edit modal
- Add duplicate name validation in SeriesController (resolves TODO)
- Update nameIgnorePrefix when series name changes
- Add frontend validation for duplicate series names
- Add updateSeriesName method to PATCH series immediately on rename
- Show backend error message in toast on failure
- Add i18n string for duplicate name error message

This enables users to rename existing series while preventing duplicate
series names within the same library.
This commit is contained in:
Quentin King 2026-01-04 11:20:38 -06:00
parent 122fc34a75
commit 5431665dfe
4 changed files with 49 additions and 7 deletions

View file

@ -8,7 +8,7 @@
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
<div class="flex">
<div class="grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
</div>
<div class="w-24 sm:w-28 md:w-40 p-1">
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />

View file

@ -19,6 +19,7 @@ export default {
return {
selectedSeries: null,
originalSeriesSequence: null,
originalSeriesName: null,
showSeriesForm: false
}
},
@ -61,6 +62,7 @@ export default {
}
this.originalSeriesSequence = _series.sequence
this.originalSeriesName = _series.name
this.showSeriesForm = true
},
addNewSeries() {
@ -71,6 +73,7 @@ export default {
}
this.originalSeriesSequence = null
this.originalSeriesName = null
this.showSeriesForm = true
},
submitSeriesForm() {
@ -81,6 +84,18 @@ export default {
var existingSeriesIndex = this.seriesItems.findIndex((se) => se.id === this.selectedSeries.id)
// Check if renaming to a name that already exists in the library (different series)
var seriesSameName = this.series.find((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase())
if (seriesSameName && seriesSameName.id !== this.selectedSeries.id) {
// If editing an existing series and trying to rename to an existing name, block it
if (!this.selectedSeries.id.startsWith('new-')) {
this.$toast.error(this.$strings.ToastSeriesDuplicateName)
return
}
// For new series, use the existing series id instead
this.selectedSeries.id = seriesSameName.id
}
var existingSeriesSameName = this.seriesItems.findIndex((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase())
if (existingSeriesSameName >= 0 && existingSeriesIndex < 0) {
console.error('Attempt to add duplicate series')
@ -88,11 +103,6 @@ export default {
return
}
var seriesSameName = this.series.find((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase())
if (existingSeriesIndex < 0 && seriesSameName) {
this.selectedSeries.id = seriesSameName.id
}
var selectedSeriesCopy = { ...this.selectedSeries }
selectedSeriesCopy.displayName = selectedSeriesCopy.sequence ? `${selectedSeriesCopy.name} #${selectedSeriesCopy.sequence}` : selectedSeriesCopy.name
@ -105,7 +115,25 @@ export default {
this.seriesItems = seriesCopy
}
// If this is an existing series (not new), update the series name immediately
if (!this.selectedSeries.id.startsWith('new-')) {
const hasNameChanged = this.originalSeriesName && this.selectedSeries.name !== this.originalSeriesName
if (hasNameChanged) {
this.updateSeriesName(this.selectedSeries.id, this.selectedSeries.name)
}
}
this.showSeriesForm = false
},
async updateSeriesName(seriesId, name) {
try {
await this.$axios.$patch(`/api/series/${seriesId}`, { name })
this.$toast.success(this.$strings.ToastSeriesUpdateSuccess)
} catch (error) {
console.error('Failed to update series name:', error)
const errorMsg = error.response?.data || this.$strings.ToastSeriesUpdateFailed
this.$toast.error(errorMsg)
}
}
}
}

View file

@ -1127,6 +1127,7 @@
"ToastSelectAtLeastOneUser": "Select at least one user",
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesDuplicateName": "A series with that name already exists in this library",
"ToastSeriesSubmitFailedSameName": "Cannot add two series with the same name",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",