diff --git a/apps/client/package.json b/apps/client/package.json index 9db5093b45..1fd8bea2c6 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -22,8 +22,8 @@ "@fullcalendar/interaction": "6.1.20", "@fullcalendar/list": "6.1.20", "@fullcalendar/multimonth": "6.1.20", - "@fullcalendar/timegrid": "6.1.20", "@fullcalendar/rrule": "6.1.20", + "@fullcalendar/timegrid": "6.1.20", "@maplibre/maplibre-gl-leaflet": "0.1.3", "@mermaid-js/layout-elk": "0.2.0", "@mind-elixir/node-menu": "5.0.1", @@ -64,6 +64,7 @@ "react-i18next": "16.5.4", "react-window": "2.2.7", "reveal.js": "5.2.1", + "rrule": "2.8.1", "svg-pan-zoom": "3.6.2", "tabulator-tables": "6.3.1", "vanilla-js-wheel-zoom": "9.0.4" diff --git a/apps/client/src/widgets/collections/calendar/event_builder.ts b/apps/client/src/widgets/collections/calendar/event_builder.ts index 4967a5ca1b..b60a203d89 100644 --- a/apps/client/src/widgets/collections/calendar/event_builder.ts +++ b/apps/client/src/widgets/collections/calendar/event_builder.ts @@ -1,9 +1,11 @@ import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js"; import clsx from "clsx"; +import * as rruleLib from 'rrule'; import FNote from "../../../entities/fnote"; import froca from "../../../services/froca"; import server from "../../../services/server"; +import toastService from "../../../services/toast"; import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils"; interface Event { @@ -122,13 +124,30 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e } if (recurrence) { - delete eventData.end; - eventData.rrule = `DTSTART:${startDate.replace(/[-:]/g, "")}\n${recurrence}`; - if (endDate){ - const diffMs = new Date(endDate).getTime() - new Date(startDate).getTime(); - const hours = Math.floor(diffMs / 3600000); - const minutes = Math.floor((diffMs / 60000) % 60); - eventData.duration = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; + // Generate rrule string + const rruleString = `DTSTART:${startDate.replace(/[-:]/g, "")}\n${recurrence}`; + + // Validate rrule string + let rruleValid = true; + try { + rruleLib.rrulestr(rruleString, { forceset: true }) as rruleLib.RRuleSet; + } catch { + rruleValid = false; + } + + if (rruleValid) { + delete eventData.end; + eventData.rrule = rruleString; + if (endDate){ + const diffMs = new Date(endDate).getTime() - new Date(startDate).getTime(); + const hours = Math.floor(diffMs / 3600000); + const minutes = Math.floor((diffMs / 60000) % 60); + eventData.duration = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; + } + } else { + const errorMessage = `Note "${note.noteId} ${note.title}" has an invalid #recurrence string. This note will not recur.`; + toastService.showError(errorMessage); + console.error(errorMessage); } } events.push(eventData); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b1e2d6e85..0097f4aaf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -326,6 +326,9 @@ importers: reveal.js: specifier: 5.2.1 version: 5.2.1 + rrule: + specifier: 2.8.1 + version: 2.8.1 svg-pan-zoom: specifier: 3.6.2 version: 3.6.2