From a9cebe312fa6f3fc7d70730164cbf57d831d9c56 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 10:56:10 +0200 Subject: [PATCH] refactor(views/calendar): use specific API for date notes for performance --- src/public/app/services/utils.ts | 22 ++++ .../app/widgets/view_widgets/calendar_view.ts | 122 ++++++++++++------ 2 files changed, 104 insertions(+), 40 deletions(-) diff --git a/src/public/app/services/utils.ts b/src/public/app/services/utils.ts index 6fd5678de..7accea1fb 100644 --- a/src/public/app/services/utils.ts +++ b/src/public/app/services/utils.ts @@ -30,6 +30,27 @@ function parseDate(str: string) { } } +// Source: https://stackoverflow.com/a/30465299/4898894 +function getMonthsInDateRange(startDate: string, endDate: string) { + const start = startDate.split('-'); + const end = endDate.split('-'); + const startYear = parseInt(start[0]); + const endYear = parseInt(end[0]); + const dates = []; + + for (let i = startYear; i <= endYear; i++) { + const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; + const startMon = i === startYear ? parseInt(start[1])-1 : 0; + + for(let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j+1) { + const month = j+1; + const displayMonth = month < 10 ? '0'+month : month; + dates.push([i, displayMonth].join('-')); + } + } + return dates; +} + function padNum(num: number) { return `${num <= 9 ? "0" : ""}${num}`; } @@ -621,6 +642,7 @@ export default { reloadFrontendApp, reloadTray, parseDate, + getMonthsInDateRange, formatDateISO, formatDateTime, formatTimeInterval, diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index bfbafb847..87b94e3e8 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,4 +1,4 @@ -import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; +import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventInput, EventSourceFunc, EventSourceFuncArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; @@ -97,10 +97,17 @@ export default class CalendarView extends ViewMode { plugins.push((await import("@fullcalendar/interaction")).default); } + let eventBuilder: EventSourceFunc; + if (!this.isCalendarRoot) { + eventBuilder = async () => await this.#buildEvents(this.noteIds) + } else { + eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e); + } + const calendar = new Calendar(this.$calendarContainer[0], { plugins, initialView: "dayGridMonth", - events: async () => await this.#buildEvents(this.noteIds), + events: eventBuilder, editable: isEditable, selectable: isEditable, select: (e) => this.#onCalendarSelection(e), @@ -223,32 +230,59 @@ export default class CalendarView extends ViewMode { } } - async #buildEvents(noteIds: string[], enforcedStartDate?: string) { + async #buildEventsForCalendar(e: EventSourceFuncArg) { + const events = []; + + // Gather all the required date note IDs. + const dateRange = utils.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}`); + 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 CalendarView.#buildEvent(dateNote, startDate)); + + if (dateNote.hasChildren()) { + const childNoteIds = dateNote.getChildNoteIds(); + 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 CalendarView.#buildEvent(childNote, startDate); + events.push(event); + } + + return events.flat(); + } + + async #buildEvents(noteIds: string[]) { const notes = await froca.getNotes(noteIds); const events: EventSourceInput = []; for (const note of notes) { - let startDate; - - if (enforcedStartDate) { - startDate = enforcedStartDate; - } else if (!this.isCalendarRoot) { - startDate = note.getLabelValue("startDate"); - } else { - startDate = note.getLabelValue("dateNote"); - } - const customTitle = note.getAttributeValue("label", "calendar:title"); - const color = note.getAttributeValue("label", "calendar:color") ?? note.getAttributeValue("label", "color") ?? undefined; + let startDate = note.getLabelValue("startDate"); if (note.hasChildren()) { - const dateNote = note.getLabelValue("dateNote"); - let enforcedStartDate = undefined; - if (dateNote) { - // This is a day note which can have children. Make sure the children are added to the calendar even if they themselves don't have it. - enforcedStartDate = dateNote; - } - - const childrenEventData = await this.#buildEvents(note.getChildNoteIds(), enforcedStartDate); + const childrenEventData = await this.#buildEvents(note.getChildNoteIds()); if (childrenEventData.length > 0) { events.push(childrenEventData); } @@ -258,29 +292,37 @@ export default class CalendarView extends ViewMode { continue; } - const titles = await CalendarView.#parseCustomTitle(customTitle, note); - for (const title of titles) { - const eventData: typeof events[0] = { - title: title, - start: startDate, - url: `#${note.noteId}`, - noteId: note.noteId, - color: color, - iconClass: note.getLabelValue("iconClass") - }; - - const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, 1); - if (endDate) { - eventData.end = CalendarView.#formatDateToLocalISO(endDate); - } - - events.push(eventData); - } + const endDate = note.getAttributeValue("label", "endDate"); + events.push(await CalendarView.#buildEvent(note, startDate, endDate)); } return events.flat(); } + static async #buildEvent(note: FNote, startDate: string, endDate?: string | null) { + const customTitle = note.getLabelValue("calendar:title"); + const titles = await CalendarView.#parseCustomTitle(customTitle, note); + const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color"); + const events: EventInput[] = []; + for (const title of titles) { + const eventData: EventInput = { + title: title, + start: startDate, + url: `#${note.noteId}`, + noteId: note.noteId, + color: color ?? undefined, + iconClass: note.getLabelValue("iconClass") + }; + + const endDateOffset = CalendarView.#offsetDate(endDate ?? startDate, 1); + if (endDateOffset) { + eventData.end = CalendarView.#formatDateToLocalISO(endDateOffset); + } + events.push(eventData); + } + return events; + } + static async #parseCustomTitle(customTitleValue: string | null, note: FNote, allowRelations = true): Promise { if (customTitleValue) { const attributeName = customTitleValue.substring(1);