Skip to content

Commit aea533e

Browse files
authored
fix: add missing slides + minutes tabs to session materials dialog ietf-tools#4274 (ietf-tools#4275)
* fix: handle session dropdown link on mobile view * feat: agenda session materials API endpoint * feat: agenda-neue - add slides + minutes tabs to session materials dialog * fix: remove commented line leftover
1 parent 4e649b9 commit aea533e

5 files changed

Lines changed: 181 additions & 7 deletions

File tree

client/agenda/AgendaDetailsModal.vue

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,64 @@ n-modal(v-model:show='modalShown')
7070
span.badge {{eventDetails.locationShort}}
7171
span {{eventDetails.locationName}}
7272
span {{eventDetails.room}}
73-
.detail-text(v-if='eventDetails.materialsUrl')
74-
iframe(
75-
:src='eventDetails.materialsUrl'
73+
nav.detail-nav.nav.nav-pills.nav-justified.mt-3
74+
a.nav-link(
75+
:class='{ active: state.tab === `agenda` }'
76+
@click='state.tab = `agenda`'
77+
)
78+
i.bi.bi-list-columns-reverse.me-2
79+
span Agenda
80+
a.nav-link(
81+
:class='{ active: state.tab === `slides` }'
82+
@click='state.tab = `slides`'
83+
)
84+
i.bi.bi-easel.me-2
85+
span Slides
86+
a.nav-link(
87+
:class='{ active: state.tab === `minutes` }'
88+
@click='state.tab = `minutes`'
7689
)
90+
i.bi.bi-journal-text.me-2
91+
span Minutes
92+
.detail-text(v-if='eventDetails.materialsUrl')
93+
template(v-if='state.tab === `agenda`')
94+
iframe(
95+
:src='eventDetails.materialsUrl'
96+
)
97+
template(v-else-if='state.tab === `slides`')
98+
.text-center(v-if='state.isLoading')
99+
n-spin(description='Loading slides...')
100+
.text-center.p-3(v-else-if='!state.materials || !state.materials.slides || state.materials.slides.length < 1')
101+
span No slides submitted for this session.
102+
.list-group(v-else)
103+
a.list-group-item(
104+
v-for='slide of state.materials.slides'
105+
:key='slide.id'
106+
:href='slide.url'
107+
target='_blank'
108+
)
109+
i.bi.me-2(:class='`bi-filetype-` + slide.ext')
110+
span {{slide.title}}
111+
template(v-else)
112+
.text-center(v-if='state.isLoading')
113+
n-spin(description='Loading minutes...')
114+
.text-center.p-3(v-else-if='!state.materials || !state.materials.minutes')
115+
span No minutes submitted for this session.
116+
iframe(
117+
v-else
118+
:src='state.materials.minutes.url'
119+
)
77120
</template>
78121

79122
<script setup>
80-
import { computed } from 'vue'
123+
import { computed, reactive, watch } from 'vue'
81124
import {
82125
NButton,
83126
NCard,
84127
NModal,
85-
NPopover
128+
NPopover,
129+
NSpin,
130+
useMessage
86131
} from 'naive-ui'
87132
88133
import { useAgendaStore } from './store'
@@ -101,6 +146,10 @@ const props = defineProps({
101146
}
102147
})
103148
149+
// MESSAGE PROVIDER
150+
151+
const message = useMessage()
152+
104153
// STORES
105154
106155
const agendaStore = useAgendaStore()
@@ -109,6 +158,14 @@ const agendaStore = useAgendaStore()
109158
110159
const emit = defineEmits(['update:shown'])
111160
161+
// STATE
162+
163+
const state = reactive({
164+
tab: 'agenda',
165+
isLoading: false,
166+
materials: {}
167+
})
168+
112169
// COMPUTED
113170
114171
const eventDetails = computed(() => {
@@ -142,6 +199,38 @@ const modalShown = computed({
142199
}
143200
})
144201
202+
// WATCHERS
203+
204+
watch(() => props.shown, (newValue) => {
205+
if (newValue) {
206+
state.materials = {}
207+
state.tab = 'agenda'
208+
if (props.event.flags.showAgenda) {
209+
fetchSessionMaterials()
210+
}
211+
}
212+
})
213+
214+
// METHODS
215+
216+
async function fetchSessionMaterials () {
217+
if (!props.event) { return null }
218+
219+
state.isLoading = true
220+
221+
try {
222+
const resp = await fetch(`/api/meeting/session/${props.event.sessionId}/materials`, { credentials: 'omit' })
223+
if (!resp.ok) {
224+
throw new Error(resp.statusText)
225+
}
226+
state.materials = await resp.json()
227+
} catch (err) {
228+
console.warn(err)
229+
message.error('Failed to fetch session materials.')
230+
}
231+
state.isLoading = false
232+
}
233+
145234
</script>
146235
147236
<style lang="scss">
@@ -209,6 +298,27 @@ const modalShown = computed({
209298
}
210299
}
211300
301+
nav.detail-nav {
302+
padding: 5px;
303+
background-color: #FFF;
304+
border: 1px solid $gray-300;
305+
border-radius: 5px;
306+
font-weight: 500;
307+
308+
a {
309+
cursor: pointer;
310+
311+
.bi {
312+
font-size: inherit;
313+
color: inherit;
314+
}
315+
316+
&:not(.active):hover {
317+
background-color: rgba($blue-100, .25);
318+
}
319+
}
320+
}
321+
212322
.detail-text {
213323
padding: 12px;
214324
background-color: #FAFAFA;
@@ -217,6 +327,10 @@ const modalShown = computed({
217327
margin-top: 12px;
218328
border-radius: 5px;
219329
330+
.bi {
331+
color: $blue;
332+
}
333+
220334
> iframe {
221335
width: 100%;
222336
height: 50vh;

client/agenda/AgendaScheduleList.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,12 @@
107107
span.badge.is-rescheduled(v-else-if='!isMobile && item.status === `resched`') Rescheduled
108108
.agenda-table-cell-links-buttons(v-else-if='agendaStore.viewport < 1200 && item.links && item.links.length > 0')
109109
n-dropdown(
110+
v-if='!agendaStore.colorPickerVisible'
110111
trigger='click'
111112
:options='item.links'
112113
key-field='id'
113114
:render-icon='renderLinkIcon'
114-
v-if='!agendaStore.colorPickerVisible'
115+
@select='goToSessionLink'
115116
)
116117
n-button(size='tiny')
117118
i.bi.bi-three-dots
@@ -189,13 +190,18 @@ import {
189190
NCheckbox,
190191
NCheckboxGroup,
191192
NDropdown,
192-
NPopover
193+
NPopover,
194+
useMessage
193195
} from 'naive-ui'
194196
195197
import AgendaDetailsModal from './AgendaDetailsModal.vue'
196198
197199
import { useAgendaStore } from './store'
198200
201+
// MESSAGE PROVIDER
202+
203+
const message = useMessage()
204+
199205
// STORES
200206
201207
const agendaStore = useAgendaStore()
@@ -491,6 +497,14 @@ function toggleColorPicker () {
491497
})
492498
}
493499
500+
function goToSessionLink (lnkKey, lnk) {
501+
if (lnk.href) {
502+
window.location.assign(lnk.href)
503+
} else {
504+
message.error('Missing link for this dropdown item.')
505+
}
506+
}
507+
494508
function showMaterials (eventId) {
495509
state.eventDetails = find(agendaStore.scheduleAdjusted, ['id', eventId])
496510
state.showEventDetails = true

ietf/api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url),
3030
# Meeting agenda + floorplan data
3131
url(r'^meeting/(?P<num>[A-Za-z0-9._+-]+)/agenda-data$', meeting_views.api_get_agenda_data),
32+
# Meeting session materials
33+
url(r'^meeting/session/(?P<session_id>[A-Za-z0-9._+-]+)/materials$', meeting_views.api_get_session_materials),
3234
# Let Meetecho trigger recording imports
3335
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
3436
# Let MeetEcho upload bluesheets

ietf/meeting/tests_views.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def test_meeting_agenda(self):
170170
self.assertEqual(r.status_code, 200)
171171

172172
# Agenda API tests
173+
# -> Meeting data
173174
r = self.client.get(urlreverse("ietf.meeting.views.api_get_agenda_data", kwargs=dict(num=meeting.number)))
174175
self.assertEqual(r.status_code, 200)
175176
rjson = json.loads(r.content.decode("utf8"))
@@ -193,6 +194,24 @@ def test_meeting_agenda(self):
193194
"floors": []
194195
}
195196
)
197+
# -> Session Materials
198+
r = self.client.get(urlreverse("ietf.meeting.views.api_get_session_materials", kwargs=dict(session_id=session.id)))
199+
self.assertEqual(r.status_code, 200)
200+
rjson = json.loads(r.content.decode("utf8"))
201+
minutes = session.minutes()
202+
self.assertJSONEqual(
203+
r.content.decode("utf8"),
204+
{
205+
"url": session.agenda().get_href(),
206+
"slides": rjson.get("slides"), # Just expect the value to exist
207+
"minutes": {
208+
"id": minutes.id,
209+
"title": minutes.title,
210+
"url": minutes.get_href(),
211+
"ext": minutes.file_extension()
212+
} if minutes is not None else None
213+
}
214+
)
196215

197216
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number,utc='-utc')))
198217
self.assertEqual(r.status_code, 200)

ietf/meeting/views.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,9 +1639,26 @@ def api_get_agenda_data (request, num=None):
16391639
"floors": list(map(agenda_extract_floorplan, floors))
16401640
})
16411641

1642+
def api_get_session_materials (request, session_id=None):
1643+
session = get_object_or_404(Session,pk=session_id)
1644+
1645+
minutes = session.minutes()
1646+
1647+
return JsonResponse({
1648+
"url": session.agenda().get_href(),
1649+
"slides": list(map(agenda_extract_slide, session.slides())),
1650+
"minutes": {
1651+
"id": minutes.id,
1652+
"title": minutes.title,
1653+
"url": minutes.get_href(),
1654+
"ext": minutes.file_extension()
1655+
} if minutes is not None else None
1656+
})
1657+
16421658
def agenda_extract_schedule (item):
16431659
return {
16441660
"id": item.id,
1661+
"sessionId": item.session.id,
16451662
"room": item.room_name,
16461663
"location": {
16471664
"short": item.timeslot.location.floorplan.short,
@@ -1727,6 +1744,14 @@ def agenda_extract_recording (item):
17271744
"url": item.external_url
17281745
}
17291746

1747+
def agenda_extract_slide (item):
1748+
return {
1749+
"id": item.id,
1750+
"title": item.title,
1751+
"url": item.get_versionless_href(),
1752+
"ext": item.file_extension()
1753+
}
1754+
17301755
def agenda_csv(schedule, filtered_assignments):
17311756
response = HttpResponse(content_type="text/csv; charset=%s"%settings.DEFAULT_CHARSET)
17321757
writer = csv.writer(response, delimiter=str(','), quoting=csv.QUOTE_ALL)

0 commit comments

Comments
 (0)