diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 0d8575c810..89a29cf14c 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -387,8 +387,24 @@ Module.register("calendar", { if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper); if (event.fullDayEvent) { - titleWrapper.colSpan = "2"; - titleWrapper.classList.add("align-left"); + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { + const endMomentAdjusted = eventEndDateMoment.clone().subtract(1, "second"); + if (!eventStartDateMoment.isSame(endMomentAdjusted, "d")) { + const timeWrapper = document.createElement("td"); + timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; + timeWrapper.style.paddingLeft = "2px"; + timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; + timeWrapper.innerHTML = `-${CalendarUtils.capFirst(endMomentAdjusted.format(this.config.fullDayEventDateFormat))}`; + eventWrapper.appendChild(timeWrapper); + if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right"); + } else { + titleWrapper.colSpan = "2"; + titleWrapper.classList.add("align-left"); + } + } else { + titleWrapper.colSpan = "2"; + titleWrapper.classList.add("align-left"); + } } else { const timeWrapper = document.createElement("td"); timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`; @@ -396,12 +412,13 @@ Module.register("calendar", { timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left"; timeWrapper.innerHTML = eventStartDateMoment.format("LT"); - // Add endDate to dataheaders if showEnd is enabled + // Add endDate to dateheaders if showEnd is enabled if (this.config.showEnd) { if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) { // no duration here, don't display end } else { - timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`; + const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(endFormat))}`; } } @@ -423,8 +440,9 @@ Module.register("calendar", { if (this.config.showEnd) { // and has a duration if (event.startDate !== event.endDate) { + const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; timeWrapper.innerHTML += "-"; - timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat)); + timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(endFormat)); } } @@ -454,17 +472,26 @@ Module.register("calendar", { } if (event.fullDayEvent && this.config.nextDaysRelative) { // Full days events within the next two days + let relativeLabel = false; if (event.today) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY")); + relativeLabel = true; } else if (event.yesterday) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY")); + relativeLabel = true; } else if (event.tomorrow) { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW")); + relativeLabel = true; } else if (event.dayAfterTomorrow) { if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); + relativeLabel = true; } } + // Append end date only if a relative label replaced the start date + if (relativeLabel && this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) { + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat))}`; + } } } else { // Show relative times @@ -501,11 +528,22 @@ Module.register("calendar", { timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW")); } } + // Show end date for multi-day full-day events if showEnd is configured + if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) { + const endMomentAdjusted = eventEndDateMoment.clone().subtract(1, "second"); + if (!eventStartDateMoment.isSame(endMomentAdjusted, "d")) { + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(endMomentAdjusted.format(this.config.fullDayEventDateFormat))}`; + } + } Log.info("[calendar] event fullday"); } else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) { Log.info("[calendar] not full day but within getRelative size"); // If event is within getRelative hours, display 'in xxx' time format or moment.fromNow() timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`; + } else if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || event.startDate !== event.endDate)) { + // Show end time for timed events + const endFormat = eventStartDateMoment.isSame(eventEndDateMoment, "d") ? "LT" : this.config.dateEndFormat; + timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format(endFormat))}`; } } else { // Ongoing event diff --git a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js new file mode 100644 index 0000000000..b7dd24218c --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js @@ -0,0 +1,33 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js new file mode 100644 index 0000000000..92afb5f184 --- /dev/null +++ b/tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js @@ -0,0 +1,33 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + dateEndFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_yearly.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js b/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js new file mode 100644 index 0000000000..2033d55d60 --- /dev/null +++ b/tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js @@ -0,0 +1,31 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "dateheaders", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js b/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js new file mode 100644 index 0000000000..32ca4ec2b1 --- /dev/null +++ b/tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js @@ -0,0 +1,32 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + showEnd: true, + nextDaysRelative: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/configs/modules/calendar/fullday_multiday_showend_relative.js b/tests/configs/modules/calendar/fullday_multiday_showend_relative.js new file mode 100644 index 0000000000..c4dc08cc2d --- /dev/null +++ b/tests/configs/modules/calendar/fullday_multiday_showend_relative.js @@ -0,0 +1,31 @@ +let config = { + address: "0.0.0.0", + ipWhitelist: [], + + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + fullDayEventDateFormat: "Do.MMM", + timeFormat: "relative", + getRelative: 0, + showEnd: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/fullday_event_over_multiple_days_nonrepeating.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index 2a9845ae18..6436940d77 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -297,6 +297,18 @@ describe("Calendar module", () => { }); }); + describe("showEnd for timed multi-day events", () => { + it("relative timeFormat shows start and end for timed multi-day events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true); + }); + + it("dateheaders timeFormat shows end for timed multi-day events", async () => { + await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "-26th.Oct, 06:00", first)).resolves.toBe(true); + }); + }); + describe("count and check symbols", () => { it("in array", async () => { await helpers.startApplication("tests/configs/modules/calendar/symboltest.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); @@ -314,4 +326,21 @@ describe("Calendar module", () => { await expect(doTestTableContent(".testNotification", ".elementCount", "12", first)).resolves.toBe(true); }); }); + + describe("showEnd for multi-day full-day events", () => { + it("relative timeFormat shows start and end date", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_relative.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct-30th.Oct", first)).resolves.toBe(true); + }); + + it("dateheaders timeFormat shows end date in time cell", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_dateheaders.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "-30th.Oct", first)).resolves.toBe(true); + }); + + it("absolute timeFormat with nextDaysRelative shows relative label and end date", async () => { + await helpers.startApplication("tests/configs/modules/calendar/fullday_multiday_showend_nextdaysrelative.js", "24 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago"); + await expect(doTestTableContent(".calendar .event", ".time", "Tomorrow-30th.Oct", first)).resolves.toBe(true); + }); + }); }); diff --git a/tests/mocks/event_with_time_over_multiple_days_yearly.ics b/tests/mocks/event_with_time_over_multiple_days_yearly.ics new file mode 100644 index 0000000000..00f65b2f1d --- /dev/null +++ b/tests/mocks/event_with_time_over_multiple_days_yearly.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//MagicMirror Test//timed-multiday-yearly//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +DTSTART:20241026T010000Z +DTEND:20241026T110000Z +DTSTAMP:20241024T153358Z +UID:4maud6s79m41a99pj2g7j5km0a@google.com +CREATED:20241024T153313Z +LAST-MODIFIED:20241024T153330Z +SEQUENCE:0 +STATUS:CONFIRMED +RRULE:FREQ=YEARLY +SUMMARY:Sleep over at Bobs +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR