Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion MMM-GoogleCalendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ Module.register("MMM-GoogleCalendar", {
// if those are common, as it avoids the listContainsEvent check for duplicates.

// Filter based on `excludedEvents` config
if (this.config.excludedEvents?.length && this.config.excludedEvents.includes(event.summary)) {
if (this.config.excludedEvents?.length && this.isEventExcluded(event.summary)) {
Log.debug(`Event ${event.id} ('${event.summary}') filtered due to excludedEvents settings.`);
return true;
}
Expand All @@ -552,6 +552,54 @@ Module.register("MMM-GoogleCalendar", {
return false; // Event should not be filtered out
},

/**
* Determines whether an event title matches any `excludedEvents` filter.
*
* Mirrors the default MagicMirror calendar module: each entry may be a plain
* string (case-insensitive substring match) or an object
* `{ filterBy, caseSensitive, regex }`. Previously this used an exact,
* case-sensitive full-title equality check, so partial filters such as
* "Birthday" never matched "John's Birthday" (issue #55).
*
* @param {string} title The event summary/title to test.
* @returns {boolean} True if the title matches an exclusion filter.
*/
isEventExcluded: function (title) {
if (typeof title !== "string") {
return false;
}

for (const filter of this.config.excludedEvents) {
let pattern = filter;
let caseSensitive = false;
let useRegex = false;

if (filter && typeof filter === "object") {
pattern = filter.filterBy;
caseSensitive = !!filter.caseSensitive;
useRegex = !!filter.regex;
}

if (typeof pattern !== "string" || pattern === "") {
continue;
}

if (useRegex) {
if (new RegExp(pattern, caseSensitive ? "" : "i").test(title)) {
return true;
}
} else if (caseSensitive) {
if (title.includes(pattern)) {
return true;
}
} else if (title.toLowerCase().includes(pattern.toLowerCase())) {
return true;
}
}

return false;
},

fetchCalendars: function () {
this.config.calendars.forEach((calendar) => {
if (!calendar.calendarID) {
Expand Down
40 changes: 40 additions & 0 deletions __tests__/MMM-GoogleCalendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,45 @@ describe('MMM-GoogleCalendar', () => {
});
});

describe('isEventExcluded', () => {
test('matches a case-insensitive substring (issue #55)', () => {
GCal.config.excludedEvents = ['Birthday'];
expect(GCal.isEventExcluded("John's Birthday")).toBe(true);
expect(GCal.isEventExcluded('my birthday party')).toBe(true);
});

test('does not match unrelated titles', () => {
GCal.config.excludedEvents = ['Birthday'];
expect(GCal.isEventExcluded('Team standup')).toBe(false);
});

test('still matches an exact full title (backwards compatible)', () => {
GCal.config.excludedEvents = ['Lunch'];
expect(GCal.isEventExcluded('Lunch')).toBe(true);
});

test('object form with caseSensitive only matches exact case', () => {
GCal.config.excludedEvents = [{ filterBy: 'Standup', caseSensitive: true }];
expect(GCal.isEventExcluded('Daily Standup')).toBe(true);
expect(GCal.isEventExcluded('daily standup')).toBe(false);
});

test('object form with regex matches a pattern', () => {
GCal.config.excludedEvents = [{ filterBy: '^\\[private\\]', regex: true }];
expect(GCal.isEventExcluded('[private] dentist')).toBe(true);
expect(GCal.isEventExcluded('not [private]')).toBe(false);
});

test('returns false for a private event with no summary', () => {
GCal.config.excludedEvents = ['Birthday'];
expect(GCal.isEventExcluded(undefined)).toBe(false);
});

test('ignores empty/invalid filter entries', () => {
GCal.config.excludedEvents = ['', { filterBy: '' }, {}];
expect(GCal.isEventExcluded('anything')).toBe(false);
});
});

// Add more describe blocks for other pure functions if identified
});
Loading