From 36a11ec2fdb62ade0ca32f0cb7e7c2b1179bb259 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Tue, 4 Aug 2026 17:03:47 +0200 Subject: [PATCH] WIP: Adding custom tempus event loader --- public/js/components/Calendar/Tempus.js | 6 +- public/js/components/Tempus/Tempus.js | 1 + .../composables/Tempus/TempusEventLoader.js | 233 ++++++++++++++++++ 3 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 public/js/composables/Tempus/TempusEventLoader.js diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 7f69e9e9a..7f191569a 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -1,6 +1,6 @@ import FhcCalendar from "./Base.js"; -import { useEventLoader } from '../../composables/EventLoader.js'; +import { useEventLoader } from '../../composables/Tempus/TempusEventLoader.js'; import ModeWeek from './Mode/Week.js'; import ModeMonth from './Mode/Month.js'; @@ -171,14 +171,10 @@ export default { }, setup(props, context) { const rangeInterval = Vue.ref(null); - const preserveRequestedRange = Vue.computed(() => { - return ['week', 'month'].includes(props.mode?.toLowerCase()); - }); const { events, lv, reset } = useEventLoader( rangeInterval, props.getPromiseFunc, - preserveRequestedRange, () => props.cacheMultiplier ); diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index f75b4ea7a..168e89058 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -1301,6 +1301,7 @@ export default { :mode="currentMode" :parkedEvents="parkedKeys" :visible-lecturers="visibleLecturerUids" + :cacheMultiplier="currentMode === 'week' ? 0 : 0" @drop="dropHandler" @resize="resizeHandler" @update:date="handleChangeDate" diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js new file mode 100644 index 000000000..75d87985c --- /dev/null +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -0,0 +1,233 @@ +export function useEventLoader( + rangeInterval, + getPromiseFunc, + cacheMultiplier = 1, +) { + let allEvents = Vue.ref([]); + const events = []; + + const lv = Vue.ref(null); + + let newlyLoadedEvents = []; + + let cachedEventsStartTimestamp = null; + let cachedEventsEndTimestamp = null; + + let allowedCacheStartTimestamp = null; + let allowedCacheEndTimestamp = null; + + let currentlyDisplayedDateRange; + + const reload = () => { + if ( + currentlyDisplayedDateRange && + currentlyDisplayedDateRange?.start?.ts === + rangeInterval?.value?.start?.ts && + currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts + ) { + console.log( + "cached range is the same as requested range, skipping reload", + ); + return; + } + + currentlyDisplayedDateRange = Vue.toValue(rangeInterval); + + if ( + !currentlyDisplayedDateRange?.start || + !currentlyDisplayedDateRange?.end + ) + return; + + if (!(currentlyDisplayedDateRange instanceof luxon.Interval)) return; + + if ( + cachedEventsStartTimestamp === currentlyDisplayedDateRange.start.ts && + cachedEventsEndTimestamp === currentlyDisplayedDateRange.end.ts + ) { + console.log( + "cached range is the same as requested range, skipping reload", + ); + return; + } + + const promises = requestEvents( + currentlyDisplayedDateRange.start.ts, + currentlyDisplayedDateRange.end.ts, + ); + + ensureCacheRangeIsAllowedRange( + allowedCacheStartTimestamp, + allowedCacheEndTimestamp, + ); + + if (promises.length === 0) { + allEvents.value = removeVisualForEventsLoading( + ensureEventsAreInValidCacheRange(allEvents.value), + ); + + return; + } + + addVisualForEventsLoading(); + + Promise.allSettled(promises).then((results) => { + let newlyLoadedEvents = []; + + results.forEach((res) => { + if ( + !(res.status === "fulfilled" && res.value.meta.status === "success") + ) + return; + if (res.value.meta.lv) lv.value = res.value.meta.lv; + newlyLoadedEvents = res.value.data; + }); + + let tempAllEvents = Array.from( + new Map( + [...allEvents.value, ...newlyLoadedEvents].map((event) => [ + event.eindeutige_gruppen_id, + event, + ]), + ).values(), + ); + + allEvents.value = removeVisualForEventsLoading( + ensureEventsAreInValidCacheRange(tempAllEvents), + ); + }); + }; + + Vue.watchEffect(reload); + + const reset = () => { + reload(); + }; + + const requestEvents = (startTimestamp, endTimestamp) => { + let result = []; + + if (!startTimestamp || !endTimestamp) return result; + if (startTimestamp >= endTimestamp) return result; + + let modifiedRequestStartTimestamp = startTimestamp; + let modifiedRequestEndTimestamp = endTimestamp; + + if (cachedEventsStartTimestamp && cachedEventsEndTimestamp) { + if ( + startTimestamp < cachedEventsStartTimestamp && + endTimestamp > cachedEventsEndTimestamp + ) { + modifiedRequestStartTimestamp = startTimestamp; + modifiedRequestEndTimestamp = endTimestamp; + cachedEventsStartTimestamp = startTimestamp; + cachedEventsEndTimestamp = endTimestamp; + } else if ( + startTimestamp < cachedEventsStartTimestamp && + endTimestamp <= cachedEventsEndTimestamp + ) { + modifiedRequestStartTimestamp = startTimestamp; + modifiedRequestEndTimestamp = cachedEventsStartTimestamp; + cachedEventsStartTimestamp = startTimestamp; + } else if ( + startTimestamp >= cachedEventsStartTimestamp && + endTimestamp > cachedEventsEndTimestamp + ) { + modifiedRequestStartTimestamp = cachedEventsEndTimestamp; + modifiedRequestEndTimestamp = endTimestamp; + cachedEventsEndTimestamp = endTimestamp; + } else { + return result; + } + } else { + cachedEventsStartTimestamp = modifiedRequestStartTimestamp; + cachedEventsEndTimestamp = modifiedRequestEndTimestamp; + } + + const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); + const cachePadding = Math.round( + currentlyDisplayedDateRange.length() * cacheSize, + ); + + allowedCacheStartTimestamp = startTimestamp - cachePadding; + allowedCacheEndTimestamp = endTimestamp + cachePadding; + + return mergePromiseElements( + getPromiseFunc( + getLuxonDateFromMillis(modifiedRequestStartTimestamp), + getLuxonDateFromMillis(modifiedRequestEndTimestamp), + ), + result, + ); + }; + + const addVisualForEventsLoading = (startTimestamp, endTimestamp) => { + allEvents.value.push({ + loading_id: 1, + type: "loading", + isostart: getISODateFromTimestamp(currentlyDisplayedDateRange.start.ts), + isoend: getISODateFromTimestamp(currentlyDisplayedDateRange.end.ts), + }); + }; + + const removeVisualForEventsLoading = (events) => { + return events.filter((event) => event.type !== "loading"); + }; + + const ensureEventsAreInValidCacheRange = (events) => { + events = events.filter((event) => { + const start = getTimestampFromISODate(event.isostart); + const end = getTimestampFromISODate(event.isoend); + return ( + allowedCacheStartTimestamp <= start && allowedCacheEndTimestamp >= end + ); + }); + + return events; + }; + + const ensureCacheRangeIsAllowedRange = ( + allowedCacheStartTimestamp, + allowedCacheEndTimestamp, + ) => { + const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); + const cachePadding = Math.round( + currentlyDisplayedDateRange.length() * cacheSize, + ); + + if ( + cachedEventsStartTimestamp < allowedCacheEndTimestamp && + cachedEventsEndTimestamp > allowedCacheStartTimestamp + ) { + if (cachedEventsStartTimestamp < allowedCacheStartTimestamp) { + cachedEventsStartTimestamp = allowedCacheStartTimestamp; + } + if (cachedEventsEndTimestamp > allowedCacheEndTimestamp) { + cachedEventsEndTimestamp = allowedCacheEndTimestamp; + } + } + }; + + return { events: allEvents, lv, reset }; +} + +const mergePromiseElements = (n, o) => { + if (Array.isArray(n)) return o.concat(n); + return (o.push(n), o); +}; + +const getDateFromTimestamp = (timestamp) => { + return luxon.DateTime.fromMillis(timestamp).toFormat("yyyy-MM-dd"); +}; + +const getISODateFromTimestamp = (timestamp) => { + return luxon.DateTime.fromMillis(timestamp).toISO(); +}; + +const getTimestampFromISODate = (isoDate) => { + return luxon.DateTime.fromISO(isoDate).ts; +}; + +const getLuxonDateFromMillis = (millis) => { + return luxon.DateTime.fromMillis(millis); +};