From 5bb9117fde05915e4c533b6e10af0f9310a2af6d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Sep 2025 17:56:35 +0300 Subject: [PATCH] chore(react/collections/calendar): render non-calendar events --- .../collections/calendar/event_builder.ts | 112 ++++++++++++++ .../widgets/collections/calendar/index.tsx | 9 +- .../src/widgets/collections/calendar/utils.ts | 22 +++ .../src/widgets/view_widgets/calendar_view.ts | 140 +----------------- 4 files changed, 143 insertions(+), 140 deletions(-) create mode 100644 apps/client/src/widgets/collections/calendar/event_builder.ts diff --git a/apps/client/src/widgets/collections/calendar/event_builder.ts b/apps/client/src/widgets/collections/calendar/event_builder.ts new file mode 100644 index 000000000..4a256728b --- /dev/null +++ b/apps/client/src/widgets/collections/calendar/event_builder.ts @@ -0,0 +1,112 @@ +import { EventInput, EventSourceInput } from "@fullcalendar/core/index.js"; +import froca from "../../../services/froca"; +import { formatDateToLocalISO, getCustomisableLabel, offsetDate } from "./utils"; +import FNote from "../../../entities/fnote"; + +interface Event { + startDate: string, + endDate?: string | null, + startTime?: string | null, + endTime?: string | null +} + +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"); + events.push(await buildEvent(note, { startDate, endDate, startTime, endTime })); + } + + return events.flat(); +} + +async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime }: 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 + }; + 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; +} diff --git a/apps/client/src/widgets/collections/calendar/index.tsx b/apps/client/src/widgets/collections/calendar/index.tsx index 7f15876d3..803f1f2cc 100644 --- a/apps/client/src/widgets/collections/calendar/index.tsx +++ b/apps/client/src/widgets/collections/calendar/index.tsx @@ -1,7 +1,7 @@ import { DateSelectArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js"; import { ViewModeProps } from "../interface"; import Calendar from "./calendar"; -import { useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import "./index.css"; import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumOption, useTriliumOptionInt } from "../../react/hooks"; import { CreateChildrenResponse, LOCALE_IDS } from "@triliumnext/commons"; @@ -12,6 +12,7 @@ import server from "../../../services/server"; import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils"; import dialog from "../../../services/dialog"; import { t } from "../../../services/i18n"; +import { buildEvents } from "./event_builder"; interface CalendarViewData { @@ -54,6 +55,11 @@ export default function CalendarView({ note, noteIds }: ViewModeProps calendarRef.current?.updateSize()); const isCalendarRoot = (calendarRoot || workspaceCalendarRoot); const isEditable = !isCalendarRoot; + const eventBuilder = useMemo(() => { + if (!isCalendarRoot) { + return async () => await buildEvents(noteIds); + } + }, [isCalendarRoot, noteIds]); const plugins = usePlugins(isEditable, isCalendarRoot); const locale = useLocale(); @@ -97,6 +103,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps { @@ -54,7 +39,7 @@ export default class CalendarView extends ViewMode<{}> { let eventBuilder: EventSourceFunc; if (!this.isCalendarRoot) { - eventBuilder = async () => await CalendarView.buildEvents(this.noteIds) + eventBuilder = } else { eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e); } @@ -242,129 +227,6 @@ export default class CalendarView extends ViewMode<{}> { return events.flat(); } - static async buildEvents(noteIds: string[]) { - const notes = await froca.getNotes(noteIds); - const events: EventSourceInput = []; - - for (const note of notes) { - const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate"); - - if (!startDate) { - continue; - } - - const endDate = CalendarView.#getCustomisableLabel(note, "endDate", "calendar:endDate"); - const startTime = CalendarView.#getCustomisableLabel(note, "startTime", "calendar:startTime"); - const endTime = CalendarView.#getCustomisableLabel(note, "endTime", "calendar:endTime"); - events.push(await CalendarView.buildEvent(note, { startDate, endDate, startTime, endTime })); - } - - return events.flat(); - } - - /** - * Allows the user to customize the attribute from which to obtain a particular value. For example, if `customLabelNameAttribute` is `calendar:startDate` - * and `defaultLabelName` is `startDate` and the note at hand has `#calendar:startDate=myStartDate #myStartDate=2025-02-26` then the value returned will - * be `2025-02-26`. If there is no custom attribute value, then the value of the default attribute is returned instead (e.g. `#startDate`). - * - * @param note the note from which to read the values. - * @param defaultLabelName the name of the label in case a custom value is not found. - * @param customLabelNameAttribute the name of the label to look for a custom value. - * @returns the value of either the custom label or the default label. - */ - static #getCustomisableLabel(note: FNote, defaultLabelName: string, customLabelNameAttribute: string) { - const customAttributeName = note.getLabelValue(customLabelNameAttribute); - if (customAttributeName) { - const customValue = note.getLabelValue(customAttributeName); - if (customValue) { - return customValue; - } - } - - return note.getLabelValue(defaultLabelName); - } - - static async buildEvent(note: FNote, { startDate, endDate, startTime, endTime }: Event) { - const customTitleAttributeName = note.getLabelValue("calendar:title"); - const titles = await CalendarView.#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 this.#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 = CalendarView.#offsetDate(endDate ?? startDate, 1); - if (endDateOffset) { - endDate = CalendarView.#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 - }; - if (endDate) { - eventData.end = endDate; - } - events.push(eventData); - } - return events; - } - - static async #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; - } - - static async #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 CalendarView.#parseCustomTitle(targetCustomTitleValue, targetNote, false); - titles.push(targetTitles.flat()); - } - - return titles.flat(); - } - } - } - - return [note.title]; - } - buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) { if (!this.calendar) { return;