feat(agenda): various agenda improvements and fixes (#4613)
* chore: update dependencies + add jsconfig * fix(agenda): handle localStorage being disabled * feat: agenda share modal * feat: agenda tour * feat: agenda share filters + picked sessions + fixes * test: fix agenda tests * test: add agenda share dialog test * test: remove agenda only flag
This commit is contained in:
parent
d08815d8da
commit
395f110df2
|
@ -34,7 +34,7 @@ indent_size = 2
|
|||
[dev/**.js]
|
||||
indent_size = 2
|
||||
|
||||
[{package.json,.eslintrc.js,.yarnrc.yml,vite.config.js,cypress.config.js}]
|
||||
[{package.json,.eslintrc.js,.yarnrc.yml,vite.config.js,jsconfig.json}]
|
||||
indent_size = 2
|
||||
|
||||
# Settings for cypress tests
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@percy-cli-upload-npm-1.11.0-329a9874d2-7b01f7f67a.zip
vendored
Normal file
BIN
.yarn/cache/@percy-cli-upload-npm-1.11.0-329a9874d2-7b01f7f67a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@percy-client-npm-1.11.0-054cbe67f8-2dcb47642a.zip
vendored
Normal file
BIN
.yarn/cache/@percy-client-npm-1.11.0-054cbe67f8-2dcb47642a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@percy-core-npm-1.11.0-6db118514e-e590983952.zip
vendored
Normal file
BIN
.yarn/cache/@percy-core-npm-1.11.0-6db118514e-e590983952.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vitejs-plugin-vue-npm-3.1.2-f3b2868971-1da84ccfc8.zip
vendored
Normal file
BIN
.yarn/cache/@vitejs-plugin-vue-npm-3.1.2-f3b2868971-1da84ccfc8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-core-npm-3.2.41-8f70d0e934-ff794351be.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-core-npm-3.2.41-8f70d0e934-ff794351be.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-dom-npm-3.2.41-1c0e991507-463f73d935.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-dom-npm-3.2.41-1c0e991507-463f73d935.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-sfc-npm-3.2.41-a5a9a4917f-0f13d9fa32.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-sfc-npm-3.2.41-a5a9a4917f-0f13d9fa32.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-ssr-npm-3.2.41-d33d233099-119913dee2.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-ssr-npm-3.2.41-d33d233099-119913dee2.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-reactivity-npm-3.2.41-facca3f9eb-3cac74db33.zip
vendored
Normal file
BIN
.yarn/cache/@vue-reactivity-npm-3.2.41-facca3f9eb-3cac74db33.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-reactivity-transform-npm-3.2.41-c0c4b830b1-f4a1d3ea62.zip
vendored
Normal file
BIN
.yarn/cache/@vue-reactivity-transform-npm-3.2.41-c0c4b830b1-f4a1d3ea62.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-runtime-core-npm-3.2.41-ac541c4be6-d7f81d0353.zip
vendored
Normal file
BIN
.yarn/cache/@vue-runtime-core-npm-3.2.41-ac541c4be6-d7f81d0353.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-runtime-dom-npm-3.2.41-abd55753cf-3bb4c586f5.zip
vendored
Normal file
BIN
.yarn/cache/@vue-runtime-dom-npm-3.2.41-abd55753cf-3bb4c586f5.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-server-renderer-npm-3.2.41-b23e1cfd6b-34ff395947.zip
vendored
Normal file
BIN
.yarn/cache/@vue-server-renderer-npm-3.2.41-b23e1cfd6b-34ff395947.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-shared-npm-3.2.41-ff2415965e-48f13e3eef.zip
vendored
Normal file
BIN
.yarn/cache/@vue-shared-npm-3.2.41-ff2415965e-48f13e3eef.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/caniuse-lite-npm-1.0.30001420-f322909669-dfa5027b2a.zip
vendored
Normal file
BIN
.yarn/cache/caniuse-lite-npm-1.0.30001420-f322909669-dfa5027b2a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-darwin-64-npm-0.15.11-0ccb211fdf-8.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-darwin-64-npm-0.15.11-0ccb211fdf-8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-darwin-arm64-npm-0.15.11-cbb0a8549f-8.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-darwin-arm64-npm-0.15.11-cbb0a8549f-8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-linux-64-npm-0.15.11-fd176c9400-8.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-linux-64-npm-0.15.11-fd176c9400-8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-linux-arm64-npm-0.15.11-eb05503e3f-8.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-linux-arm64-npm-0.15.11-eb05503e3f-8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-npm-0.15.11-352cc4ec35-afe5f2e6fb.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-npm-0.15.11-352cc4ec35-afe5f2e6fb.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-windows-64-npm-0.15.11-a6a42a35c8-8.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-windows-64-npm-0.15.11-a6a42a35c8-8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/esbuild-windows-arm64-npm-0.15.11-d36b5e4f06-8.zip
vendored
Normal file
BIN
.yarn/cache/esbuild-windows-arm64-npm-0.15.11-d36b5e4f06-8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/eslint-plugin-promise-npm-6.1.0-fbc1a09f9f-01c55f6c4d.zip
vendored
Normal file
BIN
.yarn/cache/eslint-plugin-promise-npm-6.1.0-fbc1a09f9f-01c55f6c4d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/html-validate-npm-7.6.0-c88dfc80a4-3e7ba99186.zip
vendored
Normal file
BIN
.yarn/cache/html-validate-npm-7.6.0-c88dfc80a4-3e7ba99186.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/moment-timezone-npm-0.5.38-6d3ab18886-ff7077de41.zip
vendored
Normal file
BIN
.yarn/cache/moment-timezone-npm-0.5.38-6d3ab18886-ff7077de41.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/pinia-npm-2.0.23-17bda5a8d2-004c76d80b.zip
vendored
Normal file
BIN
.yarn/cache/pinia-npm-2.0.23-17bda5a8d2-004c76d80b.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/rollup-npm-2.78.1-25ffe2a567-9034814383.zip
vendored
Normal file
BIN
.yarn/cache/rollup-npm-2.78.1-25ffe2a567-9034814383.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/shepherd.js-npm-10.0.1-64acc35968-be51f42734.zip
vendored
Normal file
BIN
.yarn/cache/shepherd.js-npm-10.0.1-64acc35968-be51f42734.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/smoothscroll-polyfill-npm-0.4.4-69b5bb4bf7-b99ff7d916.zip
vendored
Normal file
BIN
.yarn/cache/smoothscroll-polyfill-npm-0.4.4-69b5bb4bf7-b99ff7d916.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/vite-npm-3.1.8-6703f419ed-982696ad13.zip
vendored
Normal file
BIN
.yarn/cache/vite-npm-3.1.8-6703f419ed-982696ad13.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/vue-npm-3.2.40-ee1b0f06d2-fb5ca87c16.zip
vendored
BIN
.yarn/cache/vue-npm-3.2.40-ee1b0f06d2-fb5ca87c16.zip
vendored
Binary file not shown.
BIN
.yarn/cache/vue-npm-3.2.41-cb73e74f4c-5328bf14c6.zip
vendored
Normal file
BIN
.yarn/cache/vue-npm-3.2.41-cb73e74f4c-5328bf14c6.zip
vendored
Normal file
Binary file not shown.
|
@ -13,13 +13,28 @@
|
|||
|
||||
.agenda-topnav.my-3
|
||||
meeting-navigation
|
||||
n-button.d-none.d-sm-flex(
|
||||
quaternary
|
||||
@click='toggleSettings'
|
||||
)
|
||||
template(#icon)
|
||||
i.bi.bi-gear
|
||||
span Settings
|
||||
.agenda-topnav-right.d-none.d-md-flex
|
||||
n-button(
|
||||
quaternary
|
||||
@click='startTour'
|
||||
)
|
||||
template(#icon)
|
||||
i.bi.bi-question-square
|
||||
span Help
|
||||
n-button(
|
||||
quaternary
|
||||
@click='toggleShare'
|
||||
)
|
||||
template(#icon)
|
||||
i.bi.bi-share
|
||||
span Share
|
||||
n-button(
|
||||
quaternary
|
||||
@click='toggleSettings'
|
||||
)
|
||||
template(#icon)
|
||||
i.bi.bi-gear
|
||||
span Settings
|
||||
|
||||
.row
|
||||
.col
|
||||
|
@ -137,6 +152,7 @@
|
|||
agenda-quick-access
|
||||
|
||||
agenda-mobile-bar
|
||||
agenda-share-modal(v-model:shown='state.shareModalShown')
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
@ -159,10 +175,12 @@ import AgendaScheduleList from './AgendaScheduleList.vue'
|
|||
import AgendaScheduleCalendar from './AgendaScheduleCalendar.vue'
|
||||
import AgendaQuickAccess from './AgendaQuickAccess.vue'
|
||||
import AgendaSettings from './AgendaSettings.vue'
|
||||
import AgendaShareModal from './AgendaShareModal.vue'
|
||||
import AgendaMobileBar from './AgendaMobileBar.vue'
|
||||
import MeetingNavigation from './MeetingNavigation.vue'
|
||||
|
||||
import timezones from '../shared/timezones'
|
||||
import { initTour } from './tour'
|
||||
|
||||
import { useAgendaStore } from './store'
|
||||
import { useSiteStore } from '../shared/store'
|
||||
|
@ -187,6 +205,7 @@ const route = useRoute()
|
|||
|
||||
const state = reactive({
|
||||
searchText: '',
|
||||
shareModalShown: false
|
||||
})
|
||||
|
||||
// REFS
|
||||
|
@ -219,8 +238,19 @@ watch(() => agendaStore.meetingDays, () => {
|
|||
})
|
||||
|
||||
watch(() => agendaStore.isLoaded, () => {
|
||||
let resetQuery = false
|
||||
if (route.query.filters) {
|
||||
// Handle ?filters= parameter
|
||||
const keywords = route.query.filters.split(',').map(k => k.trim()).filter(k => !!k)
|
||||
if (keywords?.length > 0) {
|
||||
agendaStore.$patch({
|
||||
selectedCatSubs: keywords
|
||||
})
|
||||
}
|
||||
resetQuery = true
|
||||
}
|
||||
if (route.query.show) {
|
||||
// Handle legacy ?show= parameter
|
||||
// Handle ?show= parameter
|
||||
const keywords = route.query.show.split(',').map(k => k.trim()).filter(k => !!k)
|
||||
if (keywords?.length > 0) {
|
||||
const pickedIds = []
|
||||
|
@ -235,13 +265,23 @@ watch(() => agendaStore.isLoaded, () => {
|
|||
pickerModeView: true,
|
||||
pickedEvents: pickedIds
|
||||
})
|
||||
agendaStore.persistMeetingPreferences()
|
||||
}
|
||||
}
|
||||
resetQuery = true
|
||||
}
|
||||
if (route.query.pick) {
|
||||
// Handle legacy /personalize path (open picker mode)
|
||||
agendaStore.$patch({ pickerMode: true })
|
||||
resetQuery = true
|
||||
}
|
||||
if (route.query.tz) {
|
||||
// Handle tz param
|
||||
agendaStore.$patch({ timezone: route.query.tz })
|
||||
resetQuery = true
|
||||
}
|
||||
|
||||
if (resetQuery) {
|
||||
agendaStore.persistMeetingPreferences()
|
||||
router.replace({ query: null })
|
||||
}
|
||||
|
||||
|
@ -313,6 +353,18 @@ function toggleSettings () {
|
|||
})
|
||||
}
|
||||
|
||||
function toggleShare () {
|
||||
state.shareModalShown = !state.shareModalShown
|
||||
}
|
||||
|
||||
function startTour () {
|
||||
const tour = initTour({
|
||||
mobileMode: siteStore.viewport < 990,
|
||||
pickerMode: agendaStore.pickerMode
|
||||
})
|
||||
tour.start()
|
||||
}
|
||||
|
||||
// -> Go to current meeting if not provided
|
||||
function handleCurrentMeetingRedirect () {
|
||||
if (!route.params.meetingNumber && agendaStore.meeting.number) {
|
||||
|
@ -394,15 +446,6 @@ onMounted(() => {
|
|||
}
|
||||
})
|
||||
|
||||
// CREATED
|
||||
|
||||
// -> Handle loading tab directly based on URL
|
||||
if (window.location.pathname.indexOf('-utc') >= 0) {
|
||||
agendaStore.$patch({ timezone: 'UTC' })
|
||||
} else if (window.location.pathname.indexOf('personalize') >= 0) {
|
||||
// state.currentTab = 'personalize'
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -421,18 +464,25 @@ if (window.location.pathname.indexOf('-utc') >= 0) {
|
|||
&-topnav {
|
||||
position: relative;
|
||||
|
||||
> button {
|
||||
&-right {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
display: flex;
|
||||
|
||||
.bi {
|
||||
transition: transform 1s ease;
|
||||
button + button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> button:last-child {
|
||||
.bi {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.bi {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ n-drawer(v-model:show='state.isShown', placement='bottom', :height='state.drawer
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, unref, watch } from 'vue'
|
||||
import { nextTick, reactive, ref, unref, watch } from 'vue'
|
||||
import intersection from 'lodash/intersection'
|
||||
import difference from 'lodash/difference'
|
||||
import union from 'lodash/union'
|
||||
|
@ -113,8 +113,15 @@ function cancelFilter () {
|
|||
}
|
||||
|
||||
function saveFilter () {
|
||||
agendaStore.$patch({ selectedCatSubs: state.pendingSelection })
|
||||
state.isShown = false
|
||||
const applyLoadingMsg = message.create('Applying filters...', { type: 'loading', duration: 0 })
|
||||
setTimeout(() => {
|
||||
agendaStore.$patch({ selectedCatSubs: state.pendingSelection })
|
||||
agendaStore.persistMeetingPreferences()
|
||||
state.isShown = false
|
||||
nextTick(() => {
|
||||
applyLoadingMsg.destroy()
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function clearFilter () {
|
||||
|
|
173
client/agenda/AgendaShareModal.vue
Normal file
173
client/agenda/AgendaShareModal.vue
Normal file
|
@ -0,0 +1,173 @@
|
|||
<template lang="pug">
|
||||
n-modal(v-model:show='modalShown')
|
||||
n-card.agenda-share(
|
||||
:bordered='false'
|
||||
segmented
|
||||
role='dialog'
|
||||
aria-modal='true'
|
||||
)
|
||||
template(#header-extra)
|
||||
.agenda-share-header
|
||||
n-button.ms-4.agenda-share-close(
|
||||
ghost
|
||||
color='gray'
|
||||
strong
|
||||
@click='modalShown = false'
|
||||
)
|
||||
i.bi.bi-x
|
||||
template(#header)
|
||||
.agenda-share-header
|
||||
i.bi.bi-share
|
||||
span Share this view
|
||||
.agenda-share-content
|
||||
.text-muted.pb-2 Use the following URL for sharing the current view #[em (including any active filters)] with other users:
|
||||
n-input-group
|
||||
n-input(
|
||||
ref='filteredUrlIpt'
|
||||
size='large'
|
||||
readonly
|
||||
v-model:value='state.filteredUrl'
|
||||
)
|
||||
n-button(
|
||||
type='primary'
|
||||
primary
|
||||
strong
|
||||
size='large'
|
||||
@click='copyFilteredUrl'
|
||||
)
|
||||
template(#icon)
|
||||
i.bi.bi-clipboard-check.me-1
|
||||
span Copy
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { find } from 'lodash-es'
|
||||
import {
|
||||
NButton,
|
||||
NCard,
|
||||
NModal,
|
||||
NInputGroup,
|
||||
NInput,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
|
||||
import { useAgendaStore } from './store'
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
shown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// MESSAGE PROVIDER
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// STORES
|
||||
|
||||
const agendaStore = useAgendaStore()
|
||||
|
||||
// EMIT
|
||||
|
||||
const emit = defineEmits(['update:shown'])
|
||||
|
||||
// STATE
|
||||
|
||||
const state = reactive({
|
||||
isLoading: false,
|
||||
filteredUrl: window.location.href
|
||||
})
|
||||
const filteredUrlIpt = ref(null)
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const modalShown = computed({
|
||||
get () {
|
||||
return props.shown
|
||||
},
|
||||
set(value) {
|
||||
emit('update:shown', value)
|
||||
}
|
||||
})
|
||||
|
||||
// WATCHERS
|
||||
|
||||
watch(() => props.shown, (newValue) => {
|
||||
if (newValue) {
|
||||
generateUrl()
|
||||
}
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
function generateUrl () {
|
||||
const newUrl = new URL(window.location.href)
|
||||
const queryParams = []
|
||||
if (agendaStore.selectedCatSubs.length > 0 ) {
|
||||
queryParams.push(`filters=${agendaStore.selectedCatSubs.join(',')}`)
|
||||
}
|
||||
if (agendaStore.pickerMode && agendaStore.pickedEvents.length > 0 ) {
|
||||
const kwds = []
|
||||
for (const id of agendaStore.pickedEvents) {
|
||||
const session = find(agendaStore.scheduleAdjusted, ['id', id])
|
||||
if (session) {
|
||||
const suffix = session.sessionToken ? `-${session.sessionToken}` : ''
|
||||
kwds.push(`${session.acronym}${suffix}`)
|
||||
}
|
||||
}
|
||||
queryParams.push(`show=${kwds.join(',')}`)
|
||||
}
|
||||
newUrl.search = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''
|
||||
state.filteredUrl = newUrl.toString()
|
||||
}
|
||||
|
||||
async function copyFilteredUrl () {
|
||||
filteredUrlIpt.value?.select()
|
||||
|
||||
try {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(state.filteredUrl)
|
||||
} else {
|
||||
if (!document.execCommand('copy')) {
|
||||
throw new Error('Copy failed')
|
||||
}
|
||||
}
|
||||
message.success('URL copied to clipboard successfully.')
|
||||
} catch (err) {
|
||||
message.error('Failed to copy URL to clipboard.')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "bootstrap/scss/variables";
|
||||
|
||||
.agenda-share {
|
||||
width: 90vw;
|
||||
max-width: 1000px;
|
||||
|
||||
&-header {
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .bi {
|
||||
margin-right: 12px;
|
||||
font-size: 20px;
|
||||
color: $indigo;
|
||||
}
|
||||
}
|
||||
|
||||
&-close .bi {
|
||||
font-size: 20px;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,6 +4,7 @@ import uniqBy from 'lodash/uniqBy'
|
|||
import murmur from 'murmurhash-js/murmurhash3_gc'
|
||||
|
||||
import { useSiteStore } from '../shared/store'
|
||||
import { storageAvailable } from '../shared/feature-detect'
|
||||
|
||||
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']
|
||||
|
@ -147,7 +148,11 @@ export const useAgendaStore = defineStore('agenda', {
|
|||
const agendaData = await resp.json()
|
||||
|
||||
// -> Switch to meeting timezone
|
||||
this.timezone = window.localStorage.getItem(`agenda.${agendaData.meeting.number}.timezone`) || agendaData.meeting.timezone
|
||||
if (storageAvailable('localStorage')) {
|
||||
this.timezone = window.localStorage.getItem(`agenda.${agendaData.meeting.number}.timezone`) || agendaData.meeting.timezone
|
||||
} else {
|
||||
this.timezone = agendaData.meeting.timezone
|
||||
}
|
||||
|
||||
// -> Load meeting data
|
||||
this.categories = agendaData.categories
|
||||
|
@ -161,9 +166,17 @@ export const useAgendaStore = defineStore('agenda', {
|
|||
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`) || '[]')
|
||||
if (storageAvailable('localStorage')) {
|
||||
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.selectedCatSubs = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.filters`) || '[]')
|
||||
this.pickedEvents = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.pickedEvents`) || '[]')
|
||||
} else {
|
||||
this.infoNoteShown = true
|
||||
this.colorAssignments = {}
|
||||
this.selectedCatSubs = []
|
||||
this.pickedEvents = []
|
||||
}
|
||||
|
||||
this.isLoaded = true
|
||||
} catch (err) {
|
||||
|
@ -177,12 +190,15 @@ export const useAgendaStore = defineStore('agenda', {
|
|||
this.hideLoadingScreen()
|
||||
},
|
||||
persistMeetingPreferences () {
|
||||
if (!storageAvailable('localStorage')) { return }
|
||||
|
||||
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}.filters`, JSON.stringify(this.selectedCatSubs))
|
||||
window.localStorage.setItem(`agenda.${this.meeting.number}.pickedEvents`, JSON.stringify(this.pickedEvents))
|
||||
window.localStorage.setItem(`agenda.${this.meeting.number}.timezone`, this.timezone)
|
||||
},
|
||||
|
@ -221,10 +237,10 @@ export const useAgendaStore = defineStore('agenda', {
|
|||
}
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
enabled: storageAvailable('localStorage'),
|
||||
strategies: [
|
||||
{
|
||||
storage: localStorage,
|
||||
storage: storageAvailable('localStorage') ? localStorage : null,
|
||||
paths: [
|
||||
'areaIndicatorsShown',
|
||||
'bolderText',
|
||||
|
|
113
client/agenda/tour.js
Normal file
113
client/agenda/tour.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
import Shepherd from 'shepherd.js'
|
||||
import 'shepherd.js/dist/css/shepherd.css'
|
||||
|
||||
export function initTour ({ mobileMode, pickerMode }) {
|
||||
const tour = new Shepherd.Tour({
|
||||
useModalOverlay: true,
|
||||
defaultStepOptions: {
|
||||
classes: 'shepherd-theme-custom',
|
||||
scrollTo: false,
|
||||
modalOverlayOpeningPadding: 8,
|
||||
modalOverlayOpeningRadius: 4,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0,20]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
const defaultButtons = [
|
||||
{
|
||||
text: 'Exit',
|
||||
action: tour.cancel,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
|
||||
// STEPS
|
||||
|
||||
tour.addSteps([
|
||||
{
|
||||
title: 'Filter Areas + Groups',
|
||||
text: 'You can filter the list of sessions by areas or working groups you\'re interested in. The filters you select here also apply to the <strong>Calendar View</strong> and persist even if you come back to this page later.',
|
||||
attachTo: {
|
||||
element: mobileMode ? '.agenda-mobile-bar > button:first-child' : '#agenda-quickaccess-filterbyareagroups-btn',
|
||||
on: mobileMode ? 'top' : 'left'
|
||||
},
|
||||
buttons: defaultButtons
|
||||
},
|
||||
{
|
||||
title: 'Pick Sessions',
|
||||
text: 'Alternatively select <strong>individual sessions</strong> from the list to build your own schedule.',
|
||||
attachTo: {
|
||||
element: pickerMode ? '.agenda-quickaccess-btnrow' : '#agenda-quickaccess-picksessions-btn',
|
||||
on: 'left'
|
||||
},
|
||||
buttons: defaultButtons,
|
||||
showOn: () => !mobileMode
|
||||
},
|
||||
{
|
||||
title: 'Calendar View',
|
||||
text: 'View the current list of sessions in a <strong>calendar view</strong>, by week or by day. The filters you selected above also apply in this view.',
|
||||
attachTo: {
|
||||
element: mobileMode ? '.agenda-mobile-bar > button:nth-child(2)' : '#agenda-quickaccess-calview-btn',
|
||||
on: mobileMode ? 'top' : 'left'
|
||||
},
|
||||
buttons: defaultButtons
|
||||
},
|
||||
{
|
||||
title: 'Add to your calendar',
|
||||
text: 'Add the current list of sessions to your personal calendar application, in either <strong>webcal</strong> or <strong>ics</strong> format.',
|
||||
attachTo: {
|
||||
element: mobileMode ? '.agenda-mobile-bar > button:nth-child(3)' : '#agenda-quickaccess-addtocal-btn',
|
||||
on: mobileMode ? 'top' : 'left'
|
||||
},
|
||||
buttons: defaultButtons
|
||||
},
|
||||
{
|
||||
title: 'Search Events',
|
||||
text: 'Filter the list of sessions by searching for <strong>specific keywords</strong> in the title, location, acronym, notes or group name. Click the button again to close the search and discard its filtering.',
|
||||
attachTo: {
|
||||
element: '.agenda-table-search',
|
||||
on: 'top'
|
||||
},
|
||||
buttons: defaultButtons
|
||||
},
|
||||
{
|
||||
title: 'Assign Colors to Events',
|
||||
text: 'Assign colors to individual events to keep track of those you find interesting, wish to attend or define your own colors / descriptions from the <strong>Settings</strong> panel.',
|
||||
attachTo: {
|
||||
element: '.agenda-table-colorpicker',
|
||||
on: 'top'
|
||||
},
|
||||
buttons: defaultButtons
|
||||
},
|
||||
{
|
||||
title: 'Sessions',
|
||||
text: 'View the session materials by either clicking on its title or using the <strong>Show meeting materials</strong> button on the right. You can locate the room holding this event on the floor plan by clicking on the location name.',
|
||||
attachTo: {
|
||||
element: () => document.querySelector('.agenda-table-display-event'),
|
||||
on: 'top'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: 'Finish',
|
||||
action: tour.next
|
||||
}
|
||||
],
|
||||
modalOverlayOpeningPadding: 0,
|
||||
modalOverlayOpeningRadius: 2
|
||||
}
|
||||
])
|
||||
|
||||
return tour
|
||||
}
|
|
@ -29,6 +29,12 @@ export default createRouter({
|
|||
return { name: 'agenda' }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/meeting/:meetingNumber(\\d+)?/agenda-utc',
|
||||
redirect: to => {
|
||||
return { name: 'agenda', query: { ...to.query, tz: 'UTC' } }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/meeting/:meetingNumber(\\d+)?/agenda/personalize',
|
||||
redirect: to => {
|
||||
|
|
19
client/shared/feature-detect.js
Normal file
19
client/shared/feature-detect.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const cache = {}
|
||||
|
||||
export function storageAvailable(type) {
|
||||
if (Object.prototype.hasOwnProperty.call(cache, type)) {
|
||||
return cache[type]
|
||||
}
|
||||
try {
|
||||
let storage = window[type]
|
||||
const x = '__storage_test__'
|
||||
storage.setItem(x, x)
|
||||
storage.removeItem(x)
|
||||
cache[type] = true
|
||||
return true
|
||||
}
|
||||
catch (e) {
|
||||
cache[type] = false
|
||||
return false
|
||||
}
|
||||
}
|
21
jsconfig.json
Normal file
21
jsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".yarn",
|
||||
".vite"
|
||||
],
|
||||
"include": [
|
||||
"client/**/*",
|
||||
"playwright/**/*"
|
||||
],
|
||||
"vueCompilerOptions": {
|
||||
"target": 3,
|
||||
"plugins": [
|
||||
"@volar/vue-language-plugin-pug"
|
||||
]
|
||||
}
|
||||
}
|
29
package.json
29
package.json
|
@ -24,8 +24,8 @@
|
|||
"@popperjs/core": "2.11.6",
|
||||
"bootstrap": "5.2.2",
|
||||
"bootstrap-icons": "1.9.1",
|
||||
"browser-fs-access": "0.31.0",
|
||||
"caniuse-lite": "1.0.30001414",
|
||||
"browser-fs-access": "0.31.1",
|
||||
"caniuse-lite": "1.0.30001420",
|
||||
"d3": "7.6.1",
|
||||
"file-saver": "2.0.5",
|
||||
"highcharts": "10.2.1",
|
||||
|
@ -37,47 +37,48 @@
|
|||
"lodash-es": "4.17.21",
|
||||
"luxon": "3.0.4",
|
||||
"moment": "2.29.4",
|
||||
"moment-timezone": "0.5.37",
|
||||
"moment-timezone": "0.5.38",
|
||||
"ms": "2.1.3",
|
||||
"murmurhash-js": "1.0.0",
|
||||
"naive-ui": "2.33.3",
|
||||
"pinia": "2.0.22",
|
||||
"naive-ui": "2.33.5",
|
||||
"pinia": "2.0.23",
|
||||
"pinia-plugin-persist": "1.0.0",
|
||||
"select2": "4.1.0-rc.0",
|
||||
"select2-bootstrap-5-theme": "1.3.0",
|
||||
"send": "0.18.0",
|
||||
"shepherd.js": "10.0.1",
|
||||
"slugify": "1.6.5",
|
||||
"sortablejs": "1.15.0",
|
||||
"vue": "3.2.40",
|
||||
"vue": "3.2.41",
|
||||
"vue-router": "4.1.5",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "7.5.0",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@parcel/transformer-sass": "2.7.0",
|
||||
"@percy/cli": "1.10.4",
|
||||
"@percy/cli": "1.11.0",
|
||||
"@percy/cypress": "3.1.2",
|
||||
"@vitejs/plugin-vue": "2.3.4",
|
||||
"@vitejs/plugin-vue": "3.1.2",
|
||||
"@vue/test-utils": "2.1.0",
|
||||
"browserlist": "latest",
|
||||
"c8": "7.12.0",
|
||||
"cypress": "10.9.0",
|
||||
"cypress-real-events": "1.7.1",
|
||||
"eslint": "8.24.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-n": "15.3.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.0.1",
|
||||
"eslint-plugin-vue": "9.5.1",
|
||||
"html-validate": "7.5.0",
|
||||
"eslint-plugin-promise": "6.1.0",
|
||||
"eslint-plugin-vue": "9.6.0",
|
||||
"html-validate": "7.6.0",
|
||||
"jquery-migrate": "3.4.0",
|
||||
"parcel": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"sass": "1.55.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"vite": "2.9.15"
|
||||
"vite": "3.1.8"
|
||||
},
|
||||
"targets": {
|
||||
"ietf": {
|
||||
|
|
|
@ -81,9 +81,15 @@ test.describe('past - desktop', () => {
|
|||
await expect(navLocator.last()).toContainText('Plaintext')
|
||||
})
|
||||
|
||||
// SETTINGS BUTTON
|
||||
// RIGHT-SIDE BUTTONS
|
||||
|
||||
await expect(page.locator('.agenda .meeting-nav + button')).toContainText('Settings')
|
||||
await test.step('has the correct right side buttons', async () => {
|
||||
const btnsLocator = page.locator('.agenda .agenda-topnav-right > button')
|
||||
await expect(btnsLocator).toHaveCount(3)
|
||||
await expect(btnsLocator.first()).toContainText('Help')
|
||||
await expect(btnsLocator.nth(1)).toContainText('Share')
|
||||
await expect(btnsLocator.last()).toContainText('Settings')
|
||||
})
|
||||
})
|
||||
|
||||
test('agenda schedule list header', async ({ page }) => {
|
||||
|
@ -773,7 +779,7 @@ test.describe('past - desktop', () => {
|
|||
|
||||
test('agenda settings', async ({ page, browserName }) => {
|
||||
// Open dialog
|
||||
await page.locator('.meeting-nav + button').click()
|
||||
await page.locator('.agenda-topnav-right > button:last-child').click()
|
||||
await expect(page.locator('.agenda-settings')).toBeVisible()
|
||||
// Check header elements
|
||||
await expect(page.locator('.agenda-settings .n-drawer-header__main > span')).toContainText('Agenda Settings')
|
||||
|
@ -898,6 +904,22 @@ test.describe('past - desktop', () => {
|
|||
await expect(page.locator('.agenda-settings')).not.toBeVisible()
|
||||
})
|
||||
|
||||
// -> SHARE DIALOG
|
||||
|
||||
test('agenda share dialog', async ({ page }) => {
|
||||
// Open dialog
|
||||
await page.locator('.agenda-topnav-right > button:nth-child(2)').click()
|
||||
await expect(page.locator('.agenda-share')).toBeVisible()
|
||||
// Check header elements
|
||||
await expect(page.locator('.agenda-share .n-card-header__main > .agenda-share-header > .bi')).toBeVisible()
|
||||
await expect(page.locator('.agenda-share .n-card-header__main > .agenda-share-header > .bi + span')).toContainText('Share this view')
|
||||
// Check input URL
|
||||
await expect(page.locator('.agenda-share .agenda-share-content input[type=text]')).toHaveValue(`http://localhost:3000/meeting/${meetingData.meeting.number}/agenda`)
|
||||
// Clicking X should close the dialog
|
||||
await page.locator('.agenda-share .n-card-header__extra > .agenda-share-header > button').click()
|
||||
await expect(page.locator('.agenda-share')).not.toBeVisible()
|
||||
})
|
||||
|
||||
// -> ADD TO CALENDAR
|
||||
|
||||
test('agenda add to calendar', async ({ page }) => {
|
||||
|
@ -1012,7 +1034,7 @@ test.describe('past - desktop', () => {
|
|||
// Change color legend from settings dialog
|
||||
// ----------------------------------------
|
||||
// Open dialog
|
||||
await page.locator('.meeting-nav + button').click()
|
||||
await page.locator('.agenda-topnav-right > button:last-child').click()
|
||||
await expect(page.locator('.agenda-settings')).toBeVisible()
|
||||
// Toggle color legend switch
|
||||
await colorLgdSwitchLocator.click()
|
||||
|
@ -1051,7 +1073,7 @@ test.describe('past - desktop', () => {
|
|||
// No colored dots should appear
|
||||
await expect(page.locator('.agenda .agenda-table-display-event .agenda-table-colorindicator')).toHaveCount(0)
|
||||
// Clear all colors from Settings menu
|
||||
await page.locator('.meeting-nav + button').click()
|
||||
await page.locator('.agenda-topnav-right > button:last-child').click()
|
||||
await expect(page.locator('.agenda-settings')).toBeVisible()
|
||||
await page.locator('.agenda-settings .agenda-settings-actions > button').first().click()
|
||||
await page.locator('.n-dropdown-option:has-text("Clear Color")').click()
|
||||
|
@ -1295,7 +1317,7 @@ test.describe('live - desktop', () => {
|
|||
|
||||
test('live red line toggle', async ({ page }) => {
|
||||
// Open settings dialog
|
||||
await page.locator('.meeting-nav + button').click()
|
||||
await page.locator('.agenda-topnav-right > button:last-child').click()
|
||||
await expect(page.locator('.agenda-settings')).toBeVisible()
|
||||
// Toggle red line switch
|
||||
const redlineSwitchLocator = page.locator('#agenda-settings-tgl-redline div[role=switch]')
|
||||
|
|
Loading…
Reference in a new issue