From 60b74f5959b88b4c04082a9f5d53ceed720e920a Mon Sep 17 00:00:00 2001 From: BeatLink Date: Sun, 22 Feb 2026 18:13:08 -0500 Subject: [PATCH] Add Recurrence Support for Calendar --- apps/client/package.json | 1 + .../collections/calendar/event_builder.ts | 19 +++++- .../widgets/collections/calendar/index.tsx | 1 + pnpm-lock.yaml | 65 +++++++++++++------ 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 7acbff720f..9db5093b45 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -23,6 +23,7 @@ "@fullcalendar/list": "6.1.20", "@fullcalendar/multimonth": "6.1.20", "@fullcalendar/timegrid": "6.1.20", + "@fullcalendar/rrule": "6.1.20", "@maplibre/maplibre-gl-leaflet": "0.1.3", "@mermaid-js/layout-elk": "0.2.0", "@mind-elixir/node-menu": "5.0.1", diff --git a/apps/client/src/widgets/collections/calendar/event_builder.ts b/apps/client/src/widgets/collections/calendar/event_builder.ts index f4611ccd7e..19ff80bdc7 100644 --- a/apps/client/src/widgets/collections/calendar/event_builder.ts +++ b/apps/client/src/widgets/collections/calendar/event_builder.ts @@ -11,7 +11,8 @@ interface Event { endDate?: string | null, startTime?: string | null, endTime?: string | null, - isArchived?: boolean; + isArchived?: boolean, + recurrence?: string | null; } export async function buildEvents(noteIds: string[]) { @@ -28,8 +29,9 @@ export async function buildEvents(noteIds: string[]) { const endDate = getCustomisableLabel(note, "endDate", "calendar:endDate"); const startTime = getCustomisableLabel(note, "startTime", "calendar:startTime"); const endTime = getCustomisableLabel(note, "endTime", "calendar:endTime"); + const recurrence = getCustomisableLabel(note, "recurrence", "calendar:recurrence"); const isArchived = note.hasLabel("archived"); - events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, isArchived })); + events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, recurrence, isArchived })); } return events.flat(); @@ -79,7 +81,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg) return events.flat(); } -export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) { +export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, recurrence, isArchived }: Event) { const customTitleAttributeName = note.getLabelValue("calendar:title"); const titles = await parseCustomTitle(customTitleAttributeName, note); const colorClass = note.getColorClass(); @@ -118,6 +120,17 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e if (endDate) { eventData.end = endDate; } + + if (recurrence) { + eventData.rrule = `DTSTART:${startDate.replace(/[-:]/g, "")}\n${recurrence}`; + if (endDate){ + const duration = (d => + String(d / 36e5 | 0).padStart(2, "0") + ":" + + String(d / 6e4 % 60 | 0).padStart(2, "0") + )((new Date(endDate)) - (new Date(startDate))); + eventData.duration = duration + } + } events.push(eventData); } return events; diff --git a/apps/client/src/widgets/collections/calendar/index.tsx b/apps/client/src/widgets/collections/calendar/index.tsx index 23f8371125..349d666e6c 100644 --- a/apps/client/src/widgets/collections/calendar/index.tsx +++ b/apps/client/src/widgets/collections/calendar/index.tsx @@ -252,6 +252,7 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) { plugins.push((await import("@fullcalendar/timegrid")).default); plugins.push((await import("@fullcalendar/list")).default); plugins.push((await import("@fullcalendar/multimonth")).default); + plugins.push((await import("@fullcalendar/rrule")).default); if (isEditable || isCalendarRoot) { plugins.push((await import("@fullcalendar/interaction")).default); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55a6484280..6b1e2d6e85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: '@fullcalendar/multimonth': specifier: 6.1.20 version: 6.1.20(@fullcalendar/core@6.1.20) + '@fullcalendar/rrule': + specifier: 6.1.20 + version: 6.1.20(@fullcalendar/core@6.1.20)(rrule@2.8.1) '@fullcalendar/timegrid': specifier: 6.1.20 version: 6.1.20(@fullcalendar/core@6.1.20) @@ -3392,6 +3395,12 @@ packages: peerDependencies: '@fullcalendar/core': ~6.1.20 + '@fullcalendar/rrule@6.1.20': + resolution: {integrity: sha512-5Awk7bmaA97hSZRpIBehenXkYreVIvx8nnaMFZ/LDGRuK1mgbR4vSUrDTvVU+oEqqKnj/rqMBByWqN5NeehQxw==} + peerDependencies: + '@fullcalendar/core': ~6.1.20 + rrule: ^2.6.0 + '@fullcalendar/timegrid@6.1.20': resolution: {integrity: sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==} peerDependencies: @@ -13031,6 +13040,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rrule@2.8.1: + resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -15950,6 +15962,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.4.0 '@ckeditor/ckeditor5-upload': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@47.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -16090,12 +16104,16 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-cloud-services@47.4.0': dependencies: '@ckeditor/ckeditor5-core': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.4.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -16107,6 +16125,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-collaboration-core@47.4.0': dependencies: @@ -16286,6 +16306,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-classic@47.4.0': dependencies: @@ -16306,6 +16328,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-inline@47.4.0': dependencies: @@ -16375,6 +16399,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-undo': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-export-inline-styles@47.4.0': dependencies: @@ -16417,6 +16443,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-font@47.4.0': dependencies: @@ -16480,6 +16508,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-horizontal-line@47.4.0': dependencies: @@ -16489,6 +16519,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-html-embed@47.4.0': dependencies: @@ -16515,6 +16547,8 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-icons@47.4.0': {} @@ -16658,6 +16692,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-mention@47.4.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': dependencies: @@ -16667,8 +16703,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-merge-fields@47.4.0': dependencies: @@ -16689,8 +16723,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-operations-compressor@47.4.0': dependencies: @@ -16745,8 +16777,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-pagination@47.4.0': dependencies: @@ -16854,8 +16884,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-slash-command@47.4.0': dependencies: @@ -16868,8 +16896,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-source-editing-enhanced@47.4.0': dependencies: @@ -16917,8 +16943,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-table@47.4.0': dependencies: @@ -16931,8 +16955,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-template@47.4.0': dependencies: @@ -17007,8 +17029,6 @@ snapshots: '@ckeditor/ckeditor5-icons': 47.4.0 '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-upload@47.4.0': dependencies: @@ -17045,8 +17065,6 @@ snapshots: '@ckeditor/ckeditor5-engine': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-widget@47.4.0': dependencies: @@ -17066,8 +17084,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@codemirror/autocomplete@6.18.6': dependencies: @@ -18409,6 +18425,11 @@ snapshots: '@fullcalendar/core': 6.1.20 '@fullcalendar/daygrid': 6.1.20(@fullcalendar/core@6.1.20) + '@fullcalendar/rrule@6.1.20(@fullcalendar/core@6.1.20)(rrule@2.8.1)': + dependencies: + '@fullcalendar/core': 6.1.20 + rrule: 2.8.1 + '@fullcalendar/timegrid@6.1.20(@fullcalendar/core@6.1.20)': dependencies: '@fullcalendar/core': 6.1.20 @@ -30031,6 +30052,10 @@ snapshots: transitivePeerDependencies: - supports-color + rrule@2.8.1: + dependencies: + tslib: 2.8.1 + rrweb-cssom@0.8.0: optional: true