- 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)
128 lines
3.1 KiB
TypeScript
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;
|
|
});
|
|
}
|