From 530340f753f04c059d27dc676dd901e95bf9dbd3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 09:37:20 +0200 Subject: [PATCH 1/7] fix(client): sporadious crash with classic toolbar not being available --- src/public/app/widgets/type_widgets/editable_text.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index acd006e9b..c5f2df34b 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -223,7 +223,9 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } $classicToolbarWidget.empty(); - $classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element); + if ($classicToolbarWidget.length) { + $classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element); + } if (utils.isMobile()) { $classicToolbarWidget.addClass("visible"); From f4e6edd19edd626bdfc6aab7a2d923b2f4375770 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 09:47:48 +0200 Subject: [PATCH 2/7] feat(views/calendar): basic recursion for calendar root --- src/public/app/widgets/view_widgets/calendar_view.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 8a7e797bb..9cdb06978 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -215,10 +215,17 @@ export default class CalendarView extends ViewMode { const events: EventSourceInput = []; for (const note of notes) { - const startDate = note.getAttributeValue("label", "startDate"); + const startDate = note.getLabelValue("startDate") ?? note.getLabelValue("dateNote"); const customTitle = note.getAttributeValue("label", "calendar:title"); const color = note.getAttributeValue("label", "calendar:color") ?? note.getAttributeValue("label", "color") ?? undefined; + if (note.hasChildren()) { + const childrenEventData = await this.#buildEvents(note.getChildNoteIds()); + if (childrenEventData.length > 0) { + events.push(childrenEventData); + } + } + if (!startDate) { continue; } @@ -243,7 +250,7 @@ export default class CalendarView extends ViewMode { } } - return events; + return events.flat(); } static async #parseCustomTitle(customTitleValue: string | null, note: FNote, allowRelations = true): Promise { From bc4d820cb0ece48cc7c544fbb26836c797d03e62 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 10:00:18 +0200 Subject: [PATCH 3/7] feat(views/calendar): display child notes of days --- .../app/widgets/view_widgets/calendar_view.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 9cdb06978..f0582d5ce 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -67,6 +67,7 @@ export default class CalendarView extends ViewMode { private noteIds: string[]; private parentNote: FNote; private calendar?: Calendar; + private isCalendarRoot: boolean; constructor(args: ViewModeArgs) { super(args); @@ -75,7 +76,7 @@ export default class CalendarView extends ViewMode { this.$calendarContainer = this.$root.find(".calendar-container"); this.noteIds = args.noteIds; this.parentNote = args.parentNote; - console.log(args); + this.isCalendarRoot = false; args.$parent.append(this.$root); } @@ -97,7 +98,7 @@ export default class CalendarView extends ViewMode { const calendar = new Calendar(this.$calendarContainer[0], { plugins, initialView: "dayGridMonth", - events: async () => await CalendarView.#buildEvents(this.noteIds), + events: async () => await this.#buildEvents(this.noteIds), editable: isEditable, selectable: isEditable, select: (e) => this.#onCalendarSelection(e), @@ -121,6 +122,7 @@ export default class CalendarView extends ViewMode { }); calendar.render(); this.calendar = calendar; + this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot"); return this.$root; } @@ -210,17 +212,32 @@ export default class CalendarView extends ViewMode { } } - static async #buildEvents(noteIds: string[]) { + async #buildEvents(noteIds: string[], enforcedStartDate?: string) { const notes = await froca.getNotes(noteIds); const events: EventSourceInput = []; for (const note of notes) { - const startDate = note.getLabelValue("startDate") ?? note.getLabelValue("dateNote"); + 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; if (note.hasChildren()) { - const childrenEventData = await this.#buildEvents(note.getChildNoteIds()); + 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); if (childrenEventData.length > 0) { events.push(childrenEventData); } From 5c1db3cab2f31c283ea4dfe35c29dc74541395cc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 10:03:38 +0200 Subject: [PATCH 4/7] feat(views/calendar): disable interaction in calendar root mode --- src/public/app/widgets/view_widgets/calendar_view.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index f0582d5ce..ebce0c0d7 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -85,7 +85,8 @@ export default class CalendarView extends ViewMode { } async renderList(): Promise | undefined> { - const isEditable = true; + this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot"); + const isEditable = !this.isCalendarRoot; const { Calendar } = await import("@fullcalendar/core"); const plugins: PluginDef[] = []; @@ -122,7 +123,6 @@ export default class CalendarView extends ViewMode { }); calendar.render(); this.calendar = calendar; - this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot"); return this.$root; } From 43f79ca813efe1ecd9a9f648bb09880c088132b4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 10:12:36 +0200 Subject: [PATCH 5/7] feat(views/calendar): click to go to day note --- .../app/widgets/view_widgets/calendar_view.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index ebce0c0d7..bfbafb847 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -10,6 +10,8 @@ import dialogService from "../../services/dialog.js"; import attributes from "../../services/attributes.js"; import type { EventData } from "../../components/app_context.js"; import utils from "../../services/utils.js"; +import date_notes from "../../services/date_notes.js"; +import appContext from "../../components/app_context.js"; const TPL = `
@@ -91,8 +93,7 @@ export default class CalendarView extends ViewMode { const { Calendar } = await import("@fullcalendar/core"); const plugins: PluginDef[] = []; plugins.push((await import("@fullcalendar/daygrid")).default); - - if (isEditable) { + if (isEditable || this.isCalendarRoot) { plugins.push((await import("@fullcalendar/interaction")).default); } @@ -119,7 +120,17 @@ export default class CalendarView extends ViewMode { html += utils.escapeHtml(e.event.title); return { html }; - }) + }), + dateClick: async (e) => { + if (!this.isCalendarRoot) { + return; + } + + const note = await date_notes.getDayNote(e.dateStr); + if (note) { + appContext.tabManager.getActiveContext().setNote(note.noteId); + } + } }); calendar.render(); this.calendar = calendar; From a9cebe312fa6f3fc7d70730164cbf57d831d9c56 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 10:56:10 +0200 Subject: [PATCH 6/7] 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); From 5a8d5c59f5010758228e50c6003c30029195b0eb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 10:57:40 +0200 Subject: [PATCH 7/7] feat(views/calendar): support workspaceCalendarRoot --- src/public/app/widgets/view_widgets/calendar_view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 87b94e3e8..11e6553ff 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -87,7 +87,7 @@ export default class CalendarView extends ViewMode { } async renderList(): Promise | undefined> { - this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot"); + this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot"); const isEditable = !this.isCalendarRoot; const { Calendar } = await import("@fullcalendar/core");