mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
Merge branch 'refs/heads/master' into feat/operation-logs
* refs/heads/master: perf: lazy load ical.js to reduce initial bundle size
This commit is contained in:
commit
c7d37ab773
6 changed files with 106 additions and 89 deletions
35
package-lock.json
generated
35
package-lock.json
generated
|
|
@ -1174,18 +1174,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/@types/node": {
|
||||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
|
||||
|
|
@ -1199,15 +1187,6 @@
|
|||
"vite": "^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/vite": {
|
||||
"version": "7.1.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||
|
|
@ -15795,6 +15774,16 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"dev": true,
|
||||
|
|
@ -18677,7 +18666,7 @@
|
|||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
|
|
@ -25281,7 +25270,7 @@
|
|||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sanitize-filename": {
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ export class CalendarIntegrationService {
|
|||
},
|
||||
})
|
||||
.pipe(
|
||||
map((icalStrData) =>
|
||||
switchMap((icalStrData) =>
|
||||
getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalStrData,
|
||||
calProvider.id,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ describe('getRelevantEventsForCalendarIntegrationFromIcal - EXDATE Support', ()
|
|||
const startTimestamp = new Date('2025-01-01T00:00:00Z').getTime();
|
||||
const endTimestamp = new Date('2025-01-10T23:59:59Z').getTime();
|
||||
|
||||
it('should exclude date specified in EXDATE', () => {
|
||||
it('should exclude date specified in EXDATE', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -19,7 +19,7 @@ SUMMARY:Daily with EXDATE
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -35,7 +35,7 @@ END:VCALENDAR`;
|
|||
expect(eventDates).toEqual(['2025-01-01', '2025-01-03']);
|
||||
});
|
||||
|
||||
it('should exclude multiple dates specified in multiple EXDATE properties', () => {
|
||||
it('should exclude multiple dates specified in multiple EXDATE properties', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -50,7 +50,7 @@ SUMMARY:Daily with Multiple EXDATEs
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -67,7 +67,7 @@ END:VCALENDAR`;
|
|||
expect(eventDates).toEqual(['2025-01-01', '2025-01-03', '2025-01-05']);
|
||||
});
|
||||
|
||||
it('should exclude multiple dates specified in a single EXDATE property (comma separated)', () => {
|
||||
it('should exclude multiple dates specified in a single EXDATE property (comma separated)', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -81,7 +81,7 @@ SUMMARY:Daily with Comma Separated EXDATE
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ describe('getRelevantEventsForCalendarIntegrationFromIcal', () => {
|
|||
const endTimestamp = new Date('2025-12-31T23:59:59Z').getTime();
|
||||
|
||||
describe('non-recurring events', () => {
|
||||
it('should import event with dtend', () => {
|
||||
it('should import event with dtend', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -18,7 +18,7 @@ SUMMARY:Test Event with End
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -30,7 +30,7 @@ END:VCALENDAR`;
|
|||
expect(events[0].duration).toBe(3600000); // 1 hour in ms
|
||||
});
|
||||
|
||||
it('should import event without dtend (zero duration)', () => {
|
||||
it('should import event without dtend (zero duration)', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -41,7 +41,7 @@ SUMMARY:Test Event without End
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -55,7 +55,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
|
||||
describe('recurring events', () => {
|
||||
it('should import recurring event with dtend', () => {
|
||||
it('should import recurring event with dtend', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -68,7 +68,7 @@ SUMMARY:Weekly Meeting with End
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -82,7 +82,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should import recurring event with DURATION instead of dtend', () => {
|
||||
it('should import recurring event with DURATION instead of dtend', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -95,7 +95,7 @@ SUMMARY:Daily Task with Duration
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -109,7 +109,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should import recurring event without dtend or DURATION (zero duration)', () => {
|
||||
it('should import recurring event without dtend or DURATION (zero duration)', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -121,7 +121,7 @@ SUMMARY:Daily Reminder without Duration
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -135,7 +135,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should handle recurring event with very short duration', () => {
|
||||
it('should handle recurring event with very short duration', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -148,7 +148,7 @@ SUMMARY:15 Minute Standup
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -162,7 +162,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should keep descriptions for recurring events', () => {
|
||||
it('should keep descriptions for recurring events', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -176,7 +176,7 @@ DESCRIPTION:Recurring description text
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -192,7 +192,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
|
||||
describe('mixed event types', () => {
|
||||
it('should import mix of recurring and non-recurring events', () => {
|
||||
it('should import mix of recurring and non-recurring events', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -211,7 +211,7 @@ SUMMARY:Recurring Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -232,7 +232,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle events outside the time range', () => {
|
||||
it('should handle events outside the time range', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -244,7 +244,7 @@ SUMMARY:Past Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -254,7 +254,7 @@ END:VCALENDAR`;
|
|||
expect(events.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should continue processing even if one event fails', () => {
|
||||
it('should continue processing even if one event fails', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -272,7 +272,7 @@ SUMMARY:Bad Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -286,7 +286,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle all-day events and set isAllDay flag', () => {
|
||||
it('should handle all-day events and set isAllDay flag', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -298,7 +298,7 @@ SUMMARY:All Day Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -310,7 +310,7 @@ END:VCALENDAR`;
|
|||
expect(events[0].isAllDay).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT set isAllDay flag for timed events', () => {
|
||||
it('should NOT set isAllDay flag for timed events', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -322,7 +322,7 @@ SUMMARY:Timed Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -334,7 +334,7 @@ END:VCALENDAR`;
|
|||
expect(events[0].isAllDay).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle recurring event with UNTIL', () => {
|
||||
it('should handle recurring event with UNTIL', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -347,7 +347,7 @@ SUMMARY:Daily Until Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -361,7 +361,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should handle recurring all-day event without end time and set isAllDay flag', () => {
|
||||
it('should handle recurring all-day event without end time and set isAllDay flag', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -373,7 +373,7 @@ SUMMARY:Recurring All Day No End
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -388,7 +388,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should gracefully skip event with null dtstart', () => {
|
||||
it('should gracefully skip event with null dtstart', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -399,7 +399,7 @@ DESCRIPTION:This event has no DTSTART
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -411,7 +411,7 @@ END:VCALENDAR`;
|
|||
expect(events.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle mix of valid and invalid events gracefully', () => {
|
||||
it('should handle mix of valid and invalid events gracefully', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -433,7 +433,7 @@ SUMMARY:Another Valid Event
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -449,7 +449,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
|
||||
describe('RECURRENCE-ID handling (modified recurring events)', () => {
|
||||
it('should not create duplicate when recurring event has one modified instance', () => {
|
||||
it('should not create duplicate when recurring event has one modified instance', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -469,7 +469,7 @@ SUMMARY:Daily Meeting (Moved)
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -491,7 +491,7 @@ END:VCALENDAR`;
|
|||
expect(unmodifiedEvents.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle multiple modified instances in recurring event', () => {
|
||||
it('should handle multiple modified instances in recurring event', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -518,7 +518,7 @@ SUMMARY:Standup (Early & Short)
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -543,7 +543,7 @@ END:VCALENDAR`;
|
|||
expect(unmodifiedEvents.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle cancelled instance in recurring event', () => {
|
||||
it('should handle cancelled instance in recurring event', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -564,7 +564,7 @@ STATUS:CANCELLED
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -580,7 +580,7 @@ END:VCALENDAR`;
|
|||
expect(eventOnCancelledDate).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle all-day recurring event with DATE-based RECURRENCE-ID', () => {
|
||||
it('should handle all-day recurring event with DATE-based RECURRENCE-ID', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -598,7 +598,7 @@ SUMMARY:All Day Event (Modified)
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -615,7 +615,7 @@ END:VCALENDAR`;
|
|||
expect(unmodifiedEvents.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle recurring event without exceptions (no regression)', () => {
|
||||
it('should handle recurring event without exceptions (no regression)', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -628,7 +628,7 @@ SUMMARY:Regular Recurring
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -642,7 +642,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should handle exception outside time range gracefully', () => {
|
||||
it('should handle exception outside time range gracefully', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -662,7 +662,7 @@ SUMMARY:Event Series (Old Modified)
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -676,7 +676,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
});
|
||||
|
||||
it('should include modified instances that are in range when master recurring series is out of range', () => {
|
||||
it('should include modified instances that are in range when master recurring series is out of range', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -698,7 +698,7 @@ END:VCALENDAR`;
|
|||
|
||||
const laterStart = new Date('2025-01-10T00:00:00Z').getTime();
|
||||
const laterEnd = new Date('2025-02-01T00:00:00Z').getTime();
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
laterStart,
|
||||
|
|
@ -713,7 +713,7 @@ END:VCALENDAR`;
|
|||
expect(events[0].legacyIds).toEqual(['orphan@test']);
|
||||
});
|
||||
|
||||
it('should not include cancelled orphan exception events', () => {
|
||||
it('should not include cancelled orphan exception events', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -736,7 +736,7 @@ END:VCALENDAR`;
|
|||
|
||||
const laterStart = new Date('2025-01-10T00:00:00Z').getTime();
|
||||
const laterEnd = new Date('2025-02-01T00:00:00Z').getTime();
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
laterStart,
|
||||
|
|
@ -746,7 +746,7 @@ END:VCALENDAR`;
|
|||
expect(events.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should generate unique IDs for modified instances', () => {
|
||||
it('should generate unique IDs for modified instances', async () => {
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
|
|
@ -773,7 +773,7 @@ SUMMARY:Standup (Early)
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -795,7 +795,7 @@ END:VCALENDAR`;
|
|||
});
|
||||
|
||||
describe('timezone edge cases (Office 365 compatibility)', () => {
|
||||
it('should handle iCal with timezone reference but malformed VTIMEZONE gracefully', () => {
|
||||
it('should handle iCal with timezone reference but malformed VTIMEZONE gracefully', async () => {
|
||||
// This reproduces the issue from GitHub #5722 where Office 365 calendars
|
||||
// can cause "Cannot read properties of null (reading 'parent')" errors
|
||||
// in ICAL.helpers.updateTimezones()
|
||||
|
|
@ -829,7 +829,7 @@ END:VCALENDAR`;
|
|||
|
||||
// Should not throw an error
|
||||
expect(() => {
|
||||
const events = getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
startTimestamp,
|
||||
|
|
@ -840,7 +840,7 @@ END:VCALENDAR`;
|
|||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle iCal with TZID reference to unknown timezone gracefully', () => {
|
||||
it('should handle iCal with TZID reference to unknown timezone gracefully', async () => {
|
||||
// Some Office 365 calendars reference timezones that may not be in the
|
||||
// ical.js TimezoneService, which can cause updateTimezones to fail
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
|
|
@ -865,7 +865,7 @@ END:VCALENDAR`;
|
|||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle iCal with empty VTIMEZONE component gracefully', () => {
|
||||
it('should handle iCal with empty VTIMEZONE component gracefully', async () => {
|
||||
// Edge case: VTIMEZONE exists but has no subcomponents
|
||||
const icalData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// @ts-ignore
|
||||
import ICAL from 'ical.js';
|
||||
import { CalendarIntegrationEvent } from '../../calendar-integration/calendar-integration.model';
|
||||
import { Log } from '../../../core/log';
|
||||
import { loadIcalModule } from './ical-lazy-loader';
|
||||
|
||||
// NOTE: this sucks and is slow, but writing a new ical parser would be very hard... :(
|
||||
|
||||
|
|
@ -87,14 +86,16 @@ const buildExceptionMap = (vevents: any[]): ExceptionMap => {
|
|||
return exceptionMap;
|
||||
};
|
||||
|
||||
export const getRelevantEventsForCalendarIntegrationFromIcal = (
|
||||
export const getRelevantEventsForCalendarIntegrationFromIcal = async (
|
||||
icalData: string,
|
||||
calProviderId: string,
|
||||
startTimestamp: number,
|
||||
endTimestamp: number,
|
||||
): CalendarIntegrationEvent[] => {
|
||||
): Promise<CalendarIntegrationEvent[]> => {
|
||||
const ICAL = await loadIcalModule();
|
||||
let calendarIntegrationEvents: CalendarIntegrationEvent[] = [];
|
||||
const allPossibleFutureEvents = getAllPossibleEventsAfterStartFromIcal(
|
||||
ICAL,
|
||||
icalData,
|
||||
new Date(startTimestamp),
|
||||
);
|
||||
|
|
@ -347,7 +348,12 @@ const convertVEventToCalendarIntegrationEvent = (
|
|||
};
|
||||
};
|
||||
|
||||
const getAllPossibleEventsAfterStartFromIcal = (icalData: string, start: Date): any[] => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getAllPossibleEventsAfterStartFromIcal = (
|
||||
ICAL: any,
|
||||
icalData: string,
|
||||
start: Date,
|
||||
): any[] => {
|
||||
const c = ICAL.parse(icalData);
|
||||
const comp = new ICAL.Component(c);
|
||||
const tzAdded: string[] = [];
|
||||
|
|
|
|||
22
src/app/features/schedule/ical/ical-lazy-loader.ts
Normal file
22
src/app/features/schedule/ical/ical-lazy-loader.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Lazy loader for ical.js to reduce initial bundle size.
|
||||
* The ical.js library is ~76KB and only needed for calendar integration.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let icalModule: any = null;
|
||||
|
||||
/**
|
||||
* Lazily loads the ical.js module on first use.
|
||||
* Subsequent calls return the cached module.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const loadIcalModule = async (): Promise<any> => {
|
||||
if (!icalModule) {
|
||||
// @ts-ignore - ical.js exports default
|
||||
const mod = await import('ical.js');
|
||||
// Handle both ESM default export and CommonJS module.exports
|
||||
icalModule = mod.default || mod;
|
||||
}
|
||||
return icalModule;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue