Victor Wiebe cdb5ad022c v0.2.0: mark recurring to-dos with a recurrence symbol
- Detect recurrence via the 'recurring' tag maintained by the Repeating
  To-Dos plugin (v2 stores recurrence in userData, not the note body, so
  tag-presence is the correct signal — not body scanning)
- DataAdapter gains getNoteTagTitles; lookup runs for to-dos only (notes
  can't recur), keeping extra API calls minimal
- GtdEvent.isRecurring threaded through collector and payload
- Webview shows ↻ to the right of any icon/glyph across all views:
  strip tiles, hover-card rows, month-grid chips, unscheduled chips
- RECURRING_TAG constant leaves room for a future configurable tag name
- README documents the pairing; SPEC roadmap marks Option A shipped
- Version bumped to 0.2.0; 2 new tests (49 total)
2026-06-13 09:49:20 -04:00

128 lines
3.1 KiB
TypeScript

import {
CalendarConfig,
DataAdapter,
GtdEvent,
RawNote,
RECURRING_TAG,
} from "./types";
import resolveScopedFolderIds from "./folderScope";
import extractGtdBlock from "./gtdBlock";
import { resolveEventDate } from "./resolveDate";
export interface CollectResult {
events: GtdEvent[];
warnings: string[];
scannedFolders: number;
scannedNotes: number;
}
/**
* Collect calendar events per SPEC.md:
* - resolve folder scope from the calendar note's folder
* - fetch notes/todos in scope
* - apply the inclusion matrix (§3.2)
* - resolve dates (§3.1)
* - sort within the flat list (grouping happens at render time)
*
* The calendar note itself is always excluded.
*/
export default async function collectEvents(
adapter: DataAdapter,
calendarNoteId: string,
calendarFolderId: string,
config: CalendarConfig
): Promise<CollectResult> {
const warnings: string[] = [];
const folders = await adapter.getFolders();
const folderIds = resolveScopedFolderIds(
folders,
calendarFolderId,
config.scopeDepth
);
let scannedNotes = 0;
const events: GtdEvent[] = [];
for (const folderId of folderIds) {
const notes = await adapter.getNotesInFolder(folderId);
for (const note of notes) {
scannedNotes += 1;
if (note.id === calendarNoteId) continue;
const event = await noteToEvent(adapter, note, config, warnings);
if (event) events.push(event);
}
}
sortEvents(events, config);
return {
events,
warnings,
scannedFolders: folderIds.length,
scannedNotes,
};
}
async function noteToEvent(
adapter: DataAdapter,
note: RawNote,
config: CalendarConfig,
warnings: string[]
): Promise<GtdEvent | null> {
const mode = note.is_todo ? config.todos : config.notes;
if (mode === "none") return null;
const result = extractGtdBlock(note.body);
if (mode === "gtd-only" && !result.found) return null;
if (result.error) {
warnings.push(`"${note.title}": gtd block problem — ${result.error}`);
}
const block = result.block;
const date = resolveEventDate(note, block);
if (block && block.date && !date) {
warnings.push(`"${note.title}": unparseable date "${block.date}"`);
}
// Only to-dos can recur; skip the tag lookup for plain notes.
let isRecurring = false;
if (note.is_todo) {
const tags = await adapter.getNoteTagTitles(note.id);
isRecurring = tags.includes(RECURRING_TAG);
}
return {
id: note.id,
title: block && block.title ? block.title : note.title,
date,
isTodo: Boolean(note.is_todo),
completed: Boolean(note.is_todo && note.todo_completed > 0),
bgColour: block ? block.bgColour : null,
fgColour: block ? block.fgColour : null,
icon: block ? block.icon : null,
text: block ? block.text : null,
updatedTime: note.updated_time,
isRecurring,
};
}
function sortEvents(events: GtdEvent[], config: CalendarConfig): void {
const direction = config.sort === "desc" ? -1 : 1;
events.sort((a, b) => {
let comparison: number;
if (config.sortType === "modified_date") {
comparison = a.updatedTime - b.updatedTime;
} else {
comparison = a.title.localeCompare(b.title, undefined, {
sensitivity: "base",
});
}
return comparison * direction;
});
}