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

create separate timeslot component

parent 351fa3d0
This diff is collapsed.
......@@ -74,16 +74,14 @@
</template>
<script>
import prettyDate from '../mixins/prettyDate'
import slugify from '../mixins/slugify'
import { mapGetters } from 'vuex'
import prettyDate from '../../mixins/prettyDate'
import slugify from '../../mixins/slugify'
import axios from 'axios'
export default {
mixins: [ prettyDate, slugify ],
props: {
show: { type: Object, required: true },
showAggregate: { type: Object, required: true }
},
data () {
return {
note: {},
......@@ -99,24 +97,30 @@ export default {
host_selected: null
}
},
computed: {
slug: function () {
return this.slugify(this.title)
},
hosts: function () {
slug () { return this.slugify(this.title) },
hosts () {
// for the vue bootstrap select component we need an array of objects
// with a value, a text and optionally a disabled element
var hosts = []
for (var i in this.showAggregate.hosts) {
let hosts = []
for (let id of this.selectedShow.hosts) {
let host = this.allHosts.find(h => h.id === id)
hosts.push({
value: this.showAggregate.hosts[i].id,
text: this.showAggregate.hosts[i].name,
disabled: !this.showAggregate.hosts[i].is_active
value: host.id,
text: host.name,
disabled: !host.is_active
})
}
return hosts
}
},
...mapGetters({
selectedShow: 'shows/selectedShow',
allHosts: 'shows/hosts',
})
},
methods: {
update (event) {
// only try to save if anything has changed
......@@ -135,7 +139,7 @@ export default {
this.note.host = this.host_selected
// generate the uri for the API call:
// /api/v1/shows/1/schedules/1/timeslots/1/note/1/
var uri = process.env.VUE_APP_API_STEERING_SHOWS + this.show.id +
var uri = process.env.VUE_APP_API_STEERING_SHOWS + this.selectedShow.id +
'/schedules/' + this.scheduleID +
'/timeslots/' + this.timeslotID +
'/note/' + this.note.id + '/'
......@@ -163,6 +167,7 @@ export default {
this.$refs.modalNote.hide()
}
},
new (event) {
// title and content are necessary
if (this.title.trim() === '' || this.content.trim() === '') {
......@@ -174,7 +179,7 @@ export default {
event.preventDefault()
// prepare the new note
this.note = {
show: this.show.id,
show: this.selectedShow.id,
timeslot: this.timeslotID,
host: this.host_selected,
title: this.title,
......@@ -190,43 +195,28 @@ export default {
cba_id: null, // TODO: implement
audio_url: '' // TODO: implement
}
// generate the uri for the API call:
// /api/v1/shows/1/schedules/1/timeslots/1/note
var uri = process.env.VUE_APP_API_STEERING_SHOWS + this.show.id +
'/schedules/' + this.scheduleID +
'/timeslots/' + this.timeslotID +
'/note/'
// now send the POST request with our updated note
axios.post(uri, this.note, {
withCredentials: true,
responseType: 'json', // we need this explicitly here, as it does not seem to work automagically as in GET and PUT requests
headers: { 'Authorization': 'Bearer ' + this.$parent.$parent.user.access_token }
}).then(response => {
this.note = response.data
for (var i in this.showAggregate.timeslots) {
if (this.showAggregate.timeslots[i].id === this.timeslotID) {
this.showAggregate.timeslots[i].note_id = this.note.id
this.showAggregate.notes.push(this.note)
}
let modal = this.$refs.modalNote
this.$store.dispatch('shows/submitNote', {
id: this.selectedShow.id,
scheduleID: this.scheduleID,
timeslotID: this.timeslotID,
note: this.note,
callback: () => { modal.hide() },
callbackCancel: () => {
// as there was an error saving the show, we have to make sure
// to restore the initial values of the note object
this.note.title = this.backuptitle
this.note.summary = this.backupsummary
this.note.content = this.backupcontent
// and we have to set this back to undefined so next time we edit it
// it will still be treated as a new note and not an existing one to update
this.note.start = undefined
// and we leave the modal open, so no call to its .hide function here
}
// everything was fine, we can close the modal now
this.$refs.modalNote.hide()
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not post the new note. See console for details.')
// as there was an error saving the show, we have to make sure
// to restore the initial values of the note object
this.note.title = this.backuptitle
this.note.summary = this.backupsummary
this.note.content = this.backupcontent
// and we have to set this back to undefined so next time we edit it
// it will still be treated as a new note and not an existing one to update
this.note.start = undefined
// and we leave the modal open, so no call to its .hide function here
})
}
},
saveNote (event) {
if (typeof this.note.start === 'undefined') {
this.new(event)
......@@ -234,7 +224,8 @@ export default {
this.update(event)
}
},
showModal (note, timeslotID, scheduleID) {
openModal (note, timeslotID, scheduleID) {
if (note === null) {
this.note = {}
this.title = ''
......@@ -242,7 +233,7 @@ export default {
this.content = ''
// TODO: integrate this into the user's app settings:
// should the field be empty by default or filled with the first host of the show?
this.host_selected = this.showAggregate.hosts[0].id
this.host_selected = null
} else {
this.note = note
this.title = this.note.title
......
......@@ -66,7 +66,7 @@
</div>
<div v-else>
<img
src="../assets/radio.gif"
src="../../assets/radio.gif"
alt="loading playlists"
>
</div>
......@@ -82,7 +82,7 @@
<script>
import axios from 'axios'
import prettyDate from '../mixins/prettyDate'
import prettyDate from '../../mixins/prettyDate'
export default {
mixins: [ prettyDate ],
......
<template>
<div>
<app-modalNotes ref="appModalNotes" />
<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="user.steeringUser.is_superuser"
variant="info"
@click="$router.push({path: 'emissions', query: { show: selectedShow.slug }})"
>
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(selectedShow, 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="timeslotmeta.page"
align="center"
:total-rows="timeslotmeta.count"
:per-page="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>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import modalNotes from './NotesModal.vue'
import modalPlaylist from './ShowManagerModalPlaylist.vue'
import timeslotSort from '../../mixins/timeslotSort'
import prettyDate from '../../mixins/prettyDate'
import axios from 'axios'
export default {
components: {
'app-modalNotes': modalNotes,
'app-modalPlaylist': modalPlaylist,
},
mixins: [ timeslotSort, prettyDate ],
data () {
return {
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)),
loadedLocal: {
playlists: false,
},
note: {},
playlists: [],
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
},
// 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' },
],
}
},
computed: {
shows () { return this.$store.state.shows.shows },
user () { return this.$store.state.auth.user },
isSuperuser () { return this.$store.state.auth.user.steeringUser.is_superuser },
loaded () {
return {
shows: this.$store.state.shows.loaded.shows,
timeslots: this.$store.state.shows.loaded.timeslots,
notes: this.$store.state.shows.loaded.notes,
}
},
// 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 (let i in this.timeslots) {
let note = this.getNoteByTimeslotID(this.timeslots[i].id)
if (note !== null) { note = note.title }
let playlistTitle = ''
if (this.timeslots[i].playlist_id !== null) {
let playlist = this.playlists.find(list => list.id === this.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.timeslots[i].start),
duration: this.prettyDuration(this.timeslots[i].start, this.timeslots[i].end),
options: {
id: this.timeslots[i].id,
schedule: this.timeslots[i].schedule,
play: this.timeslots[i].playlist_id !== null
},
playlist: playlistTitle
})
}
return rows
},
...mapGetters({
selectedShow: 'shows/selectedShow',
timeslots: 'shows/timeslots',
notes: 'shows/notes',
})
},
created () {
this.resetFilter()
},
methods: {
// Apply the newly set filter parameters for our timeslot table
applyFilter () {
this.timeslotmeta.page = 1
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
// Reset the filter parameters for our timeslot table to config defaults
resetFilter () {
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.timeslotmeta.page = 1
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
},
showHasSwitched () {
this.resetFilter()
},
// Load a different page of timeslots for the timeslots table
timeslotsPage (page) {
if (this.timeslotmeta.page !== page) {
this.timeslotmeta.page = page
this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots, (page - 1) * this.numSlots)
}
},
loadPlaylists () {
this.loadedLocal.playlists = false
let uri = process.env.VUE_APP_API_TANK + 'shows/' + this.selectedShow.slug + '/playlists'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.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.playlists = response.data.results
this.loadedLocal.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 (start, end, limit, offset) {
let options = {}
let dateRegex = new RegExp('^\\d{4}-\\d{2}-\\d{2}$')
if (dateRegex.test(start)) { options.start = start }
if (dateRegex.test(end)) { options.end = end }
if (!isNaN(parseInt(limit))) { options.limit = limit }
if (!isNaN(parseInt(offset))) { options.offset = offset }
this.$store.dispatch('shows/fetchTimeslots', {
id: this.selectedShow.id,
callback: (response) => {
if (!isNaN(parseInt(limit))) {
this.timeslotmeta.count = response.data.count
this.timeslotmeta.next = response.data.next
this.timeslotmeta.previous = response.data.previous
this.timeslotmeta.perpage = parseInt(limit)
} else {
this.timeslotmeta.count = response.data.length
this.timeslotmeta.next = null
this.timeslotmeta.previous = null
this.timeslotmeta.perpage = response.data.length
}
this.getNotes()
},
...options
})
},
getNotes () {
let notes = []
for (let ts of this.timeslots) {
if (typeof ts.note_id === 'number') {
notes.push(ts.note_id)
}
}
this.$store.dispatch('shows/fetchNotes', {
id: this.selectedShow.id,
notes: notes
})
},
// Open the modal to edit a timeslot's note, given its ID and schedule ID
editTimeslotNote (timeslotID, scheduleID) {
if (this.selectedShow.hosts.length === 0) {
alert('There are no hosts set for this show!\n\nNotes can only be edited with active show hosts.')
return
}
this.note = null
for (let i in this.notes) {
if (this.notes[i].timeslot === timeslotID) {
this.note = this.notes[i]
break
}
}
this.$refs.appModalNotes.openModal(this.note, timeslotID, scheduleID)
},
editTimeslotPlaylist (show, schedule, timeslot) {
this.$refs.appModalPlaylist.open(show, schedule, timeslot, this.playlists)
},
// For a given timeslot ID return the corresponding note, if there is one
getNoteByTimeslotID (timeslotID) {
for (let i in this.notes) {
if (this.notes[i].timeslot === timeslotID && this.notes[i].title !== undefined) {
return this.notes[i]
}
}
return null
},
// For a given timeslot ID, check if there is a note and return it
prettyTimeslotNote (timeslotID) {
let note = this.getTimeslotNoteTitle(timeslotID)
if (note !== null) {
return this.prettyTitle(note)
} else {
return ''
}
},
// Limiting display of strings up to 25 characters plus "..."
prettyTitle (title) {
if (title === '') { return '...' }
else if (title.length > 25) { return title.slice(0, 25) + '...' }
else { return title }
},
}
}
</script>
......@@ -25,6 +25,7 @@ const cloneMinimalShowObject = function (show) {
const state = {
shows: [],
timeslots: [],
notes: [],
types: [],
fundingcategories: [],
categories: [],
......@@ -35,6 +36,7 @@ const state = {