import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js"; import froca from "../../../services/froca"; import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils"; import FNote from "../../../entities/fnote"; import server from "../../../services/server"; interface Event { startDate: string, endDate?: string | null, startTime?: string | null, endTime?: string | null, isArchived?: boolean; } export async function buildEvents(noteIds: string[]) { const notes = await froca.getNotes(noteIds); const events: EventSourceInput = []; for (const note of notes) { const startDate = getCustomisableLabel(note, "startDate", "calendar:startDate"); if (!startDate) { continue; } const endDate = getCustomisableLabel(note, "endDate", "calendar:endDate"); const startTime = getCustomisableLabel(note, "startTime", "calendar:startTime"); const endTime = getCustomisableLabel(note, "endTime", "calendar:endTime"); const isArchived = note.hasLabel("archived"); events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, isArchived })); } return events.flat(); } export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg) { const events: EventInput[] = []; // Gather all the required date note IDs. const dateRange = getMonthsInDateRange(e.startStr, e.endStr); let allDateNoteIds: string[] = []; for (const month of dateRange) { // TODO: Deduplicate get type. const dateNotesForMonth = await server.get>(`special-notes/notes-for-month/${month}?calendarRoot=${note.noteId}`); const dateNoteIds = Object.values(dateNotesForMonth); allDateNoteIds = [...allDateNoteIds, ...dateNoteIds]; } // Request all the date notes. const dateNotes = await froca.getNotes(allDateNoteIds); const childNoteToDateMapping: Record = {}; for (const dateNote of dateNotes) { const startDate = dateNote.getLabelValue("dateNote"); if (!startDate) { continue; } events.push(await buildEvent(dateNote, { startDate })); if (dateNote.hasChildren()) { const childNoteIds = await dateNote.getSubtreeNoteIds(); for (const childNoteId of childNoteIds) { childNoteToDateMapping[childNoteId] = startDate; } } } // Request all child notes of date notes in a single run. const childNoteIds = Object.keys(childNoteToDateMapping); const childNotes = await froca.getNotes(childNoteIds); for (const childNote of childNotes) { const startDate = childNoteToDateMapping[childNote.noteId]; const event = await buildEvent(childNote, { startDate }); events.push(event); } return events.flat(); } export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) { const customTitleAttributeName = note.getLabelValue("calendar:title"); const titles = await parseCustomTitle(customTitleAttributeName, note); const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color"); const events: EventInput[] = []; const calendarDisplayedAttributes = note.getLabelValue("calendar:displayedAttributes")?.split(","); let displayedAttributesData: Array<[string, string]> | null = null; if (calendarDisplayedAttributes) { displayedAttributesData = await buildDisplayedAttributes(note, calendarDisplayedAttributes); } for (const title of titles) { if (startTime && endTime && !endDate) { endDate = startDate; } startDate = (startTime ? `${startDate}T${startTime}:00` : startDate); if (!startTime) { const endDateOffset = offsetDate(endDate ?? startDate, 1); if (endDateOffset) { endDate = formatDateToLocalISO(endDateOffset); } } endDate = (endTime ? `${endDate}T${endTime}:00` : endDate); const eventData: EventInput = { title: title, start: startDate, url: `#${note.noteId}?popup`, noteId: note.noteId, color: color ?? undefined, iconClass: note.getLabelValue("iconClass"), promotedAttributes: displayedAttributesData, className: isArchived ? "archived" : "" }; if (endDate) { eventData.end = endDate; } events.push(eventData); } return events; } async function parseCustomTitle(customTitlettributeName: string | null, note: FNote, allowRelations = true): Promise { if (customTitlettributeName) { const labelValue = note.getAttributeValue("label", customTitlettributeName); if (labelValue) return [labelValue]; if (allowRelations) { const relations = note.getRelations(customTitlettributeName); if (relations.length > 0) { const noteIds = relations.map((r) => r.targetNoteId); const notesFromRelation = await froca.getNotes(noteIds); const titles: string[][] = []; for (const targetNote of notesFromRelation) { const targetCustomTitleValue = targetNote.getAttributeValue("label", "calendar:title"); const targetTitles = await parseCustomTitle(targetCustomTitleValue, targetNote, false); titles.push(targetTitles.flat()); } return titles.flat(); } } } return [note.title]; } async function buildDisplayedAttributes(note: FNote, calendarDisplayedAttributes: string[]) { const filteredDisplayedAttributes = note.getAttributes().filter((attr): boolean => calendarDisplayedAttributes.includes(attr.name)) const result: Array<[string, string]> = []; for (const attribute of filteredDisplayedAttributes) { if (attribute.type === "label") result.push([attribute.name, attribute.value]); else result.push([attribute.name, (await attribute.getTargetNote())?.title || ""]) } return result; }