chore: clear pre-commit eslint warnings (unused symbols, stale disables, test any)

Cleans up the lint warnings the pre-commit hook surfaces, without any
behavioral change:

- Remove unused imports/vars/destructured props (parseISO, useEffect,
  format, durMin, roles, daysInYear, ALLOWED_PLUGIN_FILES, continuesBefore,
  isPushConnected, isSelected) and the now-unused parseDuration import.
- Drop three stale `// eslint-disable-next-line no-undef` directives that
  no longer suppress anything (browser-navigation, smime/crypto-engine).
- recurrence-expansion.test.ts: replace 39 `as any` casts with a cast-only
  `rule()` helper for partial recurrence-rule fixtures, typed access to
  utcStart/utcEnd (now on CalendarEvent), and the source's
  `Partial<CalendarEvent> & { excluded?: boolean }` for the excluded
  override. No defaults are injected, so the expansion logic sees the same
  partial rules as before (35 tests still green).

Remaining: 7 react-hooks/exhaustive-deps warnings are left as-is — adding
the missing deps changes effect/memo timing and needs per-hook review, not
a mechanical fix. tsc --noEmit clean; eslint 0 errors / 7 warnings.
This commit is contained in:
Stefan Hildebrandt
2026-06-19 12:31:07 +02:00
committed by Linus Rath
parent 344795a8d9
commit 0b7203df0f
15 changed files with 66 additions and 66 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ import {
invalidateFrameOriginsCache, invalidateFrameOriginsCache,
} from '@/lib/admin/csp-frame-origins'; } from '@/lib/admin/csp-frame-origins';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { MAX_PLUGIN_SIZE, MAX_THEME_SIZE, ALL_PERMISSIONS, ALLOWED_PLUGIN_FILES } from '@/lib/plugin-types'; import { MAX_PLUGIN_SIZE, MAX_THEME_SIZE, ALL_PERMISSIONS } from '@/lib/plugin-types';
import { sanitizeThemeCSS, validateThemeCSSSafety } from '@/lib/theme-loader'; import { sanitizeThemeCSS, validateThemeCSSSafety } from '@/lib/theme-loader';
import { configManager } from '@/lib/admin/config-manager'; import { configManager } from '@/lib/admin/config-manager';
+1 -2
View File
@@ -5,7 +5,7 @@ import { useTranslations, useFormatter } from "next-intl";
import { format, isToday, isTomorrow, startOfDay } from "date-fns"; import { format, isToday, isTomorrow, startOfDay } from "date-fns";
import { MapPin, Users } from "lucide-react"; import { MapPin, Users } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { parseDuration, getEventColor } from "./event-card"; import { getEventColor } from "./event-card";
import { getEventDayBounds, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils"; import { getEventDayBounds, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
import { getParticipantCount } from "@/lib/calendar-participants"; import { getParticipantCount } from "@/lib/calendar-participants";
import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
@@ -147,7 +147,6 @@ export function CalendarAgendaView({
const calendar = calId ? calendarMap.get(calId) : undefined; const calendar = calId ? calendarMap.get(calId) : undefined;
const color = getEventColor(ev, calendar); const color = getEventColor(ev, calendar);
const start = getEventStartDate(ev); const start = getEventStartDate(ev);
const durMin = parseDuration(ev.duration);
const end = getEventEndDate(ev); const end = getEventEndDate(ev);
const locationName = ev.locations const locationName = ev.locations
? Object.values(ev.locations)[0]?.name ? Object.values(ev.locations)[0]?.name
+1 -1
View File
@@ -71,7 +71,7 @@ function createEventDragPreview(title: string, timeRange: string, color: string)
return el; return el;
} }
export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onMouseLeave, onContextMenu, isSelected, draggable: isDraggable, continuesBefore = false, continuesAfter = false, className, style }: EventCardProps) { export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onMouseLeave, onContextMenu, isSelected, draggable: isDraggable, continuesAfter = false, className, style }: EventCardProps) {
const t = useTranslations("calendar"); const t = useTranslations("calendar");
const [isBeingDragged, setIsBeingDragged] = useState(false); const [isBeingDragged, setIsBeingDragged] = useState(false);
const color = getEventColor(event, calendar); const color = getEventColor(event, calendar);
+1 -1
View File
@@ -8,7 +8,7 @@ import {
X, Clock, MapPin, Video, Users, Repeat, Bell, AlignLeft, X, Clock, MapPin, Video, Users, Repeat, Bell, AlignLeft,
Pencil, Trash2, Copy, Send, Check, Pencil, Trash2, Copy, Send, Check,
} from "lucide-react"; } from "lucide-react";
import { format, isSameDay, parseISO } from "date-fns"; import { format, isSameDay } from "date-fns";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types"; import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
import { parseDuration, getEventColor } from "./event-card"; import { parseDuration, getEventColor } from "./event-card";
-1
View File
@@ -617,7 +617,6 @@ export function EventModal({
if (isAttendeeMode && event) { if (isAttendeeMode && event) {
const startD = getEventStartDate(event); const startD = getEventStartDate(event);
const durMin = parseDuration(event.duration);
const endD = getEventEndDate(event); const endD = getEventEndDate(event);
const locationName = event.locations ? Object.values(event.locations)[0]?.name : null; const locationName = event.locations ? Object.values(event.locations)[0]?.name : null;
const participants = getParticipantList(event); const participants = getParticipantList(event);
+1 -1
View File
@@ -4,7 +4,7 @@ import { useState, useCallback, useRef, useEffect } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { X, Upload, Check, Loader2, RefreshCw, Globe } from "lucide-react"; import { X, Upload, Check, Loader2, RefreshCw, Globe } from "lucide-react";
import { format, parseISO } from "date-fns"; import { format } from "date-fns";
import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
import type { IJMAPClient } from '@/lib/jmap/client-interface'; import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { getEventStartDate } from "@/lib/calendar-utils"; import { getEventStartDate } from "@/lib/calendar-utils";
-1
View File
@@ -203,7 +203,6 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
const notes = contact.notes ? Object.values(contact.notes) : []; const notes = contact.notes ? Object.values(contact.notes) : [];
const titles = contact.titles ? Object.values(contact.titles) : []; const titles = contact.titles ? Object.values(contact.titles) : [];
const jobTitles = titles.filter(t => t.kind !== "role"); const jobTitles = titles.filter(t => t.kind !== "role");
const roles = titles.filter(t => t.kind === "role");
const onlineServices = contact.onlineServices ? Object.values(contact.onlineServices) : []; const onlineServices = contact.onlineServices ? Object.values(contact.onlineServices) : [];
const anniversaries = contact.anniversaries ? Object.values(contact.anniversaries) : []; const anniversaries = contact.anniversaries ? Object.values(contact.anniversaries) : [];
const keywords = contact.keywords ? Object.keys(contact.keywords).filter(k => contact.keywords![k]) : []; const keywords = contact.keywords ? Object.keys(contact.keywords).filter(k => contact.keywords![k]) : [];
+1 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useMemo, useCallback, useEffect, useRef } from "react"; import { useState, useMemo, useCallback, useRef } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { X, Plus, ChevronDown, ChevronRight, User, Building, MapPin, Globe, Cake, Heart, Tag, StickyNote, Mail, Phone, Calendar, UserCircle, Book, Camera, Trash2 } from "lucide-react"; import { X, Plus, ChevronDown, ChevronRight, User, Building, MapPin, Globe, Cake, Heart, Tag, StickyNote, Mail, Phone, Calendar, UserCircle, Book, Camera, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
-1
View File
@@ -166,7 +166,6 @@ export function NavigationRail({
collapsed = false, collapsed = false,
className, className,
quota, quota,
isPushConnected,
onLogout, onLogout,
onShowShortcuts, onShowShortcuts,
onManageApps, onManageApps,
-1
View File
@@ -169,7 +169,6 @@ function getIconClass(isSelected: boolean, isVirtual: boolean, colorful: boolean
function SidebarRowCounts({ function SidebarRowCounts({
unread, unread,
total, total,
isSelected,
onUnreadClick, onUnreadClick,
}: { }: {
unread?: number; unread?: number;
+59 -50
View File
@@ -1,6 +1,15 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { expandRecurringEvents } from '../recurrence-expansion'; import { expandRecurringEvents } from '../recurrence-expansion';
import type { CalendarEvent } from '@/lib/jmap/types'; import type { CalendarEvent, CalendarRecurrenceRule } from '@/lib/jmap/types';
/**
* Helper: builds a recurrence-rule fixture from only the fields a test cares
* about. Cast-only (no defaults injected) so the expansion logic sees exactly
* the same partial rule the tests previously passed via `as any`.
*/
function rule(partial: Partial<CalendarRecurrenceRule>): CalendarRecurrenceRule {
return partial as CalendarRecurrenceRule;
}
/** Helper: create a minimal CalendarEvent for testing recurrence */ /** Helper: create a minimal CalendarEvent for testing recurrence */
function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent { function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
@@ -41,7 +50,7 @@ describe('expandRecurringEvents', () => {
describe('daily frequency', () => { describe('daily frequency', () => {
it('expands daily events within range', () => { it('expands daily events within range', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-09T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-09T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -53,7 +62,7 @@ describe('expandRecurringEvents', () => {
it('respects interval', () => { it('respects interval', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily', interval: 2 } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily', interval: 2 })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-12T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-12T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -65,7 +74,7 @@ describe('expandRecurringEvents', () => {
it('respects count', () => { it('respects count', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily', count: 3 } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily', count: 3 })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-12-31T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-12-31T00:00:00');
expect(result).toHaveLength(3); expect(result).toHaveLength(3);
@@ -73,7 +82,7 @@ describe('expandRecurringEvents', () => {
it('respects until', () => { it('respects until', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily', until: '2025-01-08T09:00:00' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily', until: '2025-01-08T09:00:00' })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-12-31T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-12-31T00:00:00');
expect(result).toHaveLength(3); expect(result).toHaveLength(3);
@@ -86,7 +95,7 @@ describe('expandRecurringEvents', () => {
describe('weekly frequency', () => { describe('weekly frequency', () => {
it('expands weekly with implicit byDay (same weekday as start)', () => { it('expands weekly with implicit byDay (same weekday as start)', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
}); });
// Jan 6 is Monday, so every Monday // Jan 6 is Monday, so every Monday
const result = expand(event, '2025-01-06T00:00:00', '2025-01-28T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-28T00:00:00');
@@ -100,11 +109,11 @@ describe('expandRecurringEvents', () => {
it('expands weekly with explicit byDay (MWF)', () => { it('expands weekly with explicit byDay (MWF)', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'weekly', frequency: 'weekly',
byDay: [{ day: 'mo' }, { day: 'we' }, { day: 'fr' }], byDay: [{ day: 'mo' }, { day: 'we' }, { day: 'fr' }],
} as any], })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-13T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-13T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -116,12 +125,12 @@ describe('expandRecurringEvents', () => {
it('expands weekly with interval=2', () => { it('expands weekly with interval=2', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'weekly', frequency: 'weekly',
interval: 2, interval: 2,
byDay: [{ day: 'mo' }], byDay: [{ day: 'mo' }],
} as any], })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-02-03T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-02-03T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -138,7 +147,7 @@ describe('expandRecurringEvents', () => {
it('expands monthly with implicit byMonthDay', () => { it('expands monthly with implicit byMonthDay', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-15T10:00:00', start: '2025-01-15T10:00:00',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'monthly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'monthly' })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -151,11 +160,11 @@ describe('expandRecurringEvents', () => {
it('expands monthly with byMonthDay', () => { it('expands monthly with byMonthDay', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-01T08:00:00', start: '2025-01-01T08:00:00',
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'monthly', frequency: 'monthly',
byMonthDay: [1, 15], byMonthDay: [1, 15],
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-02-28T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-02-28T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -169,11 +178,11 @@ describe('expandRecurringEvents', () => {
it('expands monthly with negative byMonthDay (-1 = last day)', () => { it('expands monthly with negative byMonthDay (-1 = last day)', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-31T08:00:00', start: '2025-01-31T08:00:00',
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'monthly', frequency: 'monthly',
byMonthDay: [-1], byMonthDay: [-1],
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00');
const days = result.map(e => e.start.substring(0, 10)); const days = result.map(e => e.start.substring(0, 10));
@@ -183,11 +192,11 @@ describe('expandRecurringEvents', () => {
it('expands monthly with byDay + nthOfPeriod (2nd Tuesday)', () => { it('expands monthly with byDay + nthOfPeriod (2nd Tuesday)', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-14T09:00:00', // 2nd Tuesday start: '2025-01-14T09:00:00', // 2nd Tuesday
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'monthly', frequency: 'monthly',
byDay: [{ day: 'tu', nthOfPeriod: 2 }], byDay: [{ day: 'tu', nthOfPeriod: 2 }],
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00');
const days = result.map(e => e.start.substring(0, 10)); const days = result.map(e => e.start.substring(0, 10));
@@ -197,11 +206,11 @@ describe('expandRecurringEvents', () => {
it('expands monthly with byDay nthOfPeriod=-1 (last Friday)', () => { it('expands monthly with byDay nthOfPeriod=-1 (last Friday)', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-31T09:00:00', // last Friday of Jan start: '2025-01-31T09:00:00', // last Friday of Jan
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'monthly', frequency: 'monthly',
byDay: [{ day: 'fr', nthOfPeriod: -1 }], byDay: [{ day: 'fr', nthOfPeriod: -1 }],
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-04-01T00:00:00');
const days = result.map(e => e.start.substring(0, 10)); const days = result.map(e => e.start.substring(0, 10));
@@ -216,7 +225,7 @@ describe('expandRecurringEvents', () => {
it('expands yearly on the same date', () => { it('expands yearly on the same date', () => {
const event = makeEvent({ const event = makeEvent({
start: '2023-03-15T12:00:00', start: '2023-03-15T12:00:00',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'yearly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'yearly' })],
}); });
const result = expand(event, '2023-01-01T00:00:00', '2026-01-01T00:00:00'); const result = expand(event, '2023-01-01T00:00:00', '2026-01-01T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -229,12 +238,12 @@ describe('expandRecurringEvents', () => {
it('expands yearly with byMonth and byDay (last Friday of November = Thanksgiving-ish)', () => { it('expands yearly with byMonth and byDay (last Friday of November = Thanksgiving-ish)', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-11-28T09:00:00', // last Friday of Nov 2025 start: '2025-11-28T09:00:00', // last Friday of Nov 2025
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'yearly', frequency: 'yearly',
byMonth: ['11'], byMonth: ['11'],
byDay: [{ day: 'fr', nthOfPeriod: -1 }], byDay: [{ day: 'fr', nthOfPeriod: -1 }],
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2028-01-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2028-01-01T00:00:00');
const days = result.map(e => e.start.substring(0, 10)); const days = result.map(e => e.start.substring(0, 10));
@@ -246,12 +255,12 @@ describe('expandRecurringEvents', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-07-04T00:00:00', start: '2025-07-04T00:00:00',
showWithoutTime: true, showWithoutTime: true,
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'yearly', frequency: 'yearly',
byMonth: ['7'], byMonth: ['7'],
byMonthDay: [4], byMonthDay: [4],
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2028-01-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2028-01-01T00:00:00');
expect(result).toHaveLength(3); expect(result).toHaveLength(3);
@@ -265,12 +274,12 @@ describe('expandRecurringEvents', () => {
it('selects first and last from monthly byDay expansion', () => { it('selects first and last from monthly byDay expansion', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-06T10:00:00', start: '2025-01-06T10:00:00',
recurrenceRules: [{ recurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'monthly', frequency: 'monthly',
byDay: [{ day: 'mo' }, { day: 'tu' }, { day: 'we' }, { day: 'th' }, { day: 'fr' }], byDay: [{ day: 'mo' }, { day: 'tu' }, { day: 'we' }, { day: 'th' }, { day: 'fr' }],
bySetPosition: [1, -1], // first and last weekday of month bySetPosition: [1, -1], // first and last weekday of month
} as any], })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-03-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-03-01T00:00:00');
const days = result.map(e => e.start.substring(0, 10)); const days = result.map(e => e.start.substring(0, 10));
@@ -289,7 +298,7 @@ describe('expandRecurringEvents', () => {
describe('recurrenceOverrides', () => { describe('recurrenceOverrides', () => {
it('applies overrides to matching occurrences', () => { it('applies overrides to matching occurrences', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
recurrenceOverrides: { recurrenceOverrides: {
'2025-01-07T09:00:00': { title: 'Modified' }, '2025-01-07T09:00:00': { title: 'Modified' },
}, },
@@ -301,9 +310,9 @@ describe('expandRecurringEvents', () => {
it('excludes occurrences marked as excluded', () => { it('excludes occurrences marked as excluded', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
recurrenceOverrides: { recurrenceOverrides: {
'2025-01-07T09:00:00': { excluded: true } as any, '2025-01-07T09:00:00': { excluded: true } as Partial<CalendarEvent> & { excluded?: boolean },
}, },
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-09T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-09T00:00:00');
@@ -313,7 +322,7 @@ describe('expandRecurringEvents', () => {
it('adds RDATE-style overrides not generated by rules', () => { it('adds RDATE-style overrides not generated by rules', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
recurrenceOverrides: { recurrenceOverrides: {
'2025-01-08T09:00:00': { title: 'Extra Wednesday' }, // Not a Monday '2025-01-08T09:00:00': { title: 'Extra Wednesday' }, // Not a Monday
}, },
@@ -329,12 +338,12 @@ describe('expandRecurringEvents', () => {
describe('excludedRecurrenceRules', () => { describe('excludedRecurrenceRules', () => {
it('removes occurrences generated by excluded rules', () => { it('removes occurrences generated by excluded rules', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
excludedRecurrenceRules: [{ excludedRecurrenceRules: [rule({
'@type': 'RecurrenceRule', '@type': 'RecurrenceRule',
frequency: 'weekly', frequency: 'weekly',
byDay: [{ day: 'tu' }], byDay: [{ day: 'tu' }],
} as any], })],
}); });
// Jan 6-12 2025: Mon-Sun, Tuesday Jan 7 excluded // Jan 6-12 2025: Mon-Sun, Tuesday Jan 7 excluded
const result = expand(event, '2025-01-06T00:00:00', '2025-01-13T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-13T00:00:00');
@@ -353,7 +362,7 @@ describe('expandRecurringEvents', () => {
it('expands a daily series started years before the visible range', () => { it('expands a daily series started years before the visible range', () => {
const event = makeEvent({ const event = makeEvent({
start: '2022-01-03T09:00:00', start: '2022-01-03T09:00:00',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-06-02T00:00:00', '2025-06-05T00:00:00'); const result = expand(event, '2025-06-02T00:00:00', '2025-06-05T00:00:00');
expect(starts(result)).toEqual([ expect(starts(result)).toEqual([
@@ -366,7 +375,7 @@ describe('expandRecurringEvents', () => {
it('expands a weekly series started years before the visible range', () => { it('expands a weekly series started years before the visible range', () => {
const event = makeEvent({ const event = makeEvent({
start: '2020-01-06T09:00:00', // Monday start: '2020-01-06T09:00:00', // Monday
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
}); });
const result = expand(event, '2025-06-01T00:00:00', '2025-06-30T00:00:00'); const result = expand(event, '2025-06-01T00:00:00', '2025-06-30T00:00:00');
const days = result.map(e => e.start.substring(0, 10)); const days = result.map(e => e.start.substring(0, 10));
@@ -376,7 +385,7 @@ describe('expandRecurringEvents', () => {
it('still respects count for old series (no fast-forward shortcut)', () => { it('still respects count for old series (no fast-forward shortcut)', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-06T09:00:00', start: '2025-01-06T09:00:00',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily', count: 5 } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily', count: 5 })],
}); });
// Range far after the 5 occurrences ran out // Range far after the 5 occurrences ran out
const result = expand(event, '2025-06-01T00:00:00', '2025-06-30T00:00:00'); const result = expand(event, '2025-06-01T00:00:00', '2025-06-30T00:00:00');
@@ -395,13 +404,13 @@ describe('expandRecurringEvents', () => {
timeZone: 'America/New_York', timeZone: 'America/New_York',
utcStart: '2025-03-03T15:00:00Z', utcStart: '2025-03-03T15:00:00Z',
utcEnd: '2025-03-03T16:00:00Z', utcEnd: '2025-03-03T16:00:00Z',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
} as Partial<CalendarEvent>); } as Partial<CalendarEvent>);
const result = expand(event, '2025-03-03T00:00:00', '2025-03-17T00:00:00'); const result = expand(event, '2025-03-03T00:00:00', '2025-03-17T00:00:00');
const utcStarts = result.map(e => (e as any).utcStart); const utcStarts = result.map(e => e.utcStart);
expect(utcStarts[0]).toBe('2025-03-03T15:00:00.000Z'); // EST: 10:00 -5 expect(utcStarts[0]).toBe('2025-03-03T15:00:00.000Z'); // EST: 10:00 -5
expect(utcStarts[1]).toBe('2025-03-10T14:00:00.000Z'); // EDT: 10:00 -4 expect(utcStarts[1]).toBe('2025-03-10T14:00:00.000Z'); // EDT: 10:00 -4
const utcEnds = result.map(e => (e as any).utcEnd); const utcEnds = result.map(e => e.utcEnd);
expect(utcEnds[1]).toBe('2025-03-10T15:00:00.000Z'); expect(utcEnds[1]).toBe('2025-03-10T15:00:00.000Z');
}); });
}); });
@@ -414,7 +423,7 @@ describe('expandRecurringEvents', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-06T00:00:00', start: '2025-01-06T00:00:00',
showWithoutTime: true, showWithoutTime: true,
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-28T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-28T00:00:00');
expect(result).toHaveLength(4); // 4 Mondays: 6, 13, 20, 27 expect(result).toHaveLength(4); // 4 Mondays: 6, 13, 20, 27
@@ -427,7 +436,7 @@ describe('expandRecurringEvents', () => {
describe('edge cases', () => { describe('edge cases', () => {
it('does not exceed 500 occurrences', () => { it('does not exceed 500 occurrences', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2030-01-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2030-01-01T00:00:00');
expect(result.length).toBeLessThanOrEqual(500); expect(result.length).toBeLessThanOrEqual(500);
@@ -436,7 +445,7 @@ describe('expandRecurringEvents', () => {
it('handles invalid start date gracefully', () => { it('handles invalid start date gracefully', () => {
const event = makeEvent({ const event = makeEvent({
start: 'invalid', start: 'invalid',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-01-01T00:00:00', '2025-02-01T00:00:00'); const result = expand(event, '2025-01-01T00:00:00', '2025-02-01T00:00:00');
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
@@ -444,7 +453,7 @@ describe('expandRecurringEvents', () => {
it('generates synthetic IDs for occurrences', () => { it('generates synthetic IDs for occurrences', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-08T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-08T00:00:00');
expect(result[0].id).toBe('evt1:2025-01-06T09:00:00'); expect(result[0].id).toBe('evt1:2025-01-06T09:00:00');
@@ -453,7 +462,7 @@ describe('expandRecurringEvents', () => {
it('preserves originalId pointing to master', () => { it('preserves originalId pointing to master', () => {
const event = makeEvent({ const event = makeEvent({
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-08T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-08T00:00:00');
expect(result[0].originalId).toBe('evt1'); expect(result[0].originalId).toBe('evt1');
@@ -465,7 +474,7 @@ describe('expandRecurringEvents', () => {
id: 'master1', id: 'master1',
uid: 'shared-uid', uid: 'shared-uid',
start: '2025-01-06T09:00:00', start: '2025-01-06T09:00:00',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
}); });
const override1 = makeEvent({ const override1 = makeEvent({
id: 'override1', id: 'override1',
@@ -527,10 +536,10 @@ describe('expandRecurringEvents', () => {
const event = makeEvent({ const event = makeEvent({
start: '2026-09-01T12:00:00', start: '2026-09-01T12:00:00',
utcStart: '2026-09-01T10:00:00Z', utcStart: '2026-09-01T10:00:00Z',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
}); });
const result = expand(event, '2026-09-01T00:00:00', '2026-10-01T00:00:00'); const result = expand(event, '2026-09-01T00:00:00', '2026-10-01T00:00:00');
const utcStarts = result.map(e => (e as any).utcStart); const utcStarts = result.map(e => e.utcStart);
// Each occurrence should have a unique utcStart // Each occurrence should have a unique utcStart
expect(new Set(utcStarts).size).toBe(result.length); expect(new Set(utcStarts).size).toBe(result.length);
// First occurrence keeps master's UTC offset relationship // First occurrence keeps master's UTC offset relationship
@@ -545,10 +554,10 @@ describe('expandRecurringEvents', () => {
duration: 'PT1H', duration: 'PT1H',
utcStart: '2026-09-01T10:00:00Z', utcStart: '2026-09-01T10:00:00Z',
utcEnd: '2026-09-01T11:00:00Z', utcEnd: '2026-09-01T11:00:00Z',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'weekly' })],
}); });
const result = expand(event, '2026-09-01T00:00:00', '2026-10-01T00:00:00'); const result = expand(event, '2026-09-01T00:00:00', '2026-10-01T00:00:00');
const utcEnds = result.map(e => (e as any).utcEnd); const utcEnds = result.map(e => e.utcEnd);
// Each occurrence should have a unique utcEnd // Each occurrence should have a unique utcEnd
expect(new Set(utcEnds).size).toBe(result.length); expect(new Set(utcEnds).size).toBe(result.length);
expect(utcEnds[0]).toContain('2026-09-01'); expect(utcEnds[0]).toContain('2026-09-01');
@@ -559,7 +568,7 @@ describe('expandRecurringEvents', () => {
it('does not set utcStart when master has none', () => { it('does not set utcStart when master has none', () => {
const event = makeEvent({ const event = makeEvent({
start: '2025-01-06T09:00:00', start: '2025-01-06T09:00:00',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' } as any], recurrenceRules: [rule({ '@type': 'RecurrenceRule', frequency: 'daily' })],
}); });
const result = expand(event, '2025-01-06T00:00:00', '2025-01-08T00:00:00'); const result = expand(event, '2025-01-06T00:00:00', '2025-01-08T00:00:00');
expect(result[0].utcStart).toBeUndefined(); expect(result[0].utcStart).toBeUndefined();
+1 -1
View File
@@ -1,6 +1,6 @@
import type { ContactCard, CalendarEvent, Calendar, PartialDate, Timestamp } from '@/lib/jmap/types'; import type { ContactCard, CalendarEvent, Calendar, PartialDate, Timestamp } from '@/lib/jmap/types';
import { getContactDisplayName } from '@/stores/contact-store'; import { getContactDisplayName } from '@/stores/contact-store';
import { format, eachYearOfInterval, parseISO } from 'date-fns'; import { eachYearOfInterval, parseISO } from 'date-fns';
export const BIRTHDAY_CALENDAR_ID = '__birthday-calendar__'; export const BIRTHDAY_CALENDAR_ID = '__birthday-calendar__';
export const BIRTHDAY_CALENDAR_COLOR = '#eab308'; // Yellow export const BIRTHDAY_CALENDAR_COLOR = '#eab308'; // Yellow
-1
View File
@@ -67,7 +67,6 @@ export function getPathPrefix(locale?: string): string {
* // Browser at /webmail/en/inbox → /webmail/api/jmap * // Browser at /webmail/en/inbox → /webmail/api/jmap
* // Browser at /en/inbox → /api/jmap * // Browser at /en/inbox → /api/jmap
*/ */
// eslint-disable-next-line no-undef
export function apiFetch(input: string, init?: RequestInit): Promise<Response> { export function apiFetch(input: string, init?: RequestInit): Promise<Response> {
if (input.startsWith('/') && !input.startsWith('//')) { if (input.startsWith('/') && !input.startsWith('//')) {
return fetch(getPathPrefix() + input, init); return fetch(getPathPrefix() + input, init);
-1
View File
@@ -784,7 +784,6 @@ function nthWeekdayInYear(date: Date, nth: number): number {
const firstYd = getDayOfYear(first); const firstYd = getDayOfYear(first);
return Math.floor((yd - firstYd) / 7) + 1; return Math.floor((yd - firstYd) / 7) + 1;
} else { } else {
const daysInYear = isLeapYear(date.getFullYear()) ? 366 : 365;
const dec31 = new Date(date.getFullYear(), 11, 31); const dec31 = new Date(date.getFullYear(), 11, 31);
let last = dec31; let last = dec31;
while (last.getDay() !== weekday) last = addDays(last, -1); while (last.getDay() !== weekday) last = addDays(last, -1);
-2
View File
@@ -225,7 +225,6 @@ class Pkcs12CryptoEngine extends pkijs.CryptoEngine {
const cryptoKey = await this.importKey( const cryptoKey = await this.importKey(
'raw', 'raw',
keyData, keyData,
// eslint-disable-next-line no-undef
{ name: algName, length: keyLen * 8 } as Algorithm, { name: algName, length: keyLen * 8 } as Algorithm,
false, false,
['decrypt'], ['decrypt'],
@@ -234,7 +233,6 @@ class Pkcs12CryptoEngine extends pkijs.CryptoEngine {
// Decrypt // Decrypt
const ciphertext = parameters.encryptedContentInfo.getEncryptedContent(); const ciphertext = parameters.encryptedContentInfo.getEncryptedContent();
return this.decrypt( return this.decrypt(
// eslint-disable-next-line no-undef
{ name: algName, iv: ivBytes } as Algorithm, { name: algName, iv: ivBytes } as Algorithm,
cryptoKey, cryptoKey,
ciphertext, ciphertext,