mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-07-04 21:38:27 -07:00
fix(calendar): make showEnd behavior more consistent across time formats (#4059)
Closes #4053 This started as a small fix. After feedback and more debugging, I found more issues and inconsistencies and went a bit down the rabbit hole. The PR is bigger now, but I think the result is better: behavior is more predictable, and the output is more consistent. ## Changes - Multi-day full-day events now show an end date in `relative` and `dateheaders` when `showEnd` is enabled. - With `absolute` + `nextDaysRelative`, full-day events now keep the end date when the start is replaced by TODAY/TOMORROW/etc. - Timed events in `absolute` now also respect `showEndsOnlyWithDuration`. - Tests were expanded and refactored to cover more showEnd cases and keep the setup easier to maintain. - I also refactored parts of the calendar time rendering to reduce duplicate logic. ## Before <img width="1816" height="756" alt="before" src="https://github.com/user-attachments/assets/ebec81fd-0c4a-4f9f-bbe3-e2b32ef6756e" /> ## After <img width="1816" height="756" alt="after" src="https://github.com/user-attachments/assets/8a2c652d-dddc-4f6b-9074-fbef3411f9ed" />
This commit is contained in:
committed by
GitHub
parent
d072345775
commit
e1c44a86bb
+224
-124
@@ -384,32 +384,7 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
|
||||
if (event.fullDayEvent) {
|
||||
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)}`;
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
||||
timeWrapper.innerHTML = eventStartDateMoment.format("LT");
|
||||
|
||||
// Add endDate to dataheaders 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"))}`;
|
||||
}
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
|
||||
if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
|
||||
}
|
||||
if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
this.renderDateHeadersEventTime(eventWrapper, titleWrapper, event, eventStartDateMoment, eventEndDateMoment);
|
||||
} else {
|
||||
const timeWrapper = document.createElement("td");
|
||||
|
||||
@@ -417,106 +392,11 @@ Module.register("calendar", {
|
||||
const now = moment();
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat));
|
||||
// Add end time if showEnd
|
||||
if (this.config.showEnd) {
|
||||
// and has a duration
|
||||
if (event.startDate !== event.endDate) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat));
|
||||
}
|
||||
}
|
||||
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
eventEndDateMoment.subtract(1, "second");
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat));
|
||||
// only show end if requested and allowed and the dates are different
|
||||
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat));
|
||||
} else if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: eventEndDateMoment.fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.fromNow());
|
||||
}
|
||||
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.tomorrow) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.dayAfterTomorrow) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
}
|
||||
timeWrapper.innerHTML = this.buildAbsoluteTimeText(event, eventStartDateMoment, eventEndDateMoment, now);
|
||||
} else {
|
||||
// Show relative times
|
||||
if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
Log.debug("[calendar] event not hidden and not fullday");
|
||||
timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`;
|
||||
} else {
|
||||
Log.debug("[calendar] event full day or hidden");
|
||||
timeWrapper.innerHTML = `${CalendarUtils.capFirst(
|
||||
eventStartDateMoment.calendar(null, {
|
||||
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
|
||||
nextDay: `[${this.translate("TOMORROW")}]`,
|
||||
nextWeek: "dddd",
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
)}`;
|
||||
}
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.dayBeforeYesterday) {
|
||||
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
|
||||
}
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.tomorrow) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.dayAfterTomorrow) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// Ongoing event
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: eventEndDateMoment.fromNow(true)
|
||||
})
|
||||
);
|
||||
}
|
||||
timeWrapper.innerHTML = this.buildRelativeTimeText(event, eventStartDateMoment, eventEndDateMoment, now);
|
||||
}
|
||||
|
||||
timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`;
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
@@ -793,6 +673,226 @@ Module.register("calendar", {
|
||||
);
|
||||
},
|
||||
|
||||
createDateHeadersTimeWrapper (url) {
|
||||
const timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(url)}`;
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
||||
return timeWrapper;
|
||||
},
|
||||
|
||||
hasEventDuration (event) {
|
||||
return event.startDate !== event.endDate;
|
||||
},
|
||||
|
||||
shouldShowDateHeadersTimedEnd (event) {
|
||||
return this.config.showEnd && (!this.config.showEndsOnlyWithDuration || this.hasEventDuration(event));
|
||||
},
|
||||
|
||||
shouldShowRelativeTimedEnd (event) {
|
||||
return !this.config.hideTime && this.config.showEnd && (!this.config.showEndsOnlyWithDuration || this.hasEventDuration(event));
|
||||
},
|
||||
|
||||
getAdjustedFullDayEndMoment (endMoment) {
|
||||
return endMoment.clone().subtract(1, "second");
|
||||
},
|
||||
|
||||
renderDateHeadersEventTime (eventWrapper, titleWrapper, event, eventStartDateMoment, eventEndDateMoment) {
|
||||
if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
|
||||
if (event.fullDayEvent) {
|
||||
const adjustedEndMoment = this.getAdjustedFullDayEndMoment(eventEndDateMoment);
|
||||
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(adjustedEndMoment, "d")) {
|
||||
const timeWrapper = this.createDateHeadersTimeWrapper(event.url);
|
||||
timeWrapper.innerHTML = `-${CalendarUtils.capFirst(adjustedEndMoment.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 {
|
||||
const timeWrapper = this.createDateHeadersTimeWrapper(event.url);
|
||||
timeWrapper.innerHTML = eventStartDateMoment.format("LT");
|
||||
|
||||
// In dateheaders mode, keep the end as time-only to avoid redundant date info under a date header.
|
||||
if (this.shouldShowDateHeadersTimedEnd(event)) {
|
||||
timeWrapper.innerHTML += `-${CalendarUtils.capFirst(eventEndDateMoment.format("LT"))}`;
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
|
||||
}
|
||||
|
||||
if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
},
|
||||
|
||||
buildAbsoluteTimeText (event, eventStartDateMoment, eventEndDateMoment, now) {
|
||||
let timeText = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.dateFormat));
|
||||
|
||||
if (this.config.showEnd && (!this.config.showEndsOnlyWithDuration || this.hasEventDuration(event))) {
|
||||
const sameDay = this.isSameDay(eventStartDateMoment, eventEndDateMoment);
|
||||
if (sameDay && !this.dateFormatIncludesTime()) {
|
||||
timeText += `, ${eventStartDateMoment.format("LT")}`;
|
||||
}
|
||||
timeText += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`;
|
||||
}
|
||||
|
||||
if (event.fullDayEvent) {
|
||||
const adjustedEndMoment = this.getAdjustedFullDayEndMoment(eventEndDateMoment);
|
||||
timeText = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat));
|
||||
|
||||
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(adjustedEndMoment, "d")) {
|
||||
timeText += `-${CalendarUtils.capFirst(adjustedEndMoment.format(this.config.fullDayEventDateFormat))}`;
|
||||
} else if (!eventStartDateMoment.isSame(adjustedEndMoment, "d") && eventStartDateMoment.isBefore(now)) {
|
||||
timeText = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
|
||||
if (this.config.nextDaysRelative) {
|
||||
let relativeLabel = false;
|
||||
if (event.today) {
|
||||
timeText = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
relativeLabel = true;
|
||||
} else if (event.yesterday) {
|
||||
timeText = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
relativeLabel = true;
|
||||
} else if (event.tomorrow) {
|
||||
timeText = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
relativeLabel = true;
|
||||
} else if (event.dayAfterTomorrow && this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeText = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
relativeLabel = true;
|
||||
}
|
||||
|
||||
if (relativeLabel && this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(adjustedEndMoment, "d")) {
|
||||
timeText += `-${CalendarUtils.capFirst(adjustedEndMoment.format(this.config.fullDayEventDateFormat))}`;
|
||||
}
|
||||
}
|
||||
|
||||
return timeText;
|
||||
}
|
||||
|
||||
if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) {
|
||||
return CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: eventEndDateMoment.fromNow(true)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) {
|
||||
return CalendarUtils.capFirst(eventStartDateMoment.fromNow());
|
||||
}
|
||||
|
||||
return timeText;
|
||||
},
|
||||
|
||||
buildRelativeTimeText (event, eventStartDateMoment, eventEndDateMoment, now) {
|
||||
if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
let timeText;
|
||||
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
Log.debug("[calendar] event not hidden and not fullday");
|
||||
timeText = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`;
|
||||
} else {
|
||||
Log.debug("[calendar] event full day or hidden");
|
||||
timeText = `${CalendarUtils.capFirst(
|
||||
eventStartDateMoment.calendar(null, {
|
||||
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
|
||||
nextDay: `[${this.translate("TOMORROW")}]`,
|
||||
nextWeek: "dddd",
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (event.fullDayEvent) {
|
||||
if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
timeText = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.dayBeforeYesterday) {
|
||||
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
||||
timeText = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
|
||||
}
|
||||
} else if (event.yesterday) {
|
||||
timeText = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.tomorrow) {
|
||||
timeText = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.dayAfterTomorrow) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeText = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration) {
|
||||
const adjustedEndMoment = this.getAdjustedFullDayEndMoment(eventEndDateMoment);
|
||||
if (!eventStartDateMoment.isSame(adjustedEndMoment, "d")) {
|
||||
timeText += `-${CalendarUtils.capFirst(adjustedEndMoment.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");
|
||||
timeText = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`;
|
||||
} else if (this.shouldShowRelativeTimedEnd(event)) {
|
||||
if (this.isSameDay(eventStartDateMoment, eventEndDateMoment)) {
|
||||
const sameElseFormat = this.dateFormatIncludesTime() ? this.config.dateFormat : `${this.config.dateFormat}, LT`;
|
||||
timeText = CalendarUtils.capFirst(
|
||||
eventStartDateMoment.calendar(null, { sameElse: sameElseFormat })
|
||||
);
|
||||
}
|
||||
timeText += `-${this.formatTimedEventEnd(eventStartDateMoment, eventEndDateMoment)}`;
|
||||
}
|
||||
|
||||
return timeText;
|
||||
}
|
||||
|
||||
return CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: eventEndDateMoment.fromNow(true)
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines whether two moments are on the same day.
|
||||
* @param {moment.Moment} startMoment The start moment.
|
||||
* @param {moment.Moment} endMoment The end moment.
|
||||
* @returns {boolean} True when both moments share the same calendar day.
|
||||
*/
|
||||
isSameDay (startMoment, endMoment) {
|
||||
return startMoment.isSame(endMoment, "d");
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the configured dateFormat already contains time components.
|
||||
* @returns {boolean} True when dateFormat includes time tokens.
|
||||
*/
|
||||
dateFormatIncludesTime () {
|
||||
const dateFormatWithoutLiterals = this.config.dateFormat.replace(/\[[^\]]*\]/g, "");
|
||||
const localeDateFormat = moment.localeData();
|
||||
const expandedDateFormat = dateFormatWithoutLiterals.replace(
|
||||
/LTS|LT|LLLL|LLL|LL|L|llll|lll|ll|l/g,
|
||||
(token) => localeDateFormat.longDateFormat(token) || token
|
||||
);
|
||||
const expandedDateFormatWithoutLiterals = expandedDateFormat.replace(/\[[^\]]*\]/g, "");
|
||||
return (/(H{1,2}|h{1,2}|k{1,2}|m{1,2}|s{1,2}|a|A)/).test(expandedDateFormatWithoutLiterals);
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a timed event end value.
|
||||
* Uses time-only for same-day events and dateEndFormat for multi-day events.
|
||||
* @param {moment.Moment} startMoment The event start moment.
|
||||
* @param {moment.Moment} endMoment The event end moment.
|
||||
* @returns {string} The formatted end value.
|
||||
*/
|
||||
formatTimedEventEnd (startMoment, endMoment) {
|
||||
const endFormat = this.isSameDay(startMoment, endMoment) ? "LT" : this.config.dateEndFormat;
|
||||
return CalendarUtils.capFirst(endMoment.format(endFormat));
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the symbolClass for a specific calendar url.
|
||||
* @param {string} url The calendar url
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
const calendarShowEndConfigs = {
|
||||
event_with_time_over_multiple_days_non_repeating_display_end: {
|
||||
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: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_over_multiple_days_non_repeating_display_end_dateheaders: {
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_over_multiple_days_non_repeating_display_end_relative: {
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_over_multiple_days_non_repeating_no_display_end: {
|
||||
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: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
showEndsOnlyWithDuration: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_same_day_yearly_display_end_absolute: {
|
||||
address: "0.0.0.0",
|
||||
ipWhitelist: [],
|
||||
timeFormat: 24,
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
fade: false,
|
||||
urgency: 0,
|
||||
dateFormat: "Do.MMM",
|
||||
dateEndFormat: "Do.MMM, HH:mm",
|
||||
fullDayEventDateFormat: "Do.MMM",
|
||||
timeFormat: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_same_day_yearly_display_end_absolute_dateformat_lll: {
|
||||
address: "0.0.0.0",
|
||||
ipWhitelist: [],
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
fade: false,
|
||||
urgency: 0,
|
||||
dateFormat: "LLL",
|
||||
dateEndFormat: "Do.MMM, HH:mm",
|
||||
fullDayEventDateFormat: "Do.MMM",
|
||||
timeFormat: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time: {
|
||||
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: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_same_day_yearly_display_end_dateheaders: {
|
||||
address: "0.0.0.0",
|
||||
ipWhitelist: [],
|
||||
timeFormat: 24,
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
fade: false,
|
||||
urgency: 0,
|
||||
dateFormat: "Do.MMM",
|
||||
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_same_day_yearly.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_same_day_yearly_display_end_relative: {
|
||||
address: "0.0.0.0",
|
||||
ipWhitelist: [],
|
||||
timeFormat: 24,
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
fade: false,
|
||||
urgency: 0,
|
||||
dateFormat: "Do.MMM",
|
||||
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_same_day_yearly.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
event_with_time_same_day_yearly_display_end_relative_hide_time: {
|
||||
address: "0.0.0.0",
|
||||
ipWhitelist: [],
|
||||
timeFormat: 24,
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
fade: false,
|
||||
urgency: 0,
|
||||
dateFormat: "Do.MMM",
|
||||
dateEndFormat: "Do.MMM, HH:mm",
|
||||
fullDayEventDateFormat: "Do.MMM",
|
||||
timeFormat: "relative",
|
||||
getRelative: 0,
|
||||
hideTime: true,
|
||||
showEnd: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_same_day_yearly.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
fullday_multiday_showend_dateheaders: {
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
fullday_multiday_showend_nextdaysrelative: {
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
fullday_multiday_showend_relative: {
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const defaultScenario = "event_with_time_over_multiple_days_non_repeating_display_end";
|
||||
const selectedScenario = process.env.MM_CALENDAR_SHOWEND_SCENARIO || defaultScenario;
|
||||
const config = calendarShowEndConfigs[selectedScenario];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Unknown MM_CALENDAR_SHOWEND_SCENARIO: ${selectedScenario}`);
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
let 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: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
let 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: "absolute",
|
||||
getRelative: 0,
|
||||
showEnd: true,
|
||||
showEndsOnlyWithDuration: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/event_with_time_over_multiple_days_non_repeating.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
||||
@@ -50,8 +50,29 @@ describe("Calendar module", () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const defaultCalendarNow = "08 Oct 2024 12:30:00 GMT-07:00";
|
||||
const defaultCalendarTimeZone = "America/Chicago";
|
||||
const showEndConfigPath = "tests/configs/modules/calendar/calendarShowEndConfigs.js";
|
||||
|
||||
const startCalendarShowEndScenario = async (scenario, now = defaultCalendarNow, timeZone = defaultCalendarTimeZone) => {
|
||||
process.env.MM_CALENDAR_SHOWEND_SCENARIO = scenario;
|
||||
await helpers.startApplication(showEndConfigPath, now, [], timeZone);
|
||||
};
|
||||
|
||||
const expectFirstEventTimeCell = async ({ scenario, expectedTime, now = defaultCalendarNow, timeZone = defaultCalendarTimeZone }) => {
|
||||
await startCalendarShowEndScenario(scenario, now, timeZone);
|
||||
await expect(doTestTableContent(".calendar .event", ".time", expectedTime, first)).resolves.toBe(true);
|
||||
};
|
||||
|
||||
const getFirstEventTimeText = async () => {
|
||||
const timeCell = global.page.locator(".calendar .event .time").locator(`nth=${first}`);
|
||||
await timeCell.waitFor({ state: "visible" });
|
||||
return (await timeCell.textContent()) || "";
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await helpers.stopApplication();
|
||||
delete process.env.MM_CALENDAR_SHOWEND_SCENARIO;
|
||||
});
|
||||
|
||||
describe("Test css classes", () => {
|
||||
@@ -283,7 +304,7 @@ describe("Calendar module", () => {
|
||||
|
||||
describe("one event no end display", () => {
|
||||
it("don't display end", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
|
||||
await startCalendarShowEndScenario("event_with_time_over_multiple_days_non_repeating_no_display_end");
|
||||
// just
|
||||
await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00", first)).resolves.toBe(true);
|
||||
});
|
||||
@@ -291,12 +312,77 @@ describe("Calendar module", () => {
|
||||
|
||||
describe("display end display end", () => {
|
||||
it("display end", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
|
||||
await startCalendarShowEndScenario("event_with_time_over_multiple_days_non_repeating_display_end");
|
||||
// just
|
||||
await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showEnd for timed multi-day events", () => {
|
||||
const timedMultiDayCases = [
|
||||
{
|
||||
name: "relative timeFormat shows start and end for timed multi-day events",
|
||||
scenario: "event_with_time_over_multiple_days_non_repeating_display_end_relative",
|
||||
expectedTime: "25th.Oct, 20:00-26th.Oct, 06:00"
|
||||
},
|
||||
{
|
||||
name: "dateheaders timeFormat shows end for timed multi-day events",
|
||||
scenario: "event_with_time_over_multiple_days_non_repeating_display_end_dateheaders",
|
||||
expectedTime: "20:00-06:00"
|
||||
}
|
||||
];
|
||||
|
||||
it.each(timedMultiDayCases)("$name", async (testCase) => {
|
||||
expect.hasAssertions();
|
||||
await expectFirstEventTimeCell(testCase);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showEnd for timed same-day events", () => {
|
||||
const timedSameDaySimpleCases = [
|
||||
{
|
||||
name: "absolute timeFormat shows start and end time without repeating date",
|
||||
scenario: "event_with_time_same_day_yearly_display_end_absolute",
|
||||
expectedTime: "25th.Oct, 20:00-22:00"
|
||||
},
|
||||
{
|
||||
name: "absolute timeFormat with time in dateFormat does not duplicate start time",
|
||||
scenario: "event_with_time_same_day_yearly_display_end_absolute_dateformat_with_time",
|
||||
expectedTime: "25th.Oct, 20:00-22:00"
|
||||
},
|
||||
{
|
||||
name: "relative timeFormat shows start and end time without repeating date",
|
||||
scenario: "event_with_time_same_day_yearly_display_end_relative",
|
||||
expectedTime: "25th.Oct, 20:00-22:00"
|
||||
},
|
||||
{
|
||||
name: "dateheaders timeFormat shows start and end time only",
|
||||
scenario: "event_with_time_same_day_yearly_display_end_dateheaders",
|
||||
expectedTime: "20:00-22:00"
|
||||
}
|
||||
];
|
||||
|
||||
it.each(timedSameDaySimpleCases)("$name", async (testCase) => {
|
||||
expect.hasAssertions();
|
||||
await expectFirstEventTimeCell(testCase);
|
||||
});
|
||||
|
||||
it("absolute timeFormat with dateFormat LLL does not duplicate start time", async () => {
|
||||
await startCalendarShowEndScenario("event_with_time_same_day_yearly_display_end_absolute_dateformat_lll");
|
||||
const timeText = await getFirstEventTimeText();
|
||||
const timeTokens = timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || [];
|
||||
expect(timeTokens).toHaveLength(2);
|
||||
expect(timeText).toContain("-");
|
||||
});
|
||||
|
||||
it("relative timeFormat with hideTime does not show start or end times", async () => {
|
||||
await startCalendarShowEndScenario("event_with_time_same_day_yearly_display_end_relative_hide_time");
|
||||
const timeText = await getFirstEventTimeText();
|
||||
expect(timeText).toContain("25th.Oct");
|
||||
expect(timeText.match(/\d{1,2}:\d{2}(?:\s?[AP]M)?/gi) || []).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
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 +400,30 @@ describe("Calendar module", () => {
|
||||
await expect(doTestTableContent(".testNotification", ".elementCount", "12", first)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showEnd for multi-day full-day events", () => {
|
||||
const fullDayShowEndCases = [
|
||||
{
|
||||
name: "relative timeFormat shows start and end date",
|
||||
scenario: "fullday_multiday_showend_relative",
|
||||
expectedTime: "25th.Oct-30th.Oct"
|
||||
},
|
||||
{
|
||||
name: "dateheaders timeFormat shows end date in time cell",
|
||||
scenario: "fullday_multiday_showend_dateheaders",
|
||||
expectedTime: "-30th.Oct"
|
||||
},
|
||||
{
|
||||
name: "absolute timeFormat with nextDaysRelative shows relative label and end date",
|
||||
scenario: "fullday_multiday_showend_nextdaysrelative",
|
||||
expectedTime: "Tomorrow-30th.Oct",
|
||||
now: "24 Oct 2024 12:30:00 GMT-07:00"
|
||||
}
|
||||
];
|
||||
|
||||
it.each(fullDayShowEndCases)("$name", async (testCase) => {
|
||||
expect.hasAssertions();
|
||||
await expectFirstEventTimeCell(testCase);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,18 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//MagicMirror Test//timed-same-day-yearly//EN
|
||||
CALSCALE:GREGORIAN
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20241025T200000
|
||||
DTEND:20241025T220000
|
||||
DTSTAMP:20241024T153358Z
|
||||
UID:timed-same-day-yearly@magicmirror
|
||||
CREATED:20241024T153313Z
|
||||
LAST-MODIFIED:20241024T153330Z
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
RRULE:FREQ=YEARLY
|
||||
SUMMARY:Same day timed event
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
Reference in New Issue
Block a user