mirror of
https://github.com/zadam/trilium.git
synced 2025-11-03 03:46:37 +01:00
chore(react/collections/calendar): render non-calendar events
This commit is contained in:
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 { DateSelectArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
|
||||||
import { ViewModeProps } from "../interface";
|
import { ViewModeProps } from "../interface";
|
||||||
import Calendar from "./calendar";
|
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 "./index.css";
|
||||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||||
import { CreateChildrenResponse, LOCALE_IDS } from "@triliumnext/commons";
|
import { CreateChildrenResponse, LOCALE_IDS } from "@triliumnext/commons";
|
||||||
@@ -12,6 +12,7 @@ import server from "../../../services/server";
|
|||||||
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
|
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
|
||||||
import dialog from "../../../services/dialog";
|
import dialog from "../../../services/dialog";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
import { buildEvents } from "./event_builder";
|
||||||
|
|
||||||
interface CalendarViewData {
|
interface CalendarViewData {
|
||||||
|
|
||||||
@@ -54,6 +55,11 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
useResizeObserver(containerRef, () => calendarRef.current?.updateSize());
|
useResizeObserver(containerRef, () => calendarRef.current?.updateSize());
|
||||||
const isCalendarRoot = (calendarRoot || workspaceCalendarRoot);
|
const isCalendarRoot = (calendarRoot || workspaceCalendarRoot);
|
||||||
const isEditable = !isCalendarRoot;
|
const isEditable = !isCalendarRoot;
|
||||||
|
const eventBuilder = useMemo(() => {
|
||||||
|
if (!isCalendarRoot) {
|
||||||
|
return async () => await buildEvents(noteIds);
|
||||||
|
}
|
||||||
|
}, [isCalendarRoot, noteIds]);
|
||||||
|
|
||||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
@@ -97,6 +103,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
return (plugins &&
|
return (plugins &&
|
||||||
<div className="calendar-view" ref={containerRef}>
|
<div className="calendar-view" ref={containerRef}>
|
||||||
<Calendar
|
<Calendar
|
||||||
|
events={eventBuilder}
|
||||||
calendarRef={calendarRef}
|
calendarRef={calendarRef}
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
tabIndex={100}
|
tabIndex={100}
|
||||||
|
|||||||
@@ -57,3 +57,25 @@ export function formatTimeToLocalISO(date: Date | null | undefined) {
|
|||||||
.split("T")[1]
|
.split("T")[1]
|
||||||
.substring(0, 5);
|
.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 type { SegmentedControlSegment } from "electron";
|
||||||
import { LOCALE_IDS } from "@triliumnext/commons";
|
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<{}> {
|
export default class CalendarView extends ViewMode<{}> {
|
||||||
|
|
||||||
@@ -54,7 +39,7 @@ export default class CalendarView extends ViewMode<{}> {
|
|||||||
|
|
||||||
let eventBuilder: EventSourceFunc;
|
let eventBuilder: EventSourceFunc;
|
||||||
if (!this.isCalendarRoot) {
|
if (!this.isCalendarRoot) {
|
||||||
eventBuilder = async () => await CalendarView.buildEvents(this.noteIds)
|
eventBuilder =
|
||||||
} else {
|
} else {
|
||||||
eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e);
|
eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e);
|
||||||
}
|
}
|
||||||
@@ -242,129 +227,6 @@ export default class CalendarView extends ViewMode<{}> {
|
|||||||
return events.flat();
|
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">) {
|
buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) {
|
||||||
if (!this.calendar) {
|
if (!this.calendar) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user