datatracker/client/agenda/store.js
Nicolas Giard 143877ec3d
test: Use Playwright + agenda start/end dates fix (#4471)
* 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
2022-09-20 13:33:22 -05:00

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
}