diff --git a/src/Pages/EmissionManager.vue b/src/Pages/EmissionManager.vue index 68474598868e36f2dcf89a97bb820085f5cfa7fe..1d297e1a841918a387de403299926df372e458e9 100644 --- a/src/Pages/EmissionManager.vue +++ b/src/Pages/EmissionManager.vue @@ -9,7 +9,6 @@ {{ $t('calendar.view.week') }} </b-button> </b-button-group> - <ShowSelector /> </PageHeader> <template v-if="!loaded.shows"> @@ -218,7 +217,6 @@ import FullCalendar from '@fullcalendar/vue3' import fullCalendarTimeGridPlugin from '@fullcalendar/timegrid' import fullCalendarInteractionPlugin from '@fullcalendar/interaction' -import ShowSelector from '@/components/ShowSelector.vue' import modalEmissionManagerCreate from '@/components/emissions/ModalCreate.vue' import modalEmissionManagerResolve from '@/components/emissions/ModalResolve.vue' import modalEmissionManagerEdit from '@/components/emissions/ModalEdit.vue' @@ -236,7 +234,6 @@ export default { ServerErrors, FullCalendar, AuthWall, - ShowSelector, 'app-modalEmissionManagerCreate': modalEmissionManagerCreate, 'app-modalEmissionManagerResolve': modalEmissionManagerResolve, 'app-modalEmissionManagerEdit': modalEmissionManagerEdit, diff --git a/src/Pages/FileManager.vue b/src/Pages/FileManager.vue index 0489718d16149771971c804cf3d0c91eec15fa28..5a8d98f254a5317fa4dcd80d060ba335ffe9e160 100644 --- a/src/Pages/FileManager.vue +++ b/src/Pages/FileManager.vue @@ -1,8 +1,6 @@ <template> <b-container> - <PageHeader :title="$t('filePlaylistManager.title')"> - <ShowSelector /> - </PageHeader> + <PageHeader :title="$t('filePlaylistManager.title')" /> <template v-if="!loaded.shows"> <div class="tw-text-center"> @@ -30,7 +28,6 @@ <script> import { mapGetters } from 'vuex' -import ShowSelector from '../components/ShowSelector.vue' import jumbotron from '../components/filemanager/Jumbotron.vue' import files from '../components/filemanager/Files.vue' import playlists from '../components/filemanager/Playlists.vue' @@ -39,7 +36,6 @@ import PageHeader from '@/components/PageHeader.vue' export default { components: { PageHeader, - ShowSelector, jumbotron: jumbotron, files: files, playlists: playlists, diff --git a/src/Pages/ShowManager.vue b/src/Pages/ShowManager.vue index f685a0244fa4b4524f786f1289a1c707754072d5..2c4b11f387dbabb082e435ed5d084736271f8758 100644 --- a/src/Pages/ShowManager.vue +++ b/src/Pages/ShowManager.vue @@ -1,8 +1,6 @@ <template> <b-container> - <PageHeader :title="t('showManager.title')"> - <ShowSelector /> - </PageHeader> + <PageHeader :title="t('showManager.title')" /> <template v-if="!hasLoadedShows"> <div class="tw-text-center"> @@ -40,7 +38,6 @@ import ShowMetaSimpleTypes from '../components/shows/MetaSimpleTypes.vue' import ShowMetaArrays from '../components/shows/MetaArrays.vue' import ShowMetaOwners from '../components/shows/MetaOwners.vue' import ShowMetaImages from '../components/shows/MetaImages.vue' -import ShowSelector from '../components/ShowSelector.vue' import { useStore } from 'vuex' import PageHeader from '@/components/PageHeader.vue' import { useAuthStore, useUserStore } from '@/stores/auth' diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 18e88d7de5e5082534be65c2dcc070ad17a54edd..90167c6286a8ae285d2222b548b53856872fb1df 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -25,6 +25,11 @@ </b-navbar-nav> <b-navbar-nav class="tw-flex tw-items-center"> + <ShowSelector + input-class="tw-bg-white/10 tw-rounded-full tw-border-gray-900 tw-border tw-border-solid tw-shadow-inner" + input-container-class="tw-text-gray-200" + drawer-class="tw-text-gray-900" + /> <b-nav-item-dropdown :text="locale.toUpperCase()" right> <b-dropdown-item v-for="availableLocale in availableLocales" @@ -77,6 +82,7 @@ import { useI18n } from '@/i18n' import { logoutRedirect } from '@/oidc' import { Module } from '@/types' import { useAuthStore } from '@/stores/auth' +import ShowSelector from '@/components/shows/ShowSelector.vue' defineProps({ modules: { type: Array as PropType<Module[]>, required: true }, diff --git a/src/components/shows/AddShowButton.vue b/src/components/shows/AddShowButton.vue new file mode 100644 index 0000000000000000000000000000000000000000..38425dde1883a5a467ed8e826351660852aa9a34 --- /dev/null +++ b/src/components/shows/AddShowButton.vue @@ -0,0 +1,53 @@ +<template> + <div v-if="loaded.shows"> + <AddShowModal v-if="!modal" ref="addShowModal" /> + + <b-button + v-if="authStore.isSuperuser" + variant="primary" + data-testid="show-selector:add-show" + class="md:tw-whitespace-nowrap" + @click="resolvedModal.openModal()" + > + {{ t('showCreator.title') }} + </b-button> + </div> +</template> + +<script lang="ts" setup> +import { useStore } from 'vuex' +import { computed, ref } from 'vue' + +import AddShowModal from './AddShowModal.vue' +import { useI18n } from '@/i18n' +import { useAuthStore } from '@/stores/auth' + +type Modal = { + openModal: () => void +} + +const props = defineProps<{ + modal?: Modal +}>() + +const store = useStore() +const authStore = useAuthStore() +const { t } = useI18n() + +const addShowModal = ref<Modal>() +const resolvedModal = computed(() => (props.modal ?? addShowModal) as unknown as Modal) +const loaded = computed(() => ({ + shows: store.state.shows.loaded.shows, + types: store.state.shows.loaded.types, + fundingCategories: store.state.shows.loaded.fundingcategories, +})) + +// TODO: not sure these belong here. +if (!loaded.value.types) { + store.dispatch('shows/fetchMetaArray', { property: 'types', onlyActive: true }) +} + +if (!loaded.value.fundingCategories) { + store.dispatch('shows/fetchMetaArray', { property: 'fundingcategories', onlyActive: true }) +} +</script> diff --git a/src/components/shows/ShowSelector.vue b/src/components/shows/ShowSelector.vue new file mode 100644 index 0000000000000000000000000000000000000000..06793fc860cbdb2715b1c5cdf3cd65bc78759e49 --- /dev/null +++ b/src/components/shows/ShowSelector.vue @@ -0,0 +1,124 @@ +<template> + <ComboBox + v-if="!areShowsLoading" + v-model="selectedShow" + :label="t('showSelector.selectShow')" + :no-data-label="hasShows ? t('showSelector.noDataMatch') : t('showSelector.noData')" + :keyboard-shortcut="keys.ctrl_b" + :keyboard-shortcut-label="t('showSelector.keyboardShortcut')" + :choices="filteredShows" + data-testid="show-selector" + @search="showSearchQuery = $event" + @close="filterActive = null" + > + <template #default="{ choice: show, index, activeIndex, ...attrs }"> + <li v-bind="attrs"> + <p class="tw-m-0 tw-font-bold tw-flex tw-items-baseline tw-justify-between"> + <span>{{ show.name }}</span> + <span class="tw-text-xs tw-opacity-75 tw-flex-none tw-font-normal"> + ID: {{ show.id }} + </span> + </p> + <span + v-if="!show.is_active" + class="tw-text-sm tw-rounded-full tw-bg-black/10 tw-text-black/75 tw-px-2 tw-inline-block tw-float-right tw-m-0 tw-ml-1 tw-mb-1" + > + {{ t('showSelector.showState.inactive') }} + </span> + <p v-show="activeIndex === index" class="tw-my-1 tw-text-sm tw-opacity-90"> + {{ show.short_description }} + </p> + <span class="tw-clear-both" /> + </li> + </template> + + <template #filter> + <div v-if="hasShows"> + <p class="mb-2 tw-text-sm tw-font-bold">{{ t('showSelector.showState.label') }}</p> + <div class="tw-flex tw-text-sm"> + <SwitchButton + :model-value="filterActive === true" + class="tw-whitespace-nowrap" + :label="t('showSelector.showState.active')" + @update:model-value="toggleFilterActive(true)" + /> + <SwitchButton + :model-value="filterActive === false" + class="tw-whitespace-nowrap" + :label="t('showSelector.showState.inactive')" + @update:model-value="toggleFilterActive(false)" + /> + </div> + </div> + + <AddShowButton class="tw-block" :class="{ 'tw-mt-auto': hasShows }" :modal="addShowModal" /> + </template> + + <template #pre> + <AddShowModal ref="addShowModal" /> + </template> + </ComboBox> +</template> + +<script lang="ts" setup> +import { sort } from 'fast-sort' +import { computed, ref } from 'vue' +import { useStore } from 'vuex' +import { useMagicKeys } from '@vueuse/core' + +import { useI18n } from '@/i18n' +import { useSelectedShow } from '@/utilities' +import SwitchButton from '@/components/SwitchButton.vue' +import ComboBox from '@/components/ComboBox.vue' +import AddShowButton from '@/components/shows/AddShowButton.vue' +import AddShowModal from '@/components/shows/AddShowModal.vue' + +type Show = { + id: number + is_active: boolean + name: string + short_description: string +} + +const keys = useMagicKeys() +const store = useStore() +const { t } = useI18n() +const selectedShow = useSelectedShow() + +const showSearchQuery = ref('') +const filterActive = ref<null | boolean>(null) +const addShowModal = ref() +const shows = computed<Show[]>(() => store.state.shows.shows) +const areShowsLoading = computed(() => !store.state.shows.loaded.shows) +const hasShows = computed(() => shows.value.length > 0) +const sortedShows = computed(() => { + return sort(shows.value).by([ + { desc: (show) => show.is_active }, + { asc: (show) => show.name.toLowerCase() }, + ]) +}) +const filteredShows = computed(() => { + return sortedShows.value + .filter((show) => (filterActive.value !== null ? show.is_active === filterActive.value : true)) + .filter( + (show) => + show.name.includes(showSearchQuery.value) || show.id.toString() === showSearchQuery.value, + ) +}) + +function toggleFilterActive(toggleState: boolean) { + if (filterActive.value === toggleState) { + filterActive.value = null + return + } + filterActive.value = toggleState +} +</script> + +<script lang="ts"> +export default { + compatConfig: { + MODE: 3, + }, +} +</script> diff --git a/src/i18n/de.js b/src/i18n/de.js index a0aa90e39cfb4f66ca5a09939f14dc211862865a..0b1f7827f01f10cff6d09290a33f9880dc678a3e 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -187,9 +187,16 @@ export default { }, showSelector: { + keyboardShortcut: 'Strg + B', selectShowMany: 'Sendereihe wählen', selectShow: 'Sendereihe auswählen', - inactiveShow: 'inaktiv', + noData: 'Es wurden bisher keine Sendereihen angelegt.', + noDataMatch: 'Es gibt keine Sendereihen, die deinen Suchkriterien entsprechen.', + showState: { + label: 'Status der Sendereihe', + active: 'Aktiv', + inactive: 'Inaktiv', + }, }, showJumbotron: { diff --git a/src/i18n/en.js b/src/i18n/en.js index fb494b435978e1a0597d75d9775acc729a6549a9..f5a46bb7abf49bf46b9a8b883ecaa4a18a0b4efc 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -187,9 +187,16 @@ export default { }, showSelector: { + keyboardShortcut: 'Ctrl + B', selectShowMany: 'Select show:', selectShow: 'Select a radio show', - inactiveShow: 'inactive', + noData: 'No shows have been created yet.', + noDataMatch: 'There are no shows that match your search criteria.', + showState: { + label: 'Show status', + active: 'Active', + inactive: 'Inactive', + }, }, showJumbotron: { diff --git a/src/utilities.js b/src/utilities.js index adcc151a22a4b9b8e5ae0cb63e29f9886487f9be..9d5d60990910d1cee3774c723da2beb66059066b 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -35,8 +35,13 @@ export function getTimeString(date, withSeconds = false) { export function useSelectedShow() { const store = useStore() - return computed(() => { - return store.state.shows.shows[store.state.shows.selected.index] + return computed({ + get() { + return store.state.shows.shows[store.state.shows.selected.index] + }, + set(show) { + store.commit('shows/switchShowById', show.id) + }, }) } export function shouldLog(message, instance, trace) { diff --git a/tests/shows.spec.ts b/tests/shows.spec.ts index dd313d853d4eb72fbac2f2ddbbc216a107d2d36e..f6aa9c3d76a57c21f8db4429e6d97d6f0e69fcb7 100644 --- a/tests/shows.spec.ts +++ b/tests/shows.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test' test('Can create new show.', async ({ page }) => { await page.goto('/') await page.getByTestId('navbar:shows').click() + await page.getByTestId('show-selector').locator('input[id^="combobox-input-"]').focus() await page.getByTestId('show-selector:add-show').click() await page.getByTestId('add-show-modal:show-name').fill('my series') await page.getByTestId('add-show-modal:show-description').fill('my series description')