feat(webui): search authors in filters

This commit is contained in:
Gauthier Roebroeck 2021-05-31 17:55:52 +08:00
parent a45a73c8bd
commit b908ac140b
6 changed files with 145 additions and 59 deletions

View file

@ -3,7 +3,7 @@
<v-expansion-panel
v-for="(f, key) in filtersOptions"
:key="key"
:disabled="f.values.length === 0"
:disabled="(f.values && f.values.length === 0) && !f.search"
>
<v-expansion-panel-header class="text-uppercase">
<v-icon
@ -16,7 +16,25 @@
{{ f.name }}
</v-expansion-panel-header>
<v-expansion-panel-content class="no-padding">
<v-list dense>
<v-autocomplete
v-if="f.search"
v-model="model[key]"
:items="items[key]"
:search-input.sync="search[key]"
:loading="loading[key]"
:hide-no-data="!search[key] || loading[key]"
@keydown.esc="search[key] = null"
multiple
deletable-chips
small-chips
dense
solo
/>
<v-list
v-if="f.values"
dense
>
<v-list-item v-for="v in f.values"
:key="v.value"
@click.stop="click(key, v.value)"
@ -38,37 +56,80 @@
</template>
<script lang="ts">
import Vue from 'vue'
import Vue, {PropType} from 'vue'
export default Vue.extend({
name: 'FilterPanels',
data: () => {
return {
search: {} as any,
model: {} as any,
items: {} as any,
loading: {} as any,
}
},
props: {
filtersOptions: {
type: Object as () => FiltersOptions,
type: Object as PropType<FiltersOptions>,
required: true,
},
filtersActive: {
type: Object as () => FiltersActive,
type: Object as PropType<FiltersActive>,
required: true,
},
},
watch: {
search: {
deep: true,
async handler(val: any) {
for (const prop in val) {
if (val[prop] !== null) {
this.loading[prop] = true
this.$set(this.items, prop, await (this.filtersOptions[prop] as any).search(val[prop]))
this.loading[prop] = false
}
}
},
},
model: {
deep: true,
async handler(val: any) {
for (const prop in val) {
if (val[prop] !== null && val[prop] !== this.filtersActive[prop]) {
let r = this.$_.cloneDeep(this.filtersActive)
r[prop] = this.$_.clone(val[prop])
this.$emit('update:filtersActive', r)
}
}
},
},
filtersActive: {
deep: true,
immediate: true,
handler(val: any) {
for (const prop in val) {
if (val[prop].length > 0) {
// we need to add existing values to items also, else v-autocomplete won't show it
this.$set(this.items, prop, this.$_.union(this.items[prop], val[prop]))
this.$set(this.model, prop, val[prop])
}
}
},
},
},
methods: {
clear (key: string) {
clear(key: string) {
let r = this.$_.cloneDeep(this.filtersActive)
r[key] = []
this.$emit('update:filtersActive', r)
},
groupActive (key: string): boolean {
groupActive(key: string): boolean {
if (!(key in this.filtersActive)) return false
for (let v of this.filtersOptions[key].values) {
if (this.filtersActive[key].includes(v.value)) {
return true
}
}
return false
return this.filtersActive[key].length > 0;
},
click (key: string, value: string) {
click(key: string, value: string) {
let r = this.$_.cloneDeep(this.filtersActive)
if (!(key in r)) r[key] = []
if (r[key].includes(value)) this.$_.pull(r[key], (value))

View file

@ -11,15 +11,16 @@ export default class KomgaReferentialService {
this.http = http
}
async getAuthors(search?: string, libraryId?: string, collectionId?: string, seriesId?: string): Promise<AuthorDto[]> {
async getAuthors(search?: string, role?: string, libraryId?: string, collectionId?: string, seriesId?: string): Promise<Page<AuthorDto>> {
try {
const params = {} as any
if (search) params.search = search
if (role) params.role = role
if (libraryId) params.library_id = libraryId
if (collectionId) params.collection_id = collectionId
if (seriesId) params.series_id = seriesId
return (await this.http.get('/api/v1/authors', {
return (await this.http.get('/api/v2/authors', {
params: params,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data

View file

@ -1,7 +1,8 @@
interface FiltersOptions {
[key: string]: {
name?: string,
values: NameValue[],
values?: NameValue[],
search?: (search: string) => Promise<string[]>,
},
}

View file

@ -129,7 +129,6 @@ import EmptyState from '@/components/EmptyState.vue'
import {parseQueryParam} from '@/functions/query-params'
import {SeriesDto} from "@/types/komga-series";
import {authorRoles} from "@/types/author-roles";
import {groupAuthorsByRole} from "@/functions/authors";
import {AuthorDto} from "@/types/komga-books";
export default Vue.extend({
@ -249,8 +248,14 @@ export default Vue.extend({
releaseDate: {name: this.$t('filter.release_date').toString(), values: this.filterOptions.releaseDate},
} as FiltersOptions
authorRoles.forEach((role: string) => {
//@ts-ignore
r[role] = {name: this.$t(`author_roles.${role}`).toString(), values: this.$_.get(this.filterOptions, role, [])}
r[role] = {
name: this.$t(`author_roles.${role}`).toString(),
search: async search => {
return (await this.$komgaReferential.getAuthors(search, role, undefined, this.collectionId))
.content
.map(x => x.name)
},
}
})
return r
},
@ -276,16 +281,21 @@ export default Vue.extend({
name: x.name,
value: x.id,
})))
this.$set(this.filterOptions, 'genre', toNameValue(await this.$komgaReferential.getGenres(undefined, collectionId)))
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getSeriesTags(undefined, collectionId)))
this.$set(this.filterOptions, 'publisher', toNameValue(await this.$komgaReferential.getPublishers(undefined, collectionId)))
this.$set(this.filterOptions, 'language', (await this.$komgaReferential.getLanguages(undefined, collectionId)))
this.$set(this.filterOptions, 'ageRating', toNameValue(await this.$komgaReferential.getAgeRatings(undefined, collectionId)))
this.$set(this.filterOptions, 'releaseDate', toNameValue(await this.$komgaReferential.getSeriesReleaseDates(undefined, collectionId)))
const grouped = groupAuthorsByRole(await this.$komgaReferential.getAuthors(undefined, undefined, collectionId))
authorRoles.forEach((role: string) => {
this.$set(this.filterOptions, role, role in grouped ? toNameValue(grouped[role]) : [])
})
const [genres, tags, publishers, languages, ageRatings, releaseDates] = await Promise.all([
this.$komgaReferential.getGenres(undefined, collectionId),
this.$komgaReferential.getSeriesTags(undefined, collectionId),
this.$komgaReferential.getPublishers(undefined, collectionId),
this.$komgaReferential.getLanguages(undefined, collectionId),
this.$komgaReferential.getAgeRatings(undefined, collectionId),
this.$komgaReferential.getSeriesReleaseDates(undefined, collectionId),
])
this.$set(this.filterOptions, 'genre', toNameValue(genres))
this.$set(this.filterOptions, 'tag', toNameValue(tags))
this.$set(this.filterOptions, 'publisher', toNameValue(publishers))
this.$set(this.filterOptions, 'language', (languages))
this.$set(this.filterOptions, 'ageRating', toNameValue(ageRatings))
this.$set(this.filterOptions, 'releaseDate', toNameValue(releaseDates))
// get filter from query params or local storage and validate with available filter values
let activeFilters: any
@ -322,7 +332,7 @@ export default Vue.extend({
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
} as any
authorRoles.forEach((role: string) => {
validFilter[role] = filters[role]?.filter(x => ((this.filterOptions as any)[role] as NameValue[]).map(n => n.value).includes(x)) || []
validFilter[role] = filters[role] || []
})
return validFilter
},

View file

@ -122,7 +122,6 @@ import FilterPanels from '@/components/FilterPanels.vue'
import FilterList from '@/components/FilterList.vue'
import {mergeFilterParams, sortOrFilterActive, toNameValue} from '@/functions/filter'
import {SeriesDto} from "@/types/komga-series";
import {groupAuthorsByRole} from "@/functions/authors";
import {AuthorDto} from "@/types/komga-books";
import {authorRoles} from "@/types/author-roles";
@ -237,11 +236,13 @@ export default Vue.extend({
},
filterOptionsList(): FiltersOptions {
return {
readStatus: {values: [
{name: this.$t('filter.unread').toString(), value: ReadStatus.UNREAD_AND_IN_PROGRESS},
{name: this.$t('filter.in_progress').toString(), value: ReadStatus.IN_PROGRESS},
{name: this.$t('filter.read').toString(), value: ReadStatus.READ},
]},
readStatus: {
values: [
{name: this.$t('filter.unread').toString(), value: ReadStatus.UNREAD_AND_IN_PROGRESS},
{name: this.$t('filter.in_progress').toString(), value: ReadStatus.IN_PROGRESS},
{name: this.$t('filter.read').toString(), value: ReadStatus.READ},
],
},
} as FiltersOptions
},
filterOptionsPanel(): FiltersOptions {
@ -262,8 +263,14 @@ export default Vue.extend({
releaseDate: {name: this.$t('filter.release_date').toString(), values: this.filterOptions.releaseDate},
} as FiltersOptions
authorRoles.forEach((role: string) => {
//@ts-ignore
r[role] = {name: this.$t(`author_roles.${role}`).toString(), values: this.$_.get(this.filterOptions, role, [])}
r[role] = {
name: this.$t(`author_roles.${role}`).toString(),
search: async search => {
return (await this.$komgaReferential.getAuthors(search, role, this.libraryId !== LIBRARIES_ALL ? this.libraryId : undefined))
.content
.map(x => x.name)
},
}
})
return r
},
@ -306,16 +313,20 @@ export default Vue.extend({
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
// load dynamic filters
this.$set(this.filterOptions, 'genre', toNameValue(await this.$komgaReferential.getGenres(requestLibraryId)))
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getSeriesTags(requestLibraryId)))
this.$set(this.filterOptions, 'publisher', toNameValue(await this.$komgaReferential.getPublishers(requestLibraryId)))
this.$set(this.filterOptions, 'language', (await this.$komgaReferential.getLanguages(requestLibraryId)))
this.$set(this.filterOptions, 'ageRating', toNameValue(await this.$komgaReferential.getAgeRatings(requestLibraryId)))
this.$set(this.filterOptions, 'releaseDate', toNameValue(await this.$komgaReferential.getSeriesReleaseDates(requestLibraryId)))
const grouped = groupAuthorsByRole(await this.$komgaReferential.getAuthors(undefined, requestLibraryId))
authorRoles.forEach((role: string) => {
this.$set(this.filterOptions, role, role in grouped ? toNameValue(grouped[role]) : [])
})
const [genres, tags, publishers, languages, ageRatings, releaseDates] = await Promise.all([
this.$komgaReferential.getGenres(requestLibraryId),
this.$komgaReferential.getSeriesTags(requestLibraryId),
this.$komgaReferential.getPublishers(requestLibraryId),
this.$komgaReferential.getLanguages(requestLibraryId),
this.$komgaReferential.getAgeRatings(requestLibraryId),
this.$komgaReferential.getSeriesReleaseDates(requestLibraryId),
])
this.$set(this.filterOptions, 'genre', toNameValue(genres))
this.$set(this.filterOptions, 'tag', toNameValue(tags))
this.$set(this.filterOptions, 'publisher', toNameValue(publishers ))
this.$set(this.filterOptions, 'language', (languages))
this.$set(this.filterOptions, 'ageRating', toNameValue(ageRatings))
this.$set(this.filterOptions, 'releaseDate', toNameValue(releaseDates))
// get filter from query params or local storage and validate with available filter values
let activeFilters: any
@ -350,7 +361,7 @@ export default Vue.extend({
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
} as any
authorRoles.forEach((role: string) => {
validFilter[role] = filters[role]?.filter(x => ((this.filterOptions as any)[role] as NameValue[]).map(n => n.value).includes(x)) || []
validFilter[role] = filters[role] || []
})
return validFilter
},
@ -452,7 +463,7 @@ export default Vue.extend({
this.totalElements = seriesPage.totalElements
this.series = seriesPage.content
},
getLibraryLazy (libraryId: string): LibraryDto | undefined {
getLibraryLazy(libraryId: string): LibraryDto | undefined {
if (libraryId !== LIBRARIES_ALL) {
return this.$store.getters.getLibraryById(libraryId)
} else {

View file

@ -378,7 +378,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
import ItemCard from '@/components/ItemCard.vue'
import SeriesActionsMenu from '@/components/menus/SeriesActionsMenu.vue'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import {parseQueryParamAndFilter, parseQuerySort} from '@/functions/query-params'
import {parseQueryParam, parseQueryParamAndFilter, parseQuerySort} from '@/functions/query-params'
import {seriesFileUrl, seriesThumbnailUrl} from '@/functions/urls'
import {ReadStatus, replaceCompositeReadStatus} from '@/types/enum-books'
import {BOOK_CHANGED, LIBRARY_DELETED, READLIST_CHANGED, SERIES_CHANGED} from '@/types/events'
@ -468,8 +468,14 @@ export default Vue.extend({
tag: {name: this.$t('filter.tag').toString(), values: this.filterOptions.tag},
} as FiltersOptions
authorRoles.forEach((role: string) => {
//@ts-ignore
r[role] = {name: this.$t(`author_roles.${role}`).toString(), values: this.$_.get(this.filterOptions, role, [])}
r[role] = {
name: this.$t(`author_roles.${role}`).toString(),
search: async search => {
return (await this.$komgaReferential.getAuthors(search, role, undefined, undefined, this.seriesId))
.content
.map(x => x.name)
},
}
})
return r
},
@ -592,17 +598,13 @@ export default Vue.extend({
// load dynamic filters
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getBookTags(seriesId)))
const grouped = groupAuthorsByRole(await this.$komgaReferential.getAuthors(undefined, undefined, undefined, seriesId))
authorRoles.forEach((role: string) => {
this.$set(this.filterOptions, role, role in grouped ? toNameValue(grouped[role]) : [])
})
// filter query params with available filter values
this.$set(this.filters, 'readStatus', parseQueryParamAndFilter(this.$route.query.readStatus, Object.keys(ReadStatus)))
this.$set(this.filters, 'tag', parseQueryParamAndFilter(this.$route.query.tag, this.filterOptions.tag.map(x => x.value)))
authorRoles.forEach((role: string) => {
//@ts-ignore
this.$set(this.filters, role, parseQueryParamAndFilter(route.query[role], this.filterOptions[role].map((x: NameValue) => x.value)))
this.$set(this.filters, role, parseQueryParam(route.query[role]))
})
},
setWatches() {