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-expansion-panel
v-for="(f, key) in filtersOptions" v-for="(f, key) in filtersOptions"
:key="key" :key="key"
:disabled="f.values.length === 0" :disabled="(f.values && f.values.length === 0) && !f.search"
> >
<v-expansion-panel-header class="text-uppercase"> <v-expansion-panel-header class="text-uppercase">
<v-icon <v-icon
@ -16,7 +16,25 @@
{{ f.name }} {{ f.name }}
</v-expansion-panel-header> </v-expansion-panel-header>
<v-expansion-panel-content class="no-padding"> <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" <v-list-item v-for="v in f.values"
:key="v.value" :key="v.value"
@click.stop="click(key, v.value)" @click.stop="click(key, v.value)"
@ -38,20 +56,68 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue' import Vue, {PropType} from 'vue'
export default Vue.extend({ export default Vue.extend({
name: 'FilterPanels', name: 'FilterPanels',
data: () => {
return {
search: {} as any,
model: {} as any,
items: {} as any,
loading: {} as any,
}
},
props: { props: {
filtersOptions: { filtersOptions: {
type: Object as () => FiltersOptions, type: Object as PropType<FiltersOptions>,
required: true, required: true,
}, },
filtersActive: { filtersActive: {
type: Object as () => FiltersActive, type: Object as PropType<FiltersActive>,
required: true, 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: { methods: {
clear(key: string) { clear(key: string) {
let r = this.$_.cloneDeep(this.filtersActive) let r = this.$_.cloneDeep(this.filtersActive)
@ -61,12 +127,7 @@ export default Vue.extend({
}, },
groupActive(key: string): boolean { groupActive(key: string): boolean {
if (!(key in this.filtersActive)) return false if (!(key in this.filtersActive)) return false
for (let v of this.filtersOptions[key].values) { return this.filtersActive[key].length > 0;
if (this.filtersActive[key].includes(v.value)) {
return true
}
}
return false
}, },
click(key: string, value: string) { click(key: string, value: string) {
let r = this.$_.cloneDeep(this.filtersActive) let r = this.$_.cloneDeep(this.filtersActive)

View file

@ -11,15 +11,16 @@ export default class KomgaReferentialService {
this.http = http 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 { try {
const params = {} as any const params = {} as any
if (search) params.search = search if (search) params.search = search
if (role) params.role = role
if (libraryId) params.library_id = libraryId if (libraryId) params.library_id = libraryId
if (collectionId) params.collection_id = collectionId if (collectionId) params.collection_id = collectionId
if (seriesId) params.series_id = seriesId if (seriesId) params.series_id = seriesId
return (await this.http.get('/api/v1/authors', { return (await this.http.get('/api/v2/authors', {
params: params, params: params,
paramsSerializer: params => qs.stringify(params, {indices: false}), paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data })).data

View file

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

View file

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

View file

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