From 36cd08c236ad7dbbfd39f5d6d0c2929d4dcc46e2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 29 Apr 2026 20:19:49 -0400 Subject: [PATCH] feat(events): expose recurrence presets in EventSlideOver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Repeat" select (None / Daily / Weekly / Monthly / Yearly) that reads/writes the existing Event.recurrence RRULE. CalDAV-imported rules with extra parts (e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR) surface as a disabled "Custom" option with the raw rule shown read-only — visible but preserved unless the user explicitly picks a preset to replace it. EventUpdatePayload.recurrence is now string | null so we can clear via PATCH; backend service already treats null as "clear" (recurrence is in the nullable set in update_event). Co-Authored-By: Claude Opus 4.7 --- frontend/src/api/client.ts | 2 +- frontend/src/components/EventSlideOver.vue | 68 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 29bf322..f6e0819 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -545,7 +545,7 @@ export interface EventUpdatePayload { description?: string; location?: string; color?: string; - recurrence?: string; + recurrence?: string | null; project_id?: number; } diff --git a/frontend/src/components/EventSlideOver.vue b/frontend/src/components/EventSlideOver.vue index 4943a9e..a962e12 100644 --- a/frontend/src/components/EventSlideOver.vue +++ b/frontend/src/components/EventSlideOver.vue @@ -37,6 +37,36 @@ const description = ref(""); const location = ref(""); const color = ref(""); const projectId = ref(null); +const recurrence = ref(""); + +// Preset RRULE strings. The select binds to `recurrencePreset`, which writes +// through to `recurrence`. CalDAV-imported rules with extra parts +// (e.g. `FREQ=WEEKLY;BYDAY=MO,WE,FR`) fall through to "custom" and the raw +// string is shown read-only below the select. +const RECURRENCE_PRESETS: Record = { + none: "", + daily: "FREQ=DAILY", + weekly: "FREQ=WEEKLY", + monthly: "FREQ=MONTHLY", + yearly: "FREQ=YEARLY", +}; + +const recurrencePreset = computed({ + get() { + const r = (recurrence.value || "").trim(); + if (!r) return "none"; + for (const [key, val] of Object.entries(RECURRENCE_PRESETS)) { + if (val && val === r) return key; + } + return "custom"; + }, + set(key: string) { + if (key === "custom") return; // no-op; can't pick custom from dropdown + recurrence.value = RECURRENCE_PRESETS[key] ?? ""; + }, +}); + +const isCustomRecurrence = computed(() => recurrencePreset.value === "custom"); function dateFromIso(iso: string): string { const d = new Date(iso); @@ -115,6 +145,7 @@ function resetForm() { location.value = props.event.location || ""; color.value = props.event.color || ""; projectId.value = props.event.project_id; + recurrence.value = props.event.recurrence || ""; _lastDurationMin = !allDay.value && startTime.value && endTime.value ? durationMin(startTime.value, endTime.value) : 60; if (_lastDurationMin <= 0) _lastDurationMin = 60; } else { @@ -130,6 +161,7 @@ function resetForm() { location.value = ""; color.value = ""; projectId.value = null; + recurrence.value = ""; _lastDurationMin = 60; } deleteConfirm.value = false; @@ -257,6 +289,7 @@ async function save() { location: location.value, color: color.value, project_id: projectId.value ?? undefined, + recurrence: recurrence.value || null, }; const updated = await updateEvent(props.event.id, payload); emit("updated", updated); @@ -270,6 +303,7 @@ async function save() { location: location.value, color: color.value, project_id: projectId.value ?? undefined, + recurrence: recurrence.value || undefined, }; const created = await createEvent(payload); emit("created", created); @@ -374,6 +408,24 @@ async function doDelete() { + +
+ + +

+ Custom rule: {{ recurrence }} +
+ Picking a preset will replace this rule. +

+
+
@@ -539,6 +591,22 @@ async function doDelete() { color: var(--color-text-secondary); } +.recurrence-custom-hint { + margin: 4px 0 0; + font-size: 0.75rem; + color: var(--color-text-secondary); + line-height: 1.4; +} +.recurrence-custom-hint code { + background: var(--color-input-bg, #111113); + border: 1px solid var(--color-border, #2a2b30); + border-radius: 4px; + padding: 1px 5px; + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.72rem; + color: var(--color-text, #e8e9f0); +} + .toggle-btn { background: var(--color-input-bg, #111113); border: 1px solid var(--color-border, #2a2b30);