mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
A calendar holding only tasks (VTODO, e.g. from tasks.org) appeared in the event calendar UI as an empty calendar. Stalwart's Calendar/get exposes no supported-component set, so tasks-only-ness is derived from the calendar's objects (mirroring the existing event/task split): a calendar is tasks-only iff it holds >=1 object and every object is a task. - getTasksOnlyCalendarIds scans an account's objects (CalendarEvent/query + minimal /get), early-exits once every calendar has an event, only marks after a full scan, and fails open on the cap or any error. - getAllCalendars/getCalendars annotate calendars with isTasksOnly. - Such calendars stay in the store for the tasks view (task picker, task-list grouping) but are filtered from the event sidebar and default selection. Verified against Stalwart v0.16.16: the tasks-only calendar is flagged, the default (with an event) is not. Closes #761.
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
/**
|
|
* Helpers for deciding whether a calendar holds only tasks (VTODO) and so
|
|
* should be hidden from the *event* calendar UI (#761).
|
|
*
|
|
* JMAP's Calendar/get on Stalwart exposes no per-calendar supported-component
|
|
* set, so "tasks-only" has to be derived from the objects a calendar contains,
|
|
* mirroring how task objects are already told apart from events elsewhere.
|
|
*/
|
|
|
|
/**
|
|
* A calendar object as seen with the minimal properties we scan. The index
|
|
* signature keeps it tolerant of the other fields the server returns (id, etc.).
|
|
*/
|
|
export interface ScannedCalendarObject {
|
|
'@type'?: string;
|
|
due?: unknown;
|
|
progress?: unknown;
|
|
percentComplete?: unknown;
|
|
calendarIds?: Record<string, boolean> | null;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* Is this calendar object a task rather than an event? Matches an explicit
|
|
* `@type: "Task"`, and CalDAV-created tasks (which may lack the type) by the
|
|
* presence of RFC 8984 §5.2 Task-only keys (`due` / `progress` /
|
|
* `percentComplete`) - a VEVENT never carries those. Mirrors the existing
|
|
* event/task split so classification stays consistent.
|
|
*/
|
|
export function isTaskLikeObject(obj: ScannedCalendarObject): boolean {
|
|
const type = obj['@type'];
|
|
if (typeof type === 'string' && type.toLowerCase() === 'task') return true;
|
|
if (type !== 'Event' && (
|
|
('progress' in obj && typeof obj.progress === 'string') ||
|
|
('due' in obj && obj.due != null) ||
|
|
('percentComplete' in obj)
|
|
)) return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Given every object in an account and the calendar ids under consideration,
|
|
* return the ids of calendars that hold at least one object and whose objects
|
|
* are ALL tasks (no event). Empty calendars are deliberately NOT included -
|
|
* a brand-new event calendar with nothing in it must stay visible.
|
|
*/
|
|
export function findTasksOnlyCalendarIds(
|
|
objects: ScannedCalendarObject[],
|
|
calendarIds: string[],
|
|
): Set<string> {
|
|
const withAny = new Set<string>();
|
|
const withEvent = new Set<string>();
|
|
for (const obj of objects) {
|
|
const task = isTaskLikeObject(obj);
|
|
const ids = obj.calendarIds ? Object.keys(obj.calendarIds) : [];
|
|
for (const id of ids) {
|
|
withAny.add(id);
|
|
if (!task) withEvent.add(id);
|
|
}
|
|
}
|
|
const tasksOnly = new Set<string>();
|
|
for (const id of calendarIds) {
|
|
if (withAny.has(id) && !withEvent.has(id)) tasksOnly.add(id);
|
|
}
|
|
return tasksOnly;
|
|
}
|