mirror of
https://github.com/koel/koel
synced 2024-11-28 06:50:27 +00:00
feat: only show YouTube sidebar item on demand (#1621)
This commit is contained in:
parent
af467343af
commit
4e067c3d50
18 changed files with 377 additions and 268 deletions
|
@ -1,8 +0,0 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('has already been tested in the integration suite', () => expect('😄').toBeTruthy())
|
||||
}
|
||||
}
|
|
@ -1,252 +0,0 @@
|
|||
<template>
|
||||
<nav id="sidebar" v-koel-clickaway="closeIfMobile" :class="{ showing: mobileShowing }" class="side side-nav">
|
||||
<SearchForm />
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#/home">
|
||||
<icon :icon="faHome" fixed-width />
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
:class="droppableToQueue && 'droppable'"
|
||||
@dragleave="onQueueDragLeave"
|
||||
@dragover="onQueueDragOver"
|
||||
@drop="onQueueDrop"
|
||||
>
|
||||
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#/queue">
|
||||
<icon :icon="faListOl" fixed-width />
|
||||
Current Queue
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#/songs">
|
||||
<icon :icon="faMusic" fixed-width />
|
||||
All Songs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#/albums">
|
||||
<icon :icon="faCompactDisc" fixed-width />
|
||||
Albums
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#/artists">
|
||||
<icon :icon="faMicrophone" fixed-width />
|
||||
Artists
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['genres', activeScreen === 'Genres' ? 'active' : '']" href="#/genres">
|
||||
<icon :icon="faTags" fixed-width />
|
||||
Genres
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="useYouTube">
|
||||
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#/youtube">
|
||||
<icon :icon="faYoutube" fixed-width />
|
||||
YouTube Video
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<PlaylistList />
|
||||
|
||||
<section v-if="isAdmin" class="manage">
|
||||
<h1>Manage</h1>
|
||||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#/settings">
|
||||
<icon :icon="faTools" fixed-width />
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#/upload">
|
||||
<icon :icon="faUpload" fixed-width />
|
||||
Upload
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#/users">
|
||||
<icon :icon="faUsers" fixed-width />
|
||||
Users
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
faCompactDisc,
|
||||
faHome,
|
||||
faListOl,
|
||||
faMicrophone,
|
||||
faMusic,
|
||||
faTags,
|
||||
faTools,
|
||||
faUpload,
|
||||
faUsers
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
|
||||
import { ref } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { queueStore } from '@/stores'
|
||||
import { useAuthorization, useDroppable, useRouter, useThirdPartyServices } from '@/composables'
|
||||
|
||||
import PlaylistList from '@/components/playlist/PlaylistSidebarList.vue'
|
||||
import SearchForm from '@/components/ui/SearchForm.vue'
|
||||
|
||||
const mobileShowing = ref(false)
|
||||
const activeScreen = ref<ScreenName>()
|
||||
const droppableToQueue = ref(false)
|
||||
|
||||
const { onRouteChanged } = useRouter()
|
||||
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist', 'playlist'])
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const onQueueDragOver = (event: DragEvent) => {
|
||||
if (!acceptsDrop(event)) return false
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'move'
|
||||
droppableToQueue.value = true
|
||||
}
|
||||
|
||||
const onQueueDragLeave = () => (droppableToQueue.value = false)
|
||||
|
||||
const onQueueDrop = async (event: DragEvent) => {
|
||||
droppableToQueue.value = false
|
||||
|
||||
if (!acceptsDrop(event)) return false
|
||||
|
||||
event.preventDefault()
|
||||
const songs = await resolveDroppedSongs(event) || []
|
||||
songs.length && queueStore.queue(songs)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const closeIfMobile = () => (mobileShowing.value = false)
|
||||
|
||||
onRouteChanged(route => {
|
||||
mobileShowing.value = false
|
||||
activeScreen.value = route.screen
|
||||
})
|
||||
|
||||
/**
|
||||
* Listen to toggle sidebar event to show or hide the sidebar.
|
||||
* This should only be triggered on a mobile device.
|
||||
*/
|
||||
eventBus.on('TOGGLE_SIDEBAR', () => (mobileShowing.value = !mobileShowing.value))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
nav {
|
||||
width: var(--sidebar-width);
|
||||
background-color: var(--color-bg-secondary);
|
||||
padding: 2.05rem 1.5rem;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
> * + * {
|
||||
margin-top: 2.25rem;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
// Enable scroll with momentum on touch devices
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.droppable {
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||||
border-radius: 4px;
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.queue > span {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(h1) {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(a svg) {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .7rem;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
|
||||
&:active {
|
||||
padding: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
&.active, &:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
right: -1.5rem;
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
background-color: var(--color-highlight);
|
||||
box-shadow: 0 0 40px 10px var(--color-highlight);
|
||||
border-radius: 9999rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(li li a) { // submenu items
|
||||
padding-left: 11px;
|
||||
|
||||
&:active {
|
||||
padding: 2px 0 0 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@include themed-background();
|
||||
transform: translateX(-100vw);
|
||||
transition: transform .2s ease-in-out;
|
||||
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
height: calc(100vh - var(--header-height));
|
||||
|
||||
&.showing {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -10,7 +10,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import SideBar from '@/components/layout/main-wrapper/Sidebar.vue'
|
||||
import SideBar from '@/components/layout/main-wrapper/sidebar/Sidebar.vue'
|
||||
import MainContent from '@/components/layout/main-wrapper/MainContent.vue'
|
||||
import ExtraPanel from '@/components/layout/main-wrapper/ExtraPanel.vue'
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
>
|
||||
<a @click.prevent="toggle" @contextmenu.prevent="onContextMenu">
|
||||
<icon :icon="opened ? faFolderOpen : faFolder" fixed-width />
|
||||
{{ folder.name }}
|
||||
<span>{{ folder.name }}</span>
|
||||
</a>
|
||||
|
||||
<ul v-if="playlistsInFolder.length" v-show="opened">
|
||||
|
@ -34,7 +34,7 @@ import { playlistFolderStore, playlistStore } from '@/stores'
|
|||
import { eventBus } from '@/utils'
|
||||
import { useDroppable } from '@/composables'
|
||||
|
||||
const PlaylistSidebarItem = defineAsyncComponent(() => import('@/components/playlist/PlaylistSidebarItem.vue'))
|
||||
const PlaylistSidebarItem = defineAsyncComponent(() => import('./PlaylistSidebarItem.vue'))
|
||||
|
||||
const props = defineProps<{ folder: PlaylistFolder }>()
|
||||
const { folder } = toRefs(props)
|
|
@ -15,7 +15,7 @@
|
|||
<icon v-else-if="isFavoriteList(list)" :icon="faHeart" class="text-maroon" fixed-width />
|
||||
<icon v-else-if="list.is_smart" :icon="faWandMagicSparkles" fixed-width />
|
||||
<icon v-else :icon="faFileLines" fixed-width />
|
||||
{{ list.name }}
|
||||
<span>{{ list.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
|
@ -5,7 +5,6 @@
|
|||
<icon
|
||||
:icon="faCirclePlus"
|
||||
class="control create"
|
||||
data-testid="sidebar-create-playlist-btn"
|
||||
role="button"
|
||||
title="Create a new playlist or folder"
|
||||
@click.stop.prevent="requestContextMenu"
|
||||
|
@ -27,8 +26,8 @@ import { computed, toRef } from 'vue'
|
|||
import { favoriteStore, playlistFolderStore, playlistStore } from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
import PlaylistSidebarItem from '@/components/playlist/PlaylistSidebarItem.vue'
|
||||
import PlaylistFolderSidebarItem from '@/components/playlist/PlaylistFolderSidebarItem.vue'
|
||||
import PlaylistSidebarItem from './PlaylistSidebarItem.vue'
|
||||
import PlaylistFolderSidebarItem from './PlaylistFolderSidebarItem.vue'
|
||||
|
||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||
const playlists = toRef(playlistStore.state, 'playlists')
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<SidebarItem
|
||||
screen="Queue"
|
||||
href="#/queue"
|
||||
:icon="faListOl"
|
||||
:class="droppable && 'droppable'"
|
||||
@dragleave="onQueueDragLeave"
|
||||
@dragover.prevent="onQueueDragOver"
|
||||
@drop="onQueueDrop"
|
||||
>
|
||||
Current Queue
|
||||
</SidebarItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faListOl } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ref } from 'vue'
|
||||
import { queueStore } from '@/stores'
|
||||
import { useDroppable } from '@/composables'
|
||||
|
||||
import SidebarItem from './SidebarItem.vue'
|
||||
|
||||
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist', 'playlist'])
|
||||
|
||||
const droppable = ref(false)
|
||||
|
||||
const onQueueDragOver = (event: DragEvent) => (droppable.value = acceptsDrop(event))
|
||||
const onQueueDragLeave = () => (droppable.value = false)
|
||||
|
||||
const onQueueDrop = async (event: DragEvent) => {
|
||||
droppable.value = false
|
||||
|
||||
event.preventDefault()
|
||||
const songs = await resolveDroppedSongs(event) || []
|
||||
songs.length && queueStore.queue(songs)
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,55 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { screen } from '@testing-library/vue'
|
||||
import { commonStore } from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
|
||||
const standardItems = [
|
||||
'Home',
|
||||
'Current Queue',
|
||||
'All Songs',
|
||||
'Albums',
|
||||
'Artists',
|
||||
'Genres',
|
||||
'Favorites',
|
||||
'Recently Played'
|
||||
]
|
||||
|
||||
const adminItems = [...standardItems, 'Users', 'Upload', 'Settings']
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent() {
|
||||
this.render(Sidebar, {
|
||||
global: {
|
||||
stubs: {
|
||||
YouTubeSidebarItem: this.stub('youtube-sidebar-item')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test() {
|
||||
it('shows the standard items', () => {
|
||||
this.actingAs().renderComponent()
|
||||
standardItems.forEach(label => screen.getByText(label))
|
||||
})
|
||||
|
||||
it('shows administrative items', () => {
|
||||
this.actingAsAdmin().renderComponent()
|
||||
adminItems.forEach(label => screen.getByText(label))
|
||||
})
|
||||
|
||||
it('shows the YouTube sidebar item on demand', async () => {
|
||||
commonStore.state.use_you_tube = true
|
||||
this.renderComponent()
|
||||
|
||||
expect(screen.queryByTestId('youtube-sidebar-item')).toBeNull()
|
||||
|
||||
eventBus.emit('PLAY_YOUTUBE_VIDEO', { id: '123', title: 'A Random Video' })
|
||||
await this.tick()
|
||||
|
||||
screen.getByTestId('youtube-sidebar-item')
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
<template>
|
||||
<nav id="sidebar" v-koel-clickaway="closeIfMobile" :class="{ showing: mobileShowing }" class="side side-nav">
|
||||
<SearchForm />
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
|
||||
<ul class="menu">
|
||||
<SidebarItem screen="Home" href="#/home" :icon="faHome">Home</SidebarItem>
|
||||
<QueueSidebarItem />
|
||||
<SidebarItem screen="Songs" href="#/songs" :icon="faMusic">All Songs</SidebarItem>
|
||||
<SidebarItem screen="Albums" href="#/albums" :icon="faCompactDisc">Albums</SidebarItem>
|
||||
<SidebarItem screen="Artists" href="#/artists" :icon="faMicrophone">Artists</SidebarItem>
|
||||
<SidebarItem screen="Genres" href="#/genres" :icon="faTags">Genres</SidebarItem>
|
||||
<YouTubeSidebarItem v-if="showYouTube" />
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<PlaylistList />
|
||||
|
||||
<section v-if="isAdmin" class="manage">
|
||||
<h1>Manage</h1>
|
||||
|
||||
<ul class="menu">
|
||||
<SidebarItem screen="Settings" href="#/settings" :icon="faTools">Settings</SidebarItem>
|
||||
<SidebarItem screen="Upload" href="#/upload" :icon="faUpload">Upload</SidebarItem>
|
||||
<SidebarItem screen="Users" href="#/users" :icon="faUsers">Users</SidebarItem>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
faCompactDisc,
|
||||
faHome,
|
||||
faMicrophone,
|
||||
faMusic,
|
||||
faTags,
|
||||
faTools,
|
||||
faUpload,
|
||||
faUsers
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { useAuthorization, useRouter, useThirdPartyServices } from '@/composables'
|
||||
|
||||
import SidebarItem from './SidebarItem.vue'
|
||||
import QueueSidebarItem from './QueueSidebarItem.vue'
|
||||
import YouTubeSidebarItem from './YouTubeSidebarItem.vue'
|
||||
import PlaylistList from './PlaylistSidebarList.vue'
|
||||
import SearchForm from '@/components/ui/SearchForm.vue'
|
||||
|
||||
const { onRouteChanged } = useRouter()
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const mobileShowing = ref(false)
|
||||
const youTubePlaying = ref(false)
|
||||
|
||||
const showYouTube = computed(() => useYouTube.value && youTubePlaying.value)
|
||||
|
||||
const closeIfMobile = () => (mobileShowing.value = false)
|
||||
|
||||
onRouteChanged(_ => (mobileShowing.value = false))
|
||||
|
||||
/**
|
||||
* Listen to toggle sidebar event to show or hide the sidebar.
|
||||
* This should only be triggered on a mobile device.
|
||||
*/
|
||||
eventBus.on('TOGGLE_SIDEBAR', () => (mobileShowing.value = !mobileShowing.value))
|
||||
.on('PLAY_YOUTUBE_VIDEO', _ => (youTubePlaying.value = true))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
nav {
|
||||
width: var(--sidebar-width);
|
||||
background-color: var(--color-bg-secondary);
|
||||
padding: 2.05rem 1.5rem;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
> * + * {
|
||||
margin-top: 2.25rem;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
// Enable scroll with momentum on touch devices
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.droppable {
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||||
border-radius: 4px;
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.queue > span {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(h1) {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(a svg) {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .7rem;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
|
||||
&:active {
|
||||
padding: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
&.active, &:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
right: -1.5rem;
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
background-color: var(--color-highlight);
|
||||
box-shadow: 0 0 40px 10px var(--color-highlight);
|
||||
border-radius: 9999rem;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(li li a) { // submenu items
|
||||
padding-left: 11px;
|
||||
|
||||
&:active {
|
||||
padding: 2px 0 0 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@include themed-background();
|
||||
transform: translateX(-100vw);
|
||||
transition: transform .2s ease-in-out;
|
||||
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
height: calc(100vh - var(--header-height));
|
||||
|
||||
&.showing {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,35 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { screen } from '@testing-library/vue'
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
import SidebarItem from './SidebarItem.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(SidebarItem, {
|
||||
props: {
|
||||
icon: faHome,
|
||||
href: '#',
|
||||
screen: 'Home'
|
||||
},
|
||||
slots: {
|
||||
default: 'Home'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test() {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
it('activates when the screen matches', async () => {
|
||||
this.renderComponent()
|
||||
|
||||
await this.router.activateRoute({
|
||||
screen: 'Home',
|
||||
path: '_',
|
||||
})
|
||||
|
||||
expect(screen.getByRole('link').classList.contains('active')).toBe(true)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<li>
|
||||
<a :class="active && 'active'" :href="props.href">
|
||||
<icon :icon="props.icon" fixed-width />
|
||||
<span>
|
||||
<slot />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Component, ref } from 'vue'
|
||||
import { useRouter } from '@/composables'
|
||||
|
||||
const props = defineProps<{ href: string; icon: Component, screen: ScreenName }>()
|
||||
|
||||
const active = ref(false)
|
||||
|
||||
const { onRouteChanged } = useRouter()
|
||||
|
||||
onRouteChanged(route => active.value = route.screen === props.screen)
|
||||
</script>
|
|
@ -0,0 +1,17 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { eventBus } from '@/utils'
|
||||
import YouTubeSidebarItem from './YouTubeSidebarItem.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test() {
|
||||
it('renders', async () => {
|
||||
const { html } = this.render(YouTubeSidebarItem)
|
||||
|
||||
eventBus.emit('PLAY_YOUTUBE_VIDEO', { id: '123', title: 'A Random Video' })
|
||||
await this.tick()
|
||||
|
||||
expect(html()).toMatchSnapshot()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<SidebarItem screen="YouTube" href="#/youtube" :icon="faYoutube">{{ title }}</SidebarItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
|
||||
import { ref } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
import SidebarItem from './SidebarItem.vue'
|
||||
|
||||
const title = ref('')
|
||||
|
||||
eventBus.on('PLAY_YOUTUBE_VIDEO', payload => (title.value = payload.title))
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `<li><a class="" href="#"><br data-testid="icon" icon="[object Object]" fixed-width=""><span>Home</span></a></li>`;
|
|
@ -0,0 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `<li><a class="" href="#/youtube"><br data-testid="icon" icon="[object Object]" fixed-width=""><span>A Random Video</span></a></li>`;
|
|
@ -79,7 +79,7 @@ export const useDraggable = (type: DraggableType) => {
|
|||
export const useDroppable = (acceptedTypes: DraggableType[]) => {
|
||||
const acceptsDrop = (event: DragEvent) => {
|
||||
const type = getDragType(event)
|
||||
return type && acceptedTypes.includes(type)
|
||||
return Boolean(type && acceptedTypes.includes(type))
|
||||
}
|
||||
|
||||
const getDroppedData = (event: DragEvent) => {
|
||||
|
|
Loading…
Reference in a new issue