mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
Merge branch 'main' of https://github.com/bulwarkmail/webmail
This commit is contained in:
@@ -11,6 +11,7 @@ import { RecurrenceEditor, buildRecurrenceSummary, isSimpleRecurrenceRule } from
|
||||
import { parseDuration, getEventColor } from "./event-card";
|
||||
import { buildAllDayDuration, getEventDisplayEndDate, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
|
||||
import { displayNow, getEffectiveTimeZone } from "@/lib/timezone";
|
||||
import { createRecurrenceRule } from "@/lib/recurrence-rule";
|
||||
import { ParticipantInput, type ParticipantInputHandle } from "./participant-input";
|
||||
import {
|
||||
isOrganizer,
|
||||
@@ -543,25 +544,7 @@ export function EventModal({
|
||||
if (recurrence === "custom" && customRule) {
|
||||
data.recurrenceRules = [customRule];
|
||||
} else if (recurrence !== "none" && recurrence !== "custom") {
|
||||
data.recurrenceRules = [{
|
||||
"@type": "RecurrenceRule",
|
||||
frequency: recurrence,
|
||||
interval: 1,
|
||||
rscale: "gregorian",
|
||||
skip: "omit",
|
||||
firstDayOfWeek: "mo",
|
||||
byDay: null,
|
||||
byMonthDay: null,
|
||||
byMonth: null,
|
||||
byYearDay: null,
|
||||
byWeekNo: null,
|
||||
byHour: null,
|
||||
byMinute: null,
|
||||
bySecond: null,
|
||||
bySetPosition: null,
|
||||
count: null,
|
||||
until: null,
|
||||
}];
|
||||
data.recurrenceRules = [createRecurrenceRule(recurrence)];
|
||||
} else if (event && event.recurrenceRules?.length) {
|
||||
data.recurrenceRules = null;
|
||||
if (event.recurrenceOverrides) data.recurrenceOverrides = null;
|
||||
@@ -1383,9 +1366,9 @@ export function EventModal({
|
||||
>
|
||||
{t("events.delete")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
className="w-full"
|
||||
>
|
||||
@@ -1417,10 +1400,10 @@ export function EventModal({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{!showDeleteConfirm && (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={isEdit ? () => setMode("view") : onClose}
|
||||
className="w-full"
|
||||
@@ -1428,7 +1411,7 @@ export function EventModal({
|
||||
{t("form.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
onClick={handleSave}
|
||||
disabled={!title.trim() || isSaving}
|
||||
className="w-full"
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { addYears, format } from "date-fns";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { CalendarRecurrenceRule } from "@/lib/jmap/types";
|
||||
import { createRecurrenceRule } from "@/lib/recurrence-rule";
|
||||
|
||||
type EditorFrequency = "daily" | "weekly" | "monthly" | "yearly";
|
||||
type MonthlyMode = "day" | "nth";
|
||||
@@ -210,25 +211,11 @@ export function RecurrenceEditor({ rule, eventStart, onSave, onCancel }: Recurre
|
||||
};
|
||||
|
||||
const buildRule = (): CalendarRecurrenceRule => {
|
||||
const built: CalendarRecurrenceRule = {
|
||||
"@type": "RecurrenceRule",
|
||||
frequency,
|
||||
const built: CalendarRecurrenceRule = createRecurrenceRule(frequency, {
|
||||
interval: Math.max(1, interval),
|
||||
rscale: "gregorian",
|
||||
skip: "omit",
|
||||
firstDayOfWeek: "mo",
|
||||
byDay: null,
|
||||
byMonthDay: null,
|
||||
byMonth: null,
|
||||
byYearDay: null,
|
||||
byWeekNo: null,
|
||||
byHour: null,
|
||||
byMinute: null,
|
||||
bySecond: null,
|
||||
bySetPosition: null,
|
||||
count: endsMode === "after" ? Math.max(1, count) : null,
|
||||
until: endsMode === "on" && untilDate ? `${untilDate}T23:59:59` : null,
|
||||
};
|
||||
});
|
||||
|
||||
if (frequency === "weekly") {
|
||||
const days = weekDays.length ? weekDays : [startDay];
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createRecurrenceRule } from '@/lib/recurrence-rule';
|
||||
|
||||
describe('createRecurrenceRule (#805 — no default SKIP=OMIT)', () => {
|
||||
it('does not emit rscale or skip (which serialise to RSCALE=GREGORIAN;SKIP=OMIT)', () => {
|
||||
const rule = createRecurrenceRule('yearly');
|
||||
expect('skip' in rule).toBe(false);
|
||||
expect('rscale' in rule).toBe(false);
|
||||
expect(rule.skip).toBeUndefined();
|
||||
expect(rule.rscale).toBeUndefined();
|
||||
});
|
||||
|
||||
it('builds a clean Gregorian default rule', () => {
|
||||
expect(createRecurrenceRule('daily')).toEqual({
|
||||
'@type': 'RecurrenceRule',
|
||||
frequency: 'daily',
|
||||
interval: 1,
|
||||
firstDayOfWeek: 'mo',
|
||||
byDay: null,
|
||||
byMonthDay: null,
|
||||
byMonth: null,
|
||||
byYearDay: null,
|
||||
byWeekNo: null,
|
||||
byHour: null,
|
||||
byMinute: null,
|
||||
bySecond: null,
|
||||
bySetPosition: null,
|
||||
count: null,
|
||||
until: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('applies overrides (interval / count / until) without reintroducing skip', () => {
|
||||
const rule = createRecurrenceRule('yearly', { interval: 2, count: 100 });
|
||||
expect(rule.interval).toBe(2);
|
||||
expect(rule.count).toBe(100);
|
||||
expect(rule.until).toBeNull();
|
||||
expect('skip' in rule).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -161,8 +161,6 @@ export function createDemoCalendarEvents(): CalendarEvent[] {
|
||||
'@type': 'RecurrenceRule',
|
||||
frequency: 'weekly',
|
||||
interval: 1,
|
||||
rscale: 'gregorian',
|
||||
skip: 'omit',
|
||||
firstDayOfWeek: 'mo',
|
||||
byDay: [{ day: 'mo' }],
|
||||
byMonthDay: null,
|
||||
|
||||
+5
-2
@@ -673,8 +673,11 @@ export interface CalendarRecurrenceRule {
|
||||
'@type': 'RecurrenceRule';
|
||||
frequency: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly';
|
||||
interval: number;
|
||||
rscale: string;
|
||||
skip: 'omit' | 'backward' | 'forward';
|
||||
// Optional and normally omitted: these RFC 7529 (RSCALE/SKIP) fields only
|
||||
// apply to non-Gregorian scales / invalid-date handling. Emitting a default
|
||||
// SKIP=OMIT breaks some CalDAV clients (DAVx5) - see lib/recurrence-rule.ts (#805).
|
||||
rscale?: string;
|
||||
skip?: 'omit' | 'backward' | 'forward';
|
||||
firstDayOfWeek: 'mo' | 'tu' | 'we' | 'th' | 'fr' | 'sa' | 'su';
|
||||
byDay: CalendarNDay[] | null;
|
||||
byMonthDay: number[] | null;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { CalendarRecurrenceRule } from '@/lib/jmap/types';
|
||||
|
||||
type Frequency = CalendarRecurrenceRule['frequency'];
|
||||
|
||||
/**
|
||||
* A plain Gregorian recurrence rule with all by-parts unset. `rscale`/`skip`
|
||||
* are intentionally omitted: they only matter for non-Gregorian scales /
|
||||
* invalid-date handling, and defaulting them made Stalwart serialise
|
||||
* `RSCALE=GREGORIAN;SKIP=OMIT` into the RRULE, which DAVx5 rejects (#805).
|
||||
*/
|
||||
export function createRecurrenceRule(
|
||||
frequency: Frequency,
|
||||
overrides: Partial<CalendarRecurrenceRule> = {},
|
||||
): CalendarRecurrenceRule {
|
||||
return {
|
||||
'@type': 'RecurrenceRule',
|
||||
frequency,
|
||||
interval: 1,
|
||||
firstDayOfWeek: 'mo',
|
||||
byDay: null,
|
||||
byMonthDay: null,
|
||||
byMonth: null,
|
||||
byYearDay: null,
|
||||
byWeekNo: null,
|
||||
byHour: null,
|
||||
byMinute: null,
|
||||
bySecond: null,
|
||||
bySetPosition: null,
|
||||
count: null,
|
||||
until: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user