feat(webui): edit book links

This commit is contained in:
Gauthier Roebroeck 2021-12-29 16:20:52 +08:00
parent 394123d263
commit 719554766c
4 changed files with 138 additions and 6 deletions

View file

@ -35,6 +35,10 @@
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon> <v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
{{ $t('dialog.edit_books.tab_tags') }} {{ $t('dialog.edit_books.tab_tags') }}
</v-tab> </v-tab>
<v-tab class="justify-start" v-if="single">
<v-icon left class="hidden-xs-only">mdi-link</v-icon>
{{ $t('dialog.edit_books.tab_links') }}
</v-tab>
<!-- Tab: General --> <!-- Tab: General -->
<v-tab-item v-if="single"> <v-tab-item v-if="single">
@ -282,6 +286,77 @@
</v-card> </v-card>
</v-tab-item> </v-tab-item>
<!-- Tab: Links -->
<v-tab-item v-if="single">
<v-card flat min-height="100">
<v-container fluid>
<!-- Links -->
<v-form
v-model="linksValid"
ref="linksForm"
>
<v-row
v-for="(link, i) in form.links"
:key="i"
>
<v-col cols="4" class="py-0">
<v-text-field v-model="form.links[i].label"
:label="$t('dialog.edit_books.field_link_label')"
filled
dense
:rules="[linksLabelRules]"
@input="$v.form.links.$touch()"
@blur="$v.form.links.$touch()"
@change="form.linksLock = true"
>
<template v-slot:prepend>
<v-icon :color="form.linksLock ? 'secondary' : ''"
@click="form.linksLock = !form.linksLock"
>
{{ form.linksLock ? 'mdi-lock' : 'mdi-lock-open' }}
</v-icon>
</template>
</v-text-field>
</v-col>
<v-col cols="8" class="py-0">
<v-text-field v-model="form.links[i].url"
:label="$t('dialog.edit_books.field_link_url')"
filled
dense
:rules="[linksUrlRules]"
@input="$v.form.links.$touch()"
@blur="$v.form.links.$touch()"
@change="form.linksLock = true"
>
<template v-slot:append-outer>
<v-icon @click="form.links.splice(i, 1)">mdi-delete</v-icon>
</template>
</v-text-field>
</v-col>
</v-row>
</v-form>
<v-row>
<v-spacer/>
<v-col cols="auto">
<v-btn
elevation="2"
fab
small
bottom
right
color="primary"
@click="form.links.push({label:'', url:''})"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card>
</v-tab-item>
</v-tabs> </v-tabs>
<v-card-actions class="hidden-xs-only"> <v-card-actions class="hidden-xs-only">
@ -316,6 +391,7 @@ export default Vue.extend({
customRole: '', customRole: '',
customRoles: [] as string[], customRoles: [] as string[],
customRoleValid: false, customRoleValid: false,
linksValid: false,
form: { form: {
title: '', title: '',
titleLock: false, titleLock: false,
@ -333,6 +409,8 @@ export default Vue.extend({
tagsLock: false, tagsLock: false,
isbn: '', isbn: '',
isbnLock: false, isbnLock: false,
links: [],
linksLock: false,
}, },
authorSearch: [], authorSearch: [],
authorSearchResults: [] as string[], authorSearchResults: [] as string[],
@ -389,6 +467,7 @@ export default Vue.extend({
summary: {}, summary: {},
authors: {}, authors: {},
isbn: {validIsbn}, isbn: {validIsbn},
links: {},
}, },
}, },
async created() { async created() {
@ -434,12 +513,26 @@ export default Vue.extend({
return errors return errors
}, },
customRoleRules(): any[] { customRoleRules(): any[] {
if (this.customRole === '') return ['Must not be empty'] if (this.customRole === '') return [this.$t('common.required').toString()]
if (this.authorRoles.map(n => n.value.toLowerCase()).includes(this.customRole?.toLowerCase())) return ['Already exists'] if (this.authorRoles.map(n => n.value.toLowerCase()).includes(this.customRole?.toLowerCase())) return [this.$t('dialog.edit_books.add_author_role_error_duplicate').toString()]
return [true] return [true]
}, },
}, },
methods: { methods: {
linksLabelRules(label: string): boolean | string {
if (!!this.$_.trim(label)) return true
return this.$t('common.required').toString()
},
linksUrlRules(value: string): boolean | string {
let url
try {
url = new URL(value)
} catch (_) {
return this.$t('dialog.edit_books.field_link_url_error_url').toString()
}
if (url.protocol === 'http:' || url.protocol === 'https:') return true
return this.$t('dialog.edit_books.field_link_url_error_protocol').toString()
},
addRole() { addRole() {
if ((this.$refs.customRoleForm as any).validate()) { if ((this.$refs.customRoleForm as any).validate()) {
this.customRoles.push(this.customRole.toLowerCase()); this.customRoles.push(this.customRole.toLowerCase());
@ -456,12 +549,14 @@ export default Vue.extend({
dialogReset(books: BookDto | BookDto[]) { dialogReset(books: BookDto | BookDto[]) {
this.tab = 0; this.tab = 0;
(this.$refs.customRoleForm as any)?.reset() (this.$refs.customRoleForm as any)?.reset()
(this.$refs.linksForm as any)?.resetValidation()
this.customRoles = [] this.customRoles = []
this.$v.$reset() this.$v.$reset()
if (Array.isArray(books) && books.length === 0) return if (Array.isArray(books) && books.length === 0) return
else if (this.$_.isEmpty(books)) return else if (this.$_.isEmpty(books)) return
if (Array.isArray(books) && books.length > 0) { if (Array.isArray(books) && books.length > 0) {
this.form.authors = {} this.form.authors = {}
this.form.links = []
const authorsLock = this.$_.uniq(books.map(x => x.metadata.authorsLock)) const authorsLock = this.$_.uniq(books.map(x => x.metadata.authorsLock))
this.form.authorsLock = authorsLock.length > 1 ? false : authorsLock[0] this.form.authorsLock = authorsLock.length > 1 ? false : authorsLock[0]
@ -472,6 +567,7 @@ export default Vue.extend({
this.form.tagsLock = tagsLock.length > 1 ? false : tagsLock[0] this.form.tagsLock = tagsLock.length > 1 ? false : tagsLock[0]
} else { } else {
this.form.tags = [] this.form.tags = []
this.form.links = []
const book = books as BookDto const book = books as BookDto
this.$_.merge(this.form, book.metadata) this.$_.merge(this.form, book.metadata)
this.form.authors = groupAuthorsByRole(book.metadata.authors) this.form.authors = groupAuthorsByRole(book.metadata.authors)
@ -487,7 +583,7 @@ export default Vue.extend({
} }
}, },
validateForm(): any { validateForm(): any {
if (!this.$v.$invalid) { if (!this.$v.$invalid && (!this.single || (this.$refs.linksForm as any).validate())) {
const metadata = { const metadata = {
authorsLock: this.form.authorsLock, authorsLock: this.form.authorsLock,
tagsLock: this.form.tagsLock, tagsLock: this.form.tagsLock,
@ -513,6 +609,7 @@ export default Vue.extend({
summaryLock: this.form.summaryLock, summaryLock: this.form.summaryLock,
releaseDateLock: this.form.releaseDateLock, releaseDateLock: this.form.releaseDateLock,
isbnLock: this.form.isbnLock, isbnLock: this.form.isbnLock,
linksLock: this.form.linksLock,
}) })
if (this.$v.form?.title?.$dirty) { if (this.$v.form?.title?.$dirty) {
@ -538,6 +635,10 @@ export default Vue.extend({
if (this.$v.form?.isbn?.$dirty) { if (this.$v.form?.isbn?.$dirty) {
this.$_.merge(metadata, {isbn: this.form.isbn}) this.$_.merge(metadata, {isbn: this.form.isbn})
} }
if (this.$v.form?.links?.$dirty || this.form.links.length != (this.books as BookDto).metadata.links?.length) {
this.$_.merge(metadata, {links: this.form.links})
}
} }
return metadata return metadata

View file

@ -322,6 +322,7 @@
"warning_html": "The user <b>{name}</b> will be deleted from this server. This <b>cannot</b> be undone. Continue?" "warning_html": "The user <b>{name}</b> will be deleted from this server. This <b>cannot</b> be undone. Continue?"
}, },
"edit_books": { "edit_books": {
"add_author_role_error_duplicate": "Already exists",
"authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.", "authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.",
"button_cancel": "Cancel", "button_cancel": "Cancel",
"button_confirm": "Save changes", "button_confirm": "Save changes",
@ -329,6 +330,10 @@
"dialog_title_single": "Edit {book}", "dialog_title_single": "Edit {book}",
"field_isbn": "ISBN", "field_isbn": "ISBN",
"field_isbn_error": "Must be a valid ISBN 13", "field_isbn_error": "Must be a valid ISBN 13",
"field_link_label": "Label",
"field_link_url": "URL",
"field_link_url_error_protocol": "Must be http or https",
"field_link_url_error_url": "Must be a valid URL",
"field_number": "Number", "field_number": "Number",
"field_number_sort": "Sort Number", "field_number_sort": "Sort Number",
"field_number_sort_hint": "You can use decimal numbers", "field_number_sort_hint": "You can use decimal numbers",
@ -339,6 +344,7 @@
"field_title": "Title", "field_title": "Title",
"tab_authors": "Authors", "tab_authors": "Authors",
"tab_general": "General", "tab_general": "General",
"tab_links": "Links",
"tab_tags": "Tags", "tab_tags": "Tags",
"tags_notice_multiple_edit": "You are editing tags for multiple books. This will override existing tags of each book." "tags_notice_multiple_edit": "You are editing tags for multiple books. This will override existing tags of each book."
}, },

View file

@ -90,7 +90,9 @@ export interface BookMetadataUpdateDto {
tags?: string[], tags?: string[],
tagsLock?: boolean tagsLock?: boolean
isbn?: string, isbn?: string,
isbnLock?: boolean isbnLock?: boolean,
links?: WebLinkDto[],
linksLock?: boolean
} }
export interface BookMetadataUpdateBatchDto { export interface BookMetadataUpdateBatchDto {
@ -117,7 +119,7 @@ export interface BookFormat {
color: string color: string
} }
export interface BookImportBatchDto{ export interface BookImportBatchDto {
books: BookImportDto[], books: BookImportDto[],
copyMode: CopyMode, copyMode: CopyMode,
} }

View file

@ -2,8 +2,11 @@ package org.gotson.komga.interfaces.api.rest.dto
import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.BookMetadata import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.WebLink
import org.gotson.komga.infrastructure.validation.NullOrBlankOrISBN import org.gotson.komga.infrastructure.validation.NullOrBlankOrISBN
import org.gotson.komga.infrastructure.validation.NullOrNotBlank import org.gotson.komga.infrastructure.validation.NullOrNotBlank
import org.hibernate.validator.constraints.URL
import java.net.URI
import java.time.LocalDate import java.time.LocalDate
import javax.validation.Valid import javax.validation.Valid
import javax.validation.constraints.NotBlank import javax.validation.constraints.NotBlank
@ -63,6 +66,14 @@ class BookMetadataUpdateDto {
} }
var isbnLock: Boolean? = null var isbnLock: Boolean? = null
@get:Valid
var links: List<WebLinkUpdateDto>?
by Delegates.observable(null) { prop, _, _ ->
isSet[prop.name] = true
}
var linksLock: Boolean? = null
} }
class AuthorUpdateDto { class AuthorUpdateDto {
@ -73,6 +84,14 @@ class AuthorUpdateDto {
val role: String? = null val role: String? = null
} }
class WebLinkUpdateDto {
@get:NotBlank
val label: String? = null
@get:URL
val url: String? = null
}
fun BookMetadata.patch(patch: BookMetadataUpdateDto) = fun BookMetadata.patch(patch: BookMetadataUpdateDto) =
patch.let { patch.let {
this.copy( this.copy(
@ -95,6 +114,10 @@ fun BookMetadata.patch(patch: BookMetadataUpdateDto) =
} else this.tags, } else this.tags,
tagsLock = patch.tagsLock ?: this.tagsLock, tagsLock = patch.tagsLock ?: this.tagsLock,
isbn = if (patch.isSet("isbn")) patch.isbn?.filter { it.isDigit() } ?: "" else this.isbn, isbn = if (patch.isSet("isbn")) patch.isbn?.filter { it.isDigit() } ?: "" else this.isbn,
isbnLock = patch.isbnLock ?: this.isbnLock isbnLock = patch.isbnLock ?: this.isbnLock,
links = if (patch.isSet("links")) {
if (patch.links != null) patch.links!!.map { WebLink(it.label!!, URI(it.url!!)) } else emptyList()
} else this.links,
linksLock = patch.linksLock ?: this.linksLock,
) )
} }