mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 21:19:01 +01:00 
			
		
		
		
	chore(react/collections/calendar): render non-calendar events
This commit is contained in:
		
							parent
							
								
									84d35c1a37
								
							
						
					
					
						commit
						5bb9117fde
					
				
							
								
								
									
										112
									
								
								apps/client/src/widgets/collections/calendar/event_builder.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								apps/client/src/widgets/collections/calendar/event_builder.ts
									
									
									
									
									
										Normal file
									
								
							@ -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<string[]> {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
@ -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<CalendarVi
 | 
			
		||||
    useResizeObserver(containerRef, () => 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<CalendarVi
 | 
			
		||||
    return (plugins &&
 | 
			
		||||
        <div className="calendar-view" ref={containerRef}>
 | 
			
		||||
            <Calendar
 | 
			
		||||
                events={eventBuilder}
 | 
			
		||||
                calendarRef={calendarRef}
 | 
			
		||||
                plugins={plugins}
 | 
			
		||||
                tabIndex={100}
 | 
			
		||||
 | 
			
		||||
@ -57,3 +57,25 @@ export function formatTimeToLocalISO(date: Date | null | undefined) {
 | 
			
		||||
        .split("T")[1]
 | 
			
		||||
        .substring(0, 5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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.
 | 
			
		||||
 */
 | 
			
		||||
export function 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,21 +17,6 @@ import type { TouchBarItem } from "../../components/touch_bar.js";
 | 
			
		||||
import type { SegmentedControlSegment } from "electron";
 | 
			
		||||
import { LOCALE_IDS } from "@triliumnext/commons";
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate
 | 
			
		||||
interface CreateChildResponse {
 | 
			
		||||
    note: {
 | 
			
		||||
        noteId: string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Event {
 | 
			
		||||
    startDate: string,
 | 
			
		||||
    endDate?: string | null,
 | 
			
		||||
    startTime?: string | null,
 | 
			
		||||
    endTime?: string | null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class CalendarView extends ViewMode<{}> {
 | 
			
		||||
 | 
			
		||||
@ -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<string[]> {
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user