feat: search events in the calendar

This commit is contained in:
Linus Rath
2026-10-29 04:12:48 +01:00
parent 9b87100a15
commit b50270d8f3
8 changed files with 588 additions and 2 deletions
@@ -34,7 +34,7 @@ import {
queryEvents,
isCalendarAccessDenied,
} from '../calendar';
import { useCalendarStore } from '../../stores/calendar-store';
import { searchCalendarEvents, useCalendarStore } from '../../stores/calendar-store';
import { useSettingsStore } from '../../stores/settings-store';
import { saveOccurrenceEdit, deleteOccurrence, type SeriesEdit } from '../../lib/recurrence-scope-edit';
import { editorTimesChanged, editorTimesFromEvent, eventTimeFieldsToSave } from '../../lib/event-editor-times';
@@ -754,6 +754,24 @@ live('live Stalwart calendar', () => {
}, 60_000);
});
describe('event search', () => {
it('finds an event outside the loaded window by a word of its title, and a series by its stored event', async () => {
await connectSingleton();
const word = RUN.replace(/[^a-z0-9]/gi, '');
await createEvent({ '@type': 'Event', title: `${RUN} ${word}single`, start: '2027-12-10T09:00:00', duration: 'PT1H', timeZone: 'Europe/Berlin' } as Partial<CalendarEvent>, calendarId);
await createEvent(seriesData(`${word}series`, '2027-01-04T10:00:00', 4), calendarId);
await useCalendarStore.getState().fetchCalendars();
const single = await searchCalendarEvents([`${word}single`]);
expect(single.map((e) => e.title)).toEqual([`${RUN} ${word}single`]);
const series = await searchCalendarEvents([`${word}series`]);
expect(series).toHaveLength(1);
expect(series[0].recurrenceRules?.[0]?.count).toBe(4);
// Every word has to match.
expect(await searchCalendarEvents([`${word}single`, 'nosuchwordanywhere'])).toEqual([]);
}, 60_000);
});
describe('a zero-length event (#837)', () => {
it('ends at its start, stored without a duration or imported from an .ics', async () => {
await connectSingleton();
+24
View File
@@ -1010,6 +1010,30 @@ export async function findEventsByUid(uid: string, accountId?: string): Promise<
return getEvents(ids, accountId);
}
/**
* The ids of the events a CalendarEvent/query `filter` finds (the event
* search's `text` conditions), latest start first, at most `limit`. Not
* limited to a date window: a series is found by its stored event.
*/
export async function searchEventIds(
filter: Record<string, unknown>,
accountId?: string,
limit = 100,
): Promise<string[]> {
const account = accountId || jmapClient.accountId;
return withCalendarTimeZone(async (timeZone) => {
const args: Record<string, unknown> = {
accountId: account,
filter,
sort: [{ property: 'start', isAscending: false }],
limit,
};
if (timeZone) args.timeZone = timeZone;
const res = await jmapClient.request([['CalendarEvent/query', args, '0']], USING);
return methodResult<{ ids: string[] }>(res).ids ?? [];
});
}
// `sendSchedulingMessages` asks Stalwart to deliver iMIP (RFC 6047) invitation
// / reply / cancellation emails to participants. We pass it whenever an event
// has participants so creating/updating/deleting a meeting notifies attendees.