datatracker/client/agenda/AgendaDetailsModal.vue
Jennifer Richards 0ad293d6e9
feat: add slides / session materials links to session details modal (#4535)
* feat: add propose/upload slides button to session details modal

* refactor: remove unneeded chaining operator

* test: fix quotes around template string

* feat: link to meeting materials page from AgendaDetailsModal

* refactor: compute session details URL in JS instead of view

* chore: restyle materials page link

* test: fix test case to match changes to session modal
2022-10-12 16:07:23 -05:00

378 lines
8.8 KiB
Vue

<template lang="pug">
n-modal(v-model:show='modalShown')
n-card.agenda-eventdetails(
:bordered='false'
segmented
role='dialog'
aria-modal='true'
v-if='eventDetails'
)
template(#header-extra)
.detail-header
i.bi.bi-clock-history
strong {{eventDetails.start}} - {{eventDetails.end}}
n-button.ms-4.detail-close(
ghost
color='gray'
strong
@click='modalShown = false'
)
i.bi.bi-x
template(#header)
.detail-header
i.bi.bi-calendar-check
span {{eventDetails.day}}
template(#action, v-if='eventDetails.showAgenda')
.detail-action
template(v-if='eventDetails.materialsUrl')
n-button.me-2(
ghost
color='gray'
strong
:href='eventDetails.tarUrl'
tag='a'
aria-label='Download as tarball'
)
i.bi.bi-file-zip.me-2
span Download as tarball
n-button.me-2(
ghost
color='gray'
strong
:href='eventDetails.pdfUrl'
tag='a'
aria-label='Download as PDF'
)
i.bi.bi-file-pdf.me-2
span Download as PDF
n-button.me-2(
ghost
color='gray'
strong
:href='eventDetails.notepadUrl'
tag='a'
aria-label='Notepad'
)
i.bi.bi-journal-text.me-2
span Notepad
n-button.float-end(
ghost
color='gray'
strong
tag='a'
:href='eventDetails.detailsUrl'
target='_blank'
aria-label='Materials page'
)
span.me-2 {{props.event.groupAcronym}} materials page
i.bi.bi-box-arrow-up-right
.detail-content
.detail-title
h6
i.bi.bi-arrow-right-square
span {{eventDetails.title}}
.detail-location
i.bi.bi-geo-alt-fill
n-popover(
v-if='eventDetails.locationName'
trigger='hover'
)
template(#trigger)
span.badge {{eventDetails.locationShort}}
span {{eventDetails.locationName}}
span {{eventDetails.room}}
nav.detail-nav.nav.nav-pills.nav-justified.mt-3
a.nav-link(
:class='{ active: state.tab === `agenda` }'
@click='state.tab = `agenda`'
)
i.bi.bi-list-columns-reverse.me-2
span Agenda
a.nav-link(
:class='{ active: state.tab === `slides` }'
@click='state.tab = `slides`'
)
i.bi.bi-easel.me-2
span Slides
a.nav-link(
:class='{ active: state.tab === `minutes` }'
@click='state.tab = `minutes`'
)
i.bi.bi-journal-text.me-2
span Minutes
.detail-text(v-if='eventDetails.materialsUrl')
template(v-if='state.tab === `agenda`')
iframe(
:src='eventDetails.materialsUrl'
)
template(v-else-if='state.tab === `slides`')
n-card(
:bordered='false'
size='small'
)
.text-center(v-if='state.isLoading')
n-spin(description='Loading slides...')
.text-center.p-3(v-else-if='!state.materials || !state.materials.slides || !state.materials.slides.decks || state.materials.slides.decks.length < 1')
span No slides submitted for this session.
.list-group(v-else)
a.list-group-item(
v-for='deck of state.materials.slides.decks'
:key='deck.id'
:href='deck.url'
target='_blank'
)
i.bi.me-2(:class='`bi-filetype-` + deck.ext')
span {{deck.title}}
template(#action, v-if='state.materials.slides.actions')
n-button(
v-for='action of state.materials.slides.actions'
tag='a'
:href='action.url'
) {{action.label}}
template(v-else)
.text-center(v-if='state.isLoading')
n-spin(description='Loading minutes...')
.text-center.p-3(v-else-if='!state.materials || !state.materials.minutes')
span No minutes submitted for this session.
iframe(
v-else
:src='state.materials.minutes.url'
)
</template>
<script setup>
import { computed, reactive, watch } from 'vue'
import {
NButton,
NCard,
NModal,
NPopover,
NSpin,
useMessage
} from 'naive-ui'
import { useAgendaStore } from './store'
// PROPS
const props = defineProps({
shown: {
type: Boolean,
required: true,
default: false
},
event: {
type: Object,
required: true
}
})
// MESSAGE PROVIDER
const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
// EMIT
const emit = defineEmits(['update:shown'])
// STATE
const state = reactive({
tab: 'agenda',
isLoading: false,
materials: {}
})
// COMPUTED
const eventDetails = computed(() => {
if (!props.event) { return null }
const materialsUrl = props.event.agenda?.url ? (new URL(props.event.agenda.url)).pathname : null
return {
start: props.event.adjustedStart?.toFormat('T'),
end: props.event.adjustedEnd?.toFormat('T'),
day: props.event.adjustedStart?.toFormat('DDDD'),
locationShort: props.event.location?.short,
locationName: props.event.location?.name,
room: props.event.room,
title: props.event.type === 'regular' ? `${props.event.groupName} (${props.event.acronym})` : props.event.name,
showAgenda: props.event.flags.showAgenda,
materialsUrl: materialsUrl,
detailsUrl: `/meeting/${agendaStore.meeting.number}/session/${props.event.acronym}/`,
tarUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.tgz`,
pdfUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.pdf`,
notepadUrl: `https://notes.ietf.org/notes-ietf-${agendaStore.meeting.number}-${props.event.type === 'plenary' ? 'plenary' : props.event.acronym}`,
}
})
const modalShown = computed({
get () {
return props.shown
},
set(value) {
emit('update:shown', value)
}
})
// WATCHERS
watch(() => props.shown, (newValue) => {
if (newValue) {
state.materials = {}
state.tab = 'agenda'
if (props.event.flags.showAgenda) {
fetchSessionMaterials()
}
}
})
// METHODS
async function fetchSessionMaterials () {
if (!props.event) { return null }
state.isLoading = true
try {
const resp = await fetch(
`/api/meeting/session/${props.event.sessionId}/materials`,
{ credentials: 'include' }
)
if (!resp.ok) {
throw new Error(resp.statusText)
}
state.materials = await resp.json()
} catch (err) {
console.warn(err)
message.error('Failed to fetch session materials.')
}
state.isLoading = false
}
</script>
<style lang="scss">
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
.agenda-eventdetails {
width: 90vw;
max-width: 1000px;
.bi {
font-size: 20px;
color: $indigo;
}
.detail-header {
font-size: 20px;
display: flex;
align-items: center;
> .bi {
margin-right: 12px;
}
}
.detail-title {
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
.bi {
margin-right: 12px;
}
h6 {
display: flex;
align-items: center;
}
}
.detail-close .bi {
font-size: 20px;
color: inherit;
}
.detail-location {
display: flex;
align-items: center;
background-color: rgba($indigo, .05);
padding: 5px 12px;
border-radius: 5px;
.badge {
width: 30px;
font-size: .7em;
background-color: $yellow-200;
border-bottom: 1px solid $yellow-500;
border-right: 1px solid $yellow-500;
color: $yellow-900;
text-transform: uppercase;
font-weight: 700;
margin-right: 10px;
text-shadow: 1px 1px $yellow-100;
}
}
nav.detail-nav {
padding: 5px;
background-color: #FFF;
border: 1px solid $gray-300;
border-radius: 5px;
font-weight: 500;
a {
cursor: pointer;
.bi {
font-size: inherit;
color: inherit;
}
&:not(.active):hover {
background-color: rgba($blue-100, .25);
}
}
}
.detail-text {
padding: 12px;
background-color: #FAFAFA;
color: #666;
border: 1px solid #AAA;
margin-top: 12px;
border-radius: 5px;
.bi {
color: $blue;
}
> iframe {
width: 100%;
height: 50vh;
background-color: #FAFAFA;
overflow: auto;
border: none;
border-radius: 5px;
display: block;
}
}
.detail-action {
.bi {
color: inherit;
font-size: 16px;
}
}
}
</style>