feat: show a message for empty Artists/Albums/Genres screens (#1734)

This commit is contained in:
Phan An 2023-12-28 23:32:58 +01:00 committed by GitHub
parent ce0ed4a49c
commit ed6f01ad52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 98 additions and 20 deletions

View file

@ -43,6 +43,7 @@ export default abstract class UnitTestCase {
protected beforeEach (cb?: Closure) {
beforeEach(() => {
commonStore.state.song_length = 10
commonStore.state.allow_download = true
commonStore.state.use_i_tunes = true
cb && cb()
@ -51,6 +52,7 @@ export default abstract class UnitTestCase {
protected afterEach (cb?: Closure) {
afterEach(() => {
commonStore.state.song_length = 10
cleanup()
this.restoreAllMocks()
isMobile.any = false

View file

@ -1,7 +1,7 @@
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { albumStore, preferenceStore } from '@/stores'
import { albumStore, commonStore, preferenceStore } from '@/stores'
import { screen, waitFor } from '@testing-library/vue'
import AlbumListScreen from './AlbumListScreen.vue'
@ -30,6 +30,13 @@ new class extends UnitTestCase {
expect(screen.getAllByTestId('album-card')).toHaveLength(9)
})
it('shows a message when the library is empty', async () => {
commonStore.state.song_length = 0
await this.renderComponent()
await waitFor(() => screen.getByTestId('screen-empty-state'))
})
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout from preferences', async (mode) => {
preferenceStore.albumsViewMode = mode

View file

@ -7,7 +7,18 @@
</template>
</ScreenHeader>
<ScreenEmptyState v-if="libraryEmpty">
<template #icon>
<Icon :icon="faCompactDisc" />
</template>
No albums found.
<span class="secondary d-block">
{{ isAdmin ? 'Have you set up your library yet?' : 'Contact your administrator to set up your library.' }}
</span>
</ScreenEmptyState>
<div
v-else
ref="scroller"
v-koel-overflow-fade
:class="`as-${viewMode}`"
@ -27,15 +38,19 @@
</template>
<script lang="ts" setup>
import { faCompactDisc } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef, watch } from 'vue'
import { albumStore, preferenceStore as preferences } from '@/stores'
import { useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
import { albumStore, commonStore, preferenceStore as preferences } from '@/stores'
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
import { logger } from '@/utils'
import AlbumCard from '@/components/album/AlbumCard.vue'
import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ViewModeSwitch from '@/components/ui/ViewModeSwitch.vue'
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
const { isAdmin } = useAuthorization()
const viewMode = ref<ArtistAlbumViewMode>('thumbnails')
const albums = toRef(albumStore.state, 'albums')
@ -53,6 +68,7 @@ let initialized = false
const loading = ref(false)
const page = ref<number | null>(1)
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
const itemLayout = computed<ArtistAlbumCardLayout>(() => viewMode.value === 'thumbnails' ? 'full' : 'compact')
const moreAlbumsAvailable = computed(() => page.value !== null)
const showSkeletons = computed(() => loading.value && albums.value.length === 0)
@ -66,6 +82,8 @@ const fetchAlbums = async () => {
}
useRouter().onScreenActivated('Albums', async () => {
if (libraryEmpty.value) return
if (!initialized) {
viewMode.value = preferences.albumsViewMode || 'thumbnails'
initialized = true

View file

@ -17,9 +17,7 @@ new class extends UnitTestCase {
id: 42,
name: 'Led Zeppelin IV',
artist_id: 123,
artist_name: 'Led Zeppelin',
song_count: 10,
length: 1_603
artist_name: 'Led Zeppelin'
})
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)

View file

@ -1,7 +1,7 @@
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { artistStore, preferenceStore } from '@/stores'
import { artistStore, commonStore, preferenceStore } from '@/stores'
import { screen, waitFor } from '@testing-library/vue'
import ArtistListScreen from './ArtistListScreen.vue'
@ -31,6 +31,13 @@ new class extends UnitTestCase {
expect(screen.getAllByTestId('artist-card')).toHaveLength(9)
})
it('shows a message when the library is empty', async () => {
commonStore.state.song_length = 0
await this.renderComponent()
await waitFor(() => screen.getByTestId('screen-empty-state'))
})
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout:%s from preferences', async (mode) => {
preferenceStore.artistsViewMode = mode

View file

@ -7,7 +7,18 @@
</template>
</ScreenHeader>
<ScreenEmptyState v-if="libraryEmpty">
<template #icon>
<Icon :icon="faMicrophoneSlash" />
</template>
No artists found.
<span class="secondary d-block">
{{ isAdmin ? 'Have you set up your library yet?' : 'Contact your administrator to set up your library.' }}
</span>
</ScreenEmptyState>
<div
v-else
ref="scroller"
v-koel-overflow-fade
:class="`as-${viewMode}`"
@ -27,15 +38,19 @@
</template>
<script lang="ts" setup>
import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef, watch } from 'vue'
import { artistStore, preferenceStore as preferences } from '@/stores'
import { useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
import { artistStore, commonStore, preferenceStore as preferences } from '@/stores'
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
import { logger } from '@/utils'
import ArtistCard from '@/components/artist/ArtistCard.vue'
import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ViewModeSwitch from '@/components/ui/ViewModeSwitch.vue'
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
const { isAdmin } = useAuthorization()
const viewMode = ref<ArtistAlbumViewMode>('thumbnails')
const artists = toRef(artistStore.state, 'artists')
@ -53,6 +68,7 @@ let initialized = false
const loading = ref(false)
const page = ref<number | null>(1)
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
const itemLayout = computed<ArtistAlbumCardLayout>(() => viewMode.value === 'thumbnails' ? 'full' : 'compact')
const moreArtistsAvailable = computed(() => page.value !== null)
const showSkeletons = computed(() => loading.value && artists.value.length === 0)
@ -66,6 +82,7 @@ const fetchArtists = async () => {
}
useRouter().onScreenActivated('Artists', async () => {
if (libraryEmpty.value) return
if (!initialized) {
viewMode.value = preferences.artistsViewMode || 'thumbnails'
initialized = true

View file

@ -16,9 +16,6 @@ new class extends UnitTestCase {
artist = factory<Artist>('artist', {
id: 42,
name: 'Led Zeppelin',
album_count: 12,
song_count: 53,
length: 40_603
})
const resolveArtistMock = this.mock(artistStore, 'resolve').mockResolvedValue(artist)

View file

@ -1,7 +1,7 @@
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { genreStore } from '@/stores'
import { commonStore, genreStore } from '@/stores'
import { screen, waitFor } from '@testing-library/vue'
import GenreListScreen from './GenreListScreen.vue'
@ -24,5 +24,17 @@ new class extends UnitTestCase {
genres.forEach(genre => screen.getByTitle(`${genre.name}: ${genre.song_count} songs`))
})
})
it('shows a message when the library is empty', async () => {
commonStore.state.song_length = 0
const fetchMock = this.mock(genreStore, 'fetchAll')
this.render(GenreListScreen)
await waitFor(() => {
expect(fetchMock).not.toHaveBeenCalled()
screen.getByTestId('screen-empty-state')
})
})
}
}

View file

@ -1,9 +1,18 @@
<template>
<section id="genresWrapper">
<ScreenHeader layout="collapsed">
Genres
</ScreenHeader>
<div class="main-scroll-wrap">
<ScreenHeader layout="collapsed">Genres</ScreenHeader>
<ScreenEmptyState v-if="libraryEmpty">
<template #icon>
<Icon :icon="faTags" />
</template>
No genres found.
<span class="secondary d-block">
{{ isAdmin ? 'Have you set up your library yet?' : 'Contact your administrator to set up your library.' }}
</span>
</ScreenEmptyState>
<div class="main-scroll-wrap" v-else>
<ul v-if="genres" class="genres">
<li v-for="genre in genres" :key="genre.name" :class="`level-${getLevel(genre)}`">
<a
@ -25,15 +34,22 @@
</template>
<script lang="ts" setup>
import { faTags } from '@fortawesome/free-solid-svg-icons'
import { maxBy, minBy } from 'lodash'
import { computed, onMounted, ref } from 'vue'
import { genreStore } from '@/stores'
import { commonStore, genreStore } from '@/stores'
import { pluralize } from '@/utils'
import { useAuthorization } from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import GenreItemSkeleton from '@/components/ui/skeletons/GenreItemSkeleton.vue'
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
const { isAdmin } = useAuthorization()
const genres = ref<Genre[]>()
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
const mostPopular = computed(() => maxBy(genres.value, 'song_count'))
const leastPopular = computed(() => minBy(genres.value, 'song_count'))
@ -51,7 +67,10 @@ const getLevel = (genre: Genre) => {
return index === -1 ? 5 : index
}
onMounted(async () => genres.value = await genreStore.fetchAll())
onMounted(async () => {
if (libraryEmpty.value) return
genres.value = await genreStore.fetchAll()
})
</script>
<style lang="scss" scoped>

View file

@ -1,7 +1,7 @@
import { without } from 'lodash'
import { reactive } from 'vue'
import { http } from '@/services'
import { albumStore, overviewStore, songStore } from '@/stores'
import { albumStore, commonStore, overviewStore, songStore } from '@/stores'
import { logger } from '@/utils'
interface UploadResult {
@ -86,6 +86,7 @@ export const uploadService = {
songStore.syncWithVault(result.song)
albumStore.syncWithVault(result.album)
commonStore.state.song_length += 1
overviewStore.refresh()
this.proceed() // upload the next file