feat(events): expose recurrence presets in EventSlideOver
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 <noreply@anthropic.com>
This commit is contained in:
@@ -545,7 +545,7 @@ export interface EventUpdatePayload {
|
|||||||
description?: string;
|
description?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
recurrence?: string;
|
recurrence?: string | null;
|
||||||
project_id?: number;
|
project_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,36 @@ const description = ref("");
|
|||||||
const location = ref("");
|
const location = ref("");
|
||||||
const color = ref("");
|
const color = ref("");
|
||||||
const projectId = ref<number | null>(null);
|
const projectId = ref<number | null>(null);
|
||||||
|
const recurrence = ref<string>("");
|
||||||
|
|
||||||
|
// 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<string, string> = {
|
||||||
|
none: "",
|
||||||
|
daily: "FREQ=DAILY",
|
||||||
|
weekly: "FREQ=WEEKLY",
|
||||||
|
monthly: "FREQ=MONTHLY",
|
||||||
|
yearly: "FREQ=YEARLY",
|
||||||
|
};
|
||||||
|
|
||||||
|
const recurrencePreset = computed<string>({
|
||||||
|
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 {
|
function dateFromIso(iso: string): string {
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
@@ -115,6 +145,7 @@ function resetForm() {
|
|||||||
location.value = props.event.location || "";
|
location.value = props.event.location || "";
|
||||||
color.value = props.event.color || "";
|
color.value = props.event.color || "";
|
||||||
projectId.value = props.event.project_id;
|
projectId.value = props.event.project_id;
|
||||||
|
recurrence.value = props.event.recurrence || "";
|
||||||
_lastDurationMin = !allDay.value && startTime.value && endTime.value ? durationMin(startTime.value, endTime.value) : 60;
|
_lastDurationMin = !allDay.value && startTime.value && endTime.value ? durationMin(startTime.value, endTime.value) : 60;
|
||||||
if (_lastDurationMin <= 0) _lastDurationMin = 60;
|
if (_lastDurationMin <= 0) _lastDurationMin = 60;
|
||||||
} else {
|
} else {
|
||||||
@@ -130,6 +161,7 @@ function resetForm() {
|
|||||||
location.value = "";
|
location.value = "";
|
||||||
color.value = "";
|
color.value = "";
|
||||||
projectId.value = null;
|
projectId.value = null;
|
||||||
|
recurrence.value = "";
|
||||||
_lastDurationMin = 60;
|
_lastDurationMin = 60;
|
||||||
}
|
}
|
||||||
deleteConfirm.value = false;
|
deleteConfirm.value = false;
|
||||||
@@ -257,6 +289,7 @@ async function save() {
|
|||||||
location: location.value,
|
location: location.value,
|
||||||
color: color.value,
|
color: color.value,
|
||||||
project_id: projectId.value ?? undefined,
|
project_id: projectId.value ?? undefined,
|
||||||
|
recurrence: recurrence.value || null,
|
||||||
};
|
};
|
||||||
const updated = await updateEvent(props.event.id, payload);
|
const updated = await updateEvent(props.event.id, payload);
|
||||||
emit("updated", updated);
|
emit("updated", updated);
|
||||||
@@ -270,6 +303,7 @@ async function save() {
|
|||||||
location: location.value,
|
location: location.value,
|
||||||
color: color.value,
|
color: color.value,
|
||||||
project_id: projectId.value ?? undefined,
|
project_id: projectId.value ?? undefined,
|
||||||
|
recurrence: recurrence.value || undefined,
|
||||||
};
|
};
|
||||||
const created = await createEvent(payload);
|
const created = await createEvent(payload);
|
||||||
emit("created", created);
|
emit("created", created);
|
||||||
@@ -374,6 +408,24 @@ async function doDelete() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recurrence -->
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label">Repeat</label>
|
||||||
|
<select v-model="recurrencePreset" class="form-input">
|
||||||
|
<option value="none">Does not repeat</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
<option value="yearly">Yearly</option>
|
||||||
|
<option v-if="isCustomRecurrence" value="custom" disabled>Custom</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="isCustomRecurrence" class="recurrence-custom-hint">
|
||||||
|
Custom rule: <code>{{ recurrence }}</code>
|
||||||
|
<br />
|
||||||
|
<span class="form-hint">Picking a preset will replace this rule.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Location <span class="form-hint">(optional)</span></label>
|
<label class="form-label">Location <span class="form-hint">(optional)</span></label>
|
||||||
@@ -539,6 +591,22 @@ async function doDelete() {
|
|||||||
color: var(--color-text-secondary);
|
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 {
|
.toggle-btn {
|
||||||
background: var(--color-input-bg, #111113);
|
background: var(--color-input-bg, #111113);
|
||||||
border: 1px solid var(--color-border, #2a2b30);
|
border: 1px solid var(--color-border, #2a2b30);
|
||||||
|
|||||||
Reference in New Issue
Block a user