Commit 4a424a9c authored by jackie / Andrea Ida Malkah Klaura's avatar jackie / Andrea Ida Malkah Klaura
Browse files

create separate timeslot component

parent 351fa3d0
<template>
<b-container>
<!-- This first row is so far only used to provide a dropdown for
choosing one of the loaded shows (which the user has access to) -->
<!--
The show picker including the modal to add new shows should also be
refactored into an own component, so we can reuse it in the file and
emission managers
-->
<addShowModal ref="addShowModal" />
<b-row>
<b-col>
<h3>Sendungen verwalten</h3>
......@@ -31,6 +35,7 @@
</b-dropdown>
</b-col>
</b-row>
<hr>
<!-- The jumbotron is used to display the name and description of the
......@@ -52,178 +57,7 @@
<!-- When all show data is loaded, here we display all the rest -->
<div v-else>
<!-- include the modals to edit show and timeslot entries from the modal compontents -->
<app-modalNotes
ref="appModalNotes"
:show="shows[currentShow]"
:show-aggregate="current"
/>
<addShowModal
ref="addShowModal"
/>
<app-modalPlaylist
ref="appModalPlaylist"
/>
<!-- here are the filter settings for our timeslots table -->
<b-card>
<b-row>
<b-col>
<b-btn v-b-toggle.timeslotFilterCollapse>
Toggle timeslot filters
</b-btn>
</b-col>
<b-col align="right">
<b-button
v-if="$parent.user.steeringUser.is_superuser"
variant="info"
@click="$router.push({path: 'emissions', query: { show: currentShow }})"
>
Switch to Emission Manager
</b-button>
</b-col>
</b-row>
<b-collapse id="timeslotFilterCollapse">
<br>
<!-- How many slots to show per table page -->
<b-row>
<b-col sm="3">
<label for="inputNumSlots">Number of slots to show:</label>
</b-col>
<b-col sm="9">
<b-form-input
id="inputNumSlots"
v-model="numSlots"
type="number"
/>
</b-col>
</b-row>
<!-- The start date to display timeslots from (defaults to today) -->
<b-row>
<b-col sm="3">
<label for="inputDateStart">From:</label>
</b-col>
<b-col sm="9">
<b-form-input
id="inputDateStart"
v-model="dateStart"
type="date"
/>
</b-col>
</b-row>
<!-- The end date until to wich to display timeslots -->
<b-row>
<b-col sm="3">
<label for="inputNumSlots">Until (exclusive):</label>
</b-col>
<b-col sm="9">
<b-form-input
id="inputDateEnd"
v-model="dateEnd"
type="date"
/>
</b-col>
</b-row>
<br>
<!-- And finally two buttons, one to reset and one to apply the filter -->
<b-container
fluid
class="text-right"
>
<b-btn
variant="outline-danger"
@click="resetFilter()"
>
Reset filter
</b-btn> &nbsp;
<b-btn
variant="outline-success"
@click="applyFilter()"
>
Apply filter
</b-btn>
</b-container>
</b-collapse>
</b-card>
<br>
<!-- here we show our table of timeslots, if the timeslots are already
loaded (otherwise we just show the loading symbol) -->
<div v-if="loaded.timeslots">
<b-table
striped
hover
outlined
:fields="notesTableArrayFields"
:items="notesTableArray"
>
<!-- Title of the timeslot (if already set) -->
<template v-slot:cell(title)="data">
<span v-if="data.value">{{ data.value }}</span>
<span v-else><small><i>(none set)</i></small></span>
</template>
<!-- Date and time when this timeslot starts -->
<template v-slot:cell(starts)="data">
{{ data.value }}
</template>
<!-- The duration of this timeslot -->
<template v-slot:cell(duration)="data">
{{ data.value }}
</template>
<!-- And here all the buttons for editing and doing other things
with the displayed timeslot -->
<template v-slot:cell(options)="data">
<span
class="timeslotEditLink"
@click="editTimeslotNote(data.item.options.id, data.item.options.schedule)"
><img
src="../assets/16x16/emblem-system.png"
alt="Edit description"
title="Edit description"
></span>
<span
class="timeslotEditLink"
@click="editTimeslotPlaylist(shows[currentShow], data.item.options.schedule, data.item.options.id)"
><img
src="../assets/16x16/media-eject.png"
alt="Edit playlist"
title="Edit playlist"
></span>
<span
v-if="data.item.options.play"
class="timeslotEditLink"
@click="notYetImplemented()"
><img
src="../assets/16x16/media-playback-start.png"
alt="Open player"
title="Open player"
></span>
</template>
<template v-slot:cell(playlist)="data">
<span v-if="data.value">{{ data.value }}</span>
<span v-else><small><i>(none set)</i></small></span>
</template>
</b-table>
<b-pagination
v-model="current.timeslotmeta.page"
align="center"
:total-rows="current.timeslotmeta.count"
:per-page="current.timeslotmeta.perpage"
@change="timeslotsPage"
/>
</div>
<!-- If the timeslot data is not loaded, we just show the spinner instead
of the table itself -->
<div v-else>
<div style="text-align: center;">
<img
src="../assets/radio.gif"
alt="loading data"
><br>
</div>
</div>
<show-timeslots ref="timeslotsComponent" />
<hr>
......@@ -240,580 +74,69 @@
<script>
import showJumbotron from './shows/Jumbotron.vue'
import showTimeslots from './shows/Timeslots.vue'
import showMetaSimpleTypes from './shows/MetaSimpleTypes.vue'
import showMetaArrays from './shows/MetaArrays.vue'
import showMetaOwners from './shows/MetaOwners.vue'
import showMetaImages from './shows/MetaImages.vue'
import modalAddShow from './shows/AddShowModal.vue'
import modalNotes from './ShowManagerModalNotes.vue'
import modalPlaylist from './ShowManagerModalPlaylist.vue'
import timeslotSort from '../mixins/timeslotSort'
import prettyDate from '../mixins/prettyDate'
import axios from 'axios'
import DOMPurify from 'dompurify'
import { mapGetters } from 'vuex'
export default {
// all modals to edit a show and its timeslots/notes, are importet as separate
// components, to make it a tiny lickle bit less messy here
components: {
'app-modalNotes': modalNotes,
'app-modalPlaylist': modalPlaylist,
'addShowModal': modalAddShow,
'show-jumbotron': showJumbotron,
'show-timeslots': showTimeslots,
'show-metaArrays': showMetaArrays,
'show-metaSimpleTypes': showMetaSimpleTypes,
'show-metaOwners': showMetaOwners,
'show-metaImages': showMetaImages,
},
// generic functions that we want to use from our mixins folder
mixins: [ timeslotSort, prettyDate ],
// this component will be handling a lot of data - probably the component can
// be refactored to get rid of some redundancy here
data () {
return {
shows: [], // an array of objects describing our shows (empty at load, will be populated on created())
numSlots: process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_NUMSLOTS, // all form input values are provided as strings
dateStart: this.apiDate(new Date()),
dateEnd: this.apiDate(new Date(new Date().getTime() + process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_DAYS * 86400000)),
// the loaded object holds flags for the different things we will fetch
// from the AuRa steering module
loaded: {
shows: false,
timeslots: false,
notes: false,
categories: false,
hosts: false,
languages: false,
topics: false,
musicfocus: false,
fundingcategory: false,
type: false,
owners: false,
playlists: false,
},
// the current object is used to hold all the necessary data to describe
// the show which is currently selected by the user in the frontend
current: {
categories: [],
hosts: [],
languages: [],
topics: [],
musicfocus: [],
fundingcategory: [],
type: [],
playlists: [],
timeslots: [],
timeslotmeta: { // meta info when pagination is used
count: 0,
next: null,
previous: null,
page: 1, // page indexes start at 1 for <b-pagination> components
perpage: 10
},
note: {},
notes: [],
image: '',
logo: '',
owners: [],
},
// this is used to configure the table with all the filtered timeslots
notesTableArrayFields: [
{ key: 'title', label: 'Title of emission' },
{ key: 'starts', label: 'Emission start' },
{ key: 'duration', label: 'Duration' },
{ key: 'options', label: 'Edit' },
{ key: 'playlist', label: 'Playlist' },
]
}
},
// Some of the info we need in the template are not easily and directly
// retrievable, so we are computing them on the fly, when they are needed
computed: {
currentShow () { return this.$store.state.shows.selected.index },
currentShowID () { return this.$store.state.shows.selected.id },
user () { return this.$store.state.auth.user },
isSuperuser () { return this.$store.state.auth.user.steeringUser.is_superuser },
// As the show description should allow to be html-formatted, we have to
// make sure no malicous code can be inserted into the DOM. For that the
// DOMPurify library (https://github.com/cure53/DOMPurify) does us a much
// better service than trying to sanitize it with some RegExp.
sanitizedShowDescription: function () {
return DOMPurify.sanitize(this.shows[this.currentShow].description)
loaded () {
return {
shows: this.$store.state.shows.loaded.shows,
}
},
// As we do not have a single object which holds all info we need to display
// in the table with our timeslots, we use this computed array to do that
notesTableArray: function () {
let rows = []
for (var i in this.current.timeslots) {
let note = this.getNoteByTimeslotID(this.current.timeslots[i].id)
if (note !== null) { note = note.title }
let playlistTitle = ''
if (this.current.timeslots[i].playlist_id !== null) {
let playlist = this.current.playlists.find(list => list.id === this.current.timeslots[i].playlist_id)
if (playlist) {
if (playlist.description.length > 0) {
playlistTitle = playlist.description
} else {
playlistTitle = playlist.id
}
}
}
rows.push({
title: note,
starts: this.prettyDateTime(this.current.timeslots[i].start),
duration: this.prettyDuration(this.current.timeslots[i].start, this.current.timeslots[i].end),
options: {
id: this.current.timeslots[i].id,
schedule: this.current.timeslots[i].schedule,
play: this.current.timeslots[i].playlist_id !== null
},
playlist: playlistTitle
})
}
return rows
}
...mapGetters({
shows: 'shows/shows',
selectedShow: 'shows/selectedShow',
})
},
// Right after this component is set up, we want to fetch all available shows
// from the AuRa steering module.
// and the arrays for the show meta info from the AuRa steering module.
created () {
// As we don't know any shows yet, we use null as id parameter, so the
// first show in the returned show array will be displayed
this.loadAndSwitch(null)
this.$store.dispatch('shows/fetchShows')
this.$store.dispatch('shows/fetchMetaArray', {property: 'types', onlyActive: true})
this.$store.dispatch('shows/fetchMetaArray', {property: 'fundingcategories', onlyActive: true})
this.$store.dispatch('shows/fetchMetaArray', {property: 'categories'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'topics'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'musicfocus'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'languages'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'hosts'})
if (this.isSuperuser) { this.$store.dispatch('auth/fetchUsers') }
},
// Now for our hotchpotch of methods, mostly used for fetching data from the
// AuRa steering API (updateing will be done in the imported modal components)
methods: {
// Apply the newly set filter parameters for our timeslot table
applyFilter: function () {
this.current.timeslotmeta.page = 1
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
// Reset the filter parameters for our timeslot table to config defaults
resetFilter: function () {
this.numSlots = process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_NUMSLOTS
this.dateStart = this.apiDate(new Date())
this.dateEnd = this.apiDate(new Date(new Date().getTime() + process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_DAYS * 86400000))
this.current.timeslotmeta.page = 1
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
// Load a different page of timeslots for the timeslots table
timeslotsPage: function (page) {
if (this.current.timeslotmeta.page !== page) {
this.current.timeslotmeta.page = page
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots, (page - 1) * this.numSlots)
}
},
// Every time the user switches to another show, we will load all related
// data (as e.g. categories, hosts, etc.) from the AuRa steering API, in case
// something changed.
// TODO/discuss: maybe this is too inefficient and we could only load this
// data just in the beginning after shows are loaded and assume they do not
// change throughout a session or put it into the user's responsibility to
// reload the show manager page after relevant changes.
switchShow: function (index) {
// if we already had some show loaded with timeslots and notes, set these to
// not loaded, so we don't display old timeslots and notes while already
// the new show is displayed and new timeslots and notes are still loading
this.loaded.timeslots = false
this.loaded.notes = false
// also for those settings of the show which are only ids or arrays of ids
// we have to fetch the corresponding names first
this.loaded.categories = false
this.loaded.hosts = false
this.loaded.languages = false
this.loaded.musicfocus = false
this.loaded.fundingcategory = false
this.loaded.owners = false
// set the current show and its ID to whatever we want to switch to now
this.$store.commit('shows/switchShow', index)
// and check if images are available and set image strings, because we
// cannot use them directly inside the b-img if they are null
if (this.shows[this.currentShow].logo === null) { this.current.logo = '' }
else { this.current.logo = this.shows[this.currentShow].logo }
if (this.shows[this.currentShow].image === null) { this.current.image = '' }
else { this.current.image = this.shows[this.currentShow].image }
// before we load timeslots and notes, we want to fetch the general settings first
this.getCategories()
this.getHosts()
this.getLanguages()
this.getTopics()
this.getMusicfocus()
this.getFundingCategory()
this.getType()
if (this.$parent.user.steeringUser.is_superuser) {
this.getOwners()
}
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
this.loadPlaylists()
},
// (Re)Load all shows from server and switch to a show with a specific ID.
// If the id argument is null, the first show in the show array will be used
loadAndSwitch: function (id) {
this.$store.dispatch('shows/fetchShows')
this.$store.dispatch('shows/fetchMetaArray', {property: 'types', onlyActive: true})
this.$store.dispatch('shows/fetchMetaArray', {property: 'fundingcategories', onlyActive: true})
this.$store.dispatch('shows/fetchMetaArray', {property: 'categories'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'topics'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'musicfocus'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'languages'})
this.$store.dispatch('shows/fetchMetaArray', {property: 'hosts'})
if (this.isSuperuser) { this.$store.dispatch('auth/fetchUsers') }
this.loaded.shows = false
var uri = process.env.VUE_APP_API_STEERING_SHOWS
// normal users should only see their own shows, only superusers see all shows
if (!this.$parent.user.steeringUser.is_superuser) {
uri += '?owner=' + this.$parent.user.steeringUser.id
}
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// if now shows are found, we'll just print a short info message and leave
if (response.data.length === 0) {
this.$log.info('The returned show set has 0 length:')
this.$log.info(response)
alert('Info: There are no shows connected to your account. See console for details.')
this.loaded.shows = true
return
}
// now set the new show array and find the index of the show ID
this.shows = response.data
let index = 0
if (id !== null) {
index = this.shows.findIndex(show => show.id === id)
// if no show with the given ID was found, we use the first item in the show array
if (index === -1) { index = 0 }
}
this.$store.commit('shows/switchShow', index)
this.loaded.shows = true
this.switchShow(this.currentShow)
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not fetch show data. See console for details.')
})
},
loadPlaylists () {
this.loaded.playlists = false
let uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/playlists'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// we don't have to check separately, if there are playlists, because tank
// always provides an empty array if there are no playlists (or even if there is no corresponding show)
this.current.playlists = response.data.results
this.loaded.playlists = true
}).catch(error => {
//this.$log.error(error.response.status + ' ' + error.response.statusText)
//this.$log.error(error.response)
this.$log.error(error)
alert('Error: could not fetch playlists from tank. See console for details.')
})
},
// Fetch timeslots for the current show and use filter variables if provided
getTimeslots: function (start, end, limit, offset) {
var dateRegex = new RegExp('^\\d{4}-\\d{2}-\\d{2}$')
var uri = process.env.VUE_APP_API_STEERING_SHOWS + this.currentShowID + '/timeslots/?'
if (dateRegex.test(start)) { uri += 'start=' + start + '&' }
if (dateRegex.test(end)) { uri += 'end=' + end + '&' }
if (!isNaN(parseInt(limit))) { uri += 'limit=' + parseInt(limit) + '&' }
if (!isNaN(parseInt(offset))) { uri += 'offset=' + parseInt(offset) }
this.loaded.timeslots = false
this.loaded.notes = false
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// if we use the limit argument results are paginated and look different
// than without the limit argument
if (!isNaN(parseInt(limit))) {
this.current.timeslots = response.data.results
this.current.timeslotmeta.count = response.data.count
this.current.timeslotmeta.next = response.data.next
this.current.timeslotmeta.previous = response.data.previous
this.current.timeslotmeta.perpage = parseInt(limit)
} else {
this.current.timeslots = response.data
this.current.timeslotmeta.count = response.data.length
this.current.timeslotmeta.next = null
this.current.timeslotmeta.previous = null
this.current.timeslotmeta.perpage = response.data.length
}
this.loaded.timeslots = true
// now that we have the timeslots we can fetch notes for all those timeslots
uri = process.env.VUE_APP_API_STEERING_SHOWS + this.currentShowID + '/notes/?ids='
// add all note IDs from the timeslots that have existing notes
var thereIsANote = false
for (var i in this.current.timeslots) {
if (typeof this.current.timeslots[i].note_id === 'number') {
uri += this.current.timeslots[i].note_id + ','
thereIsANote = true
}
}
// now remove trailing ',' if at least one note already exists
// and make the api call to fetch them
if (thereIsANote) {
uri = uri.slice(0, -1)
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
this.current.notes = response.data
this.loaded.notes = true
}).catch(error => {
alert('There was an error fetching notes from the server' + error)
})
// done fetching notes
} else {
// if no notes exist that correspond to our selected timeslots, empty
// the corresponding array
this.current.notes = []
}
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not fetch timeslots. See console for details.')
})
// done fetching timeslots
},
// Open the modal to edit a timeslot's note, given its ID and schedule ID
editTimeslotNote: function (timeslotID, scheduleID) {
this.current.note = null
for (var i in this.current.notes) {
if (this.current.notes[i].timeslot === timeslotID) {
this.current.note = this.current.notes[i]
break
}
}
this.$refs.appModalNotes.showModal(this.current.note, timeslotID, scheduleID)
},
editTimeslotPlaylist: function (show, schedule, timeslot) {
this.$refs.appModalPlaylist.open(show, schedule, timeslot, this.current.playlists)
},
// For a given timeslot ID return the corresponding note, if there is one
getNoteByTimeslotID: function (timeslotID) {
for