* test: agenda-neue - separate timezone controls assertions to allow retries * test: agenda-neue - use dom query selectors instead of first() / eq() * test: agenda-neue - playwright * test: fix playwright setup for ci * test: playwright - remove safari + fix timezone local * test: upload playwright report * test: playwright - fix trace upload * test * test: playwright - agenda search * test: fix startdate timezone * test: playwright - agenda table events * test: playwright - remove only filter * test: remove exit early flag * test: allow longer tests * test: agenda materials dialog * test: agenda filter by area/group * test: agenda calendar view * test: agenda settings * test: jump to day * test: fix agenda jump to day timezone parse * test: increase test timeout * test: remove fail fast * test: test sharding + increase delay * test: fixes * test: use macos image * test: fixes * test: agenda color assign + future + live meeting tests * test: agenda mobile tests * test: remainder of tests for playwright + optimizations * test: red line intersection accept close value * test: add delay for agenda search tests * chore: cleanup old tests + adapt build workflow * ci: fix build workflow * ci: fix build workflow order * fix: point to playwright floor plan images + readme
274 lines
9.3 KiB
JavaScript
274 lines
9.3 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { DateTime } from 'luxon'
|
|
import uniqBy from 'lodash/uniqBy'
|
|
import murmur from 'murmurhash-js/murmurhash3_gc'
|
|
|
|
const urlRe = /http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/
|
|
const conferenceDomains = ['webex.com', 'zoom.us', 'jitsi.org', 'meetecho.com', 'gather.town']
|
|
|
|
export const useAgendaStore = defineStore('agenda', {
|
|
state: () => ({
|
|
areaIndicatorsShown: true,
|
|
bolderText: false,
|
|
debugTools: false,
|
|
calendarShown: false,
|
|
categories: [],
|
|
colorLegendShown: true,
|
|
colorPickerVisible: false,
|
|
colors: [
|
|
{ hex: '#0d6efd', tag: 'Interesting' },
|
|
{ hex: '#6f42c1', tag: 'Might Attend' },
|
|
{ hex: '#d63384', tag: 'Important' },
|
|
{ hex: '#ffc107', tag: 'Food' },
|
|
{ hex: '#20c997', tag: 'Attended' }
|
|
],
|
|
colorAssignments: {},
|
|
criticalError: null,
|
|
currentTab: 'agenda',
|
|
dayIntersectId: '',
|
|
defaultCalendarView: 'week',
|
|
eventIconsShown: true,
|
|
filterShown: false,
|
|
floorIndicatorsShown: true,
|
|
floors: [],
|
|
infoNoteHash: '',
|
|
infoNoteShown: true,
|
|
isCurrentMeeting: false,
|
|
isLoaded: false,
|
|
isMobile: /Mobi/i.test(navigator.userAgent),
|
|
listDayCollapse: false,
|
|
meeting: {},
|
|
nowDebugDiff: null,
|
|
pickerMode: false,
|
|
pickerModeView: false,
|
|
pickedEvents: [],
|
|
redhandShown: true,
|
|
schedule: [],
|
|
searchText: '',
|
|
searchVisible: false,
|
|
selectedCatSubs: [],
|
|
settingsShown: false,
|
|
timezone: DateTime.local().zoneName,
|
|
useHedgeDoc: false,
|
|
viewport: Math.round(window.innerWidth),
|
|
visibleDays: []
|
|
}),
|
|
getters: {
|
|
isTimezoneLocal (state) {
|
|
return state.timezone === DateTime.local().zoneName
|
|
},
|
|
isTimezoneMeeting (state) {
|
|
return state.timezone === state.meeting.timezone
|
|
},
|
|
scheduleAdjusted (state) {
|
|
return state.schedule.filter(s => {
|
|
// -> Apply category filters
|
|
if (state.selectedCatSubs.length > 0 && !s.filterKeywords.some(k => state.selectedCatSubs.includes(k))) {
|
|
return false
|
|
}
|
|
|
|
// -> Don't show events of type lead
|
|
if (s.type === 'lead') { return false }
|
|
|
|
// -> Filter individual events if picker mode active
|
|
if (state.pickerMode && state.pickerModeView && !state.pickedEvents.includes(s.id)) {
|
|
return false
|
|
}
|
|
|
|
// -> Filter by search text if present
|
|
if (state.searchVisible && state.searchText) {
|
|
const searchStr = `${s.name} ${s.groupName} ${s.acronym} ${s.room} ${s.note}`
|
|
if (searchStr.toLowerCase().indexOf(state.searchText) < 0) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}).map(s => {
|
|
// -> Adjust times to selected timezone
|
|
const eventStartDate = DateTime.fromISO(s.startDateTime, { zone: state.meeting.timezone }).setZone(state.timezone)
|
|
const eventEndDate = eventStartDate.plus({ seconds: s.duration })
|
|
|
|
// -> Find remote call-in URL
|
|
let remoteCallInUrl = null
|
|
if (s.note) {
|
|
remoteCallInUrl = findFirstConferenceUrl(s.note)
|
|
}
|
|
if (!remoteCallInUrl && s.remoteInstructions) {
|
|
remoteCallInUrl = findFirstConferenceUrl(s.remoteInstructions)
|
|
}
|
|
if (!remoteCallInUrl && s.links.webex) {
|
|
remoteCallInUrl = s.links.webex
|
|
}
|
|
|
|
return {
|
|
...s,
|
|
adjustedStart: eventStartDate,
|
|
adjustedEnd: eventEndDate,
|
|
adjustedStartDate: eventStartDate.toISODate(),
|
|
adjustedStartDateTime: eventStartDate.toISO(),
|
|
adjustedEndDateTime: eventEndDate.toISO(),
|
|
links: {
|
|
...s.links,
|
|
videoStream: formatLinkUrl(s.links.videoStream, s, state.meeting.number),
|
|
onsiteTool: formatLinkUrl(s.links.onsiteTool, s, state.meeting.number),
|
|
audioStream: formatLinkUrl(s.links.audioStream, s, state.meeting.number),
|
|
remoteCallIn: remoteCallInUrl
|
|
},
|
|
sessionKeyword: s.sessionToken ? `${s.groupAcronym}-${s.sessionToken}` : s.groupAcronym
|
|
}
|
|
})
|
|
},
|
|
meetingDays () {
|
|
return uniqBy(this.scheduleAdjusted, 'adjustedStartDate').sort().map(s => ({
|
|
slug: s.id.toString(),
|
|
ts: s.adjustedStartDate,
|
|
label: this.viewport < 1350 ? DateTime.fromISO(s.adjustedStartDate).toFormat('ccc LLL d') : DateTime.fromISO(s.adjustedStartDate).toLocaleString(DateTime.DATE_HUGE)
|
|
}))
|
|
},
|
|
isMeetingLive (state) {
|
|
const current = (state.nowDebugDiff ? DateTime.local().minus(state.nowDebugDiff) : DateTime.local()).setZone(state.timezone)
|
|
const isAfterStart = this.scheduleAdjusted.some(s => s.adjustedStart < current)
|
|
const isBeforeEnd = this.scheduleAdjusted.some(s => s.adjustedEnd > current)
|
|
return isAfterStart && isBeforeEnd
|
|
}
|
|
},
|
|
actions: {
|
|
async fetch (meetingNumber) {
|
|
try {
|
|
if (!meetingNumber) {
|
|
const meetingData = JSON.parse(document.getElementById('meeting-data').textContent)
|
|
meetingNumber = meetingData.meetingNumber
|
|
}
|
|
|
|
const resp = await fetch(`/api/meeting/${meetingNumber}/agenda-data`, { credentials: 'omit' })
|
|
if (!resp.ok) {
|
|
throw new Error(resp.statusText)
|
|
}
|
|
const agendaData = await resp.json()
|
|
|
|
// -> Switch to meeting timezone
|
|
this.timezone = window.localStorage.getItem(`agenda.${agendaData.meeting.number}.timezone`) || agendaData.meeting.timezone
|
|
|
|
// -> Load meeting data
|
|
this.categories = agendaData.categories
|
|
this.floors = agendaData.floors
|
|
this.isCurrentMeeting = agendaData.isCurrentMeeting
|
|
this.meeting = agendaData.meeting
|
|
this.schedule = agendaData.schedule
|
|
this.useHedgeDoc = agendaData.useHedgeDoc
|
|
|
|
// -> Compute current info note hash
|
|
this.infoNoteHash = murmur(agendaData.meeting.infoNote, 0).toString()
|
|
|
|
// -> Load meeting-specific preferences
|
|
this.infoNoteShown = !(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.hideInfo`) === this.infoNoteHash)
|
|
this.colorAssignments = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.colorAssignments`) || '{}')
|
|
this.pickedEvents = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.pickedEvents`) || '[]')
|
|
|
|
this.isLoaded = true
|
|
} catch (err) {
|
|
console.error(err)
|
|
this.criticalError = `Failed to load this meeting: ${err.message}`
|
|
}
|
|
|
|
this.hideLoadingScreen()
|
|
},
|
|
persistMeetingPreferences () {
|
|
if (this.infoNoteShown) {
|
|
window.localStorage.removeItem(`agenda.${this.meeting.number}.hideInfo`)
|
|
} else {
|
|
window.localStorage.setItem(`agenda.${this.meeting.number}.hideInfo`, this.infoNoteHash)
|
|
}
|
|
window.localStorage.setItem(`agenda.${this.meeting.number}.colorAssignments`, JSON.stringify(this.colorAssignments))
|
|
window.localStorage.setItem(`agenda.${this.meeting.number}.pickedEvents`, JSON.stringify(this.pickedEvents))
|
|
window.localStorage.setItem(`agenda.${this.meeting.number}.timezone`, this.timezone)
|
|
},
|
|
findCurrentEventId () {
|
|
const current = (this.nowDebugDiff ? DateTime.local().minus(this.nowDebugDiff) : DateTime.local()).setZone(this.timezone)
|
|
|
|
// -> Find last event before current time
|
|
let lastEvent = {}
|
|
for(const sh of this.scheduleAdjusted) {
|
|
if (sh.adjustedStart <= current && sh.adjustedEnd > current) {
|
|
// -> Use the first event of multiple events having identical times
|
|
if (lastEvent.start === sh.adjustedStart.toMillis()) {
|
|
continue
|
|
} else {
|
|
lastEvent = {
|
|
id: sh.id,
|
|
start: sh.adjustedStart.toMillis(),
|
|
end: sh.adjustedEnd.toMillis()
|
|
}
|
|
}
|
|
}
|
|
// -> Skip future events
|
|
if (sh.adjustedStart > current) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return lastEvent.id || null
|
|
},
|
|
hideLoadingScreen () {
|
|
// -> Hide loading screen
|
|
const loadingRef = document.querySelector('#app-loading')
|
|
if (loadingRef) {
|
|
loadingRef.remove()
|
|
}
|
|
}
|
|
},
|
|
persist: {
|
|
enabled: true,
|
|
strategies: [
|
|
{
|
|
storage: localStorage,
|
|
paths: [
|
|
'areaIndicatorsShown',
|
|
'bolderText',
|
|
'colorLegendShown',
|
|
'colors',
|
|
'defaultCalendarView',
|
|
'eventIconsShown',
|
|
'floorIndicatorsShown',
|
|
'listDayCollapse',
|
|
'redhandShown'
|
|
]
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Format URL by replacing inline variables
|
|
*
|
|
* @param {String} url
|
|
* @param {Object} session
|
|
* @param {String} meetingNumber
|
|
* @returns Formatted URL
|
|
*/
|
|
function formatLinkUrl (url, session, meetingNumber) {
|
|
return url ? url.replace('{meeting.number}', meetingNumber)
|
|
.replace('{group.acronym}', session.groupAcronym)
|
|
.replace('{short}', session.short)
|
|
.replace('{order_number}', session.orderInMeeting) : url
|
|
}
|
|
|
|
/**
|
|
* Find the first URL in text matching a conference domain
|
|
*
|
|
* @param {String} txt
|
|
* @returns First URL found
|
|
*/
|
|
function findFirstConferenceUrl (txt) {
|
|
try {
|
|
const fUrl = txt.match(urlRe)
|
|
if (fUrl && fUrl[0].length > 0) {
|
|
const pUrl = new URL(fUrl[0])
|
|
if (conferenceDomains.some(d => pUrl.hostname.endsWith(d))) {
|
|
return fUrl[0]
|
|
}
|
|
}
|
|
} catch (err) { }
|
|
return null
|
|
}
|