mirror of
https://github.com/zadam/trilium.git
synced 2026-03-01 18:13:39 +01:00
203 lines
7.6 KiB
TypeScript
203 lines
7.6 KiB
TypeScript
import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js";
|
|
import { dayjs } from "@triliumnext/commons";
|
|
import clsx from "clsx";
|
|
import { start } from "repl";
|
|
import * as rruleLib from 'rrule';
|
|
|
|
import FNote from "../../../entities/fnote";
|
|
import froca from "../../../services/froca";
|
|
import server from "../../../services/server";
|
|
import toastService from "../../../services/toast";
|
|
import { getCustomisableLabel, getMonthsInDateRange } from "./utils";
|
|
|
|
interface Event {
|
|
startDate: string,
|
|
endDate?: string | null,
|
|
startTime?: string | null,
|
|
endTime?: string | null,
|
|
isArchived?: boolean,
|
|
recurrence?: 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");
|
|
const recurrence = getCustomisableLabel(note, "recurrence", "calendar:recurrence");
|
|
const isArchived = note.hasLabel("archived");
|
|
try {
|
|
events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, recurrence, isArchived }));
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
const errorMessage = error.message;
|
|
toastService.showError(errorMessage);
|
|
console.error(errorMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
return events.flat();
|
|
}
|
|
|
|
export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg) {
|
|
const events: EventInput[] = [];
|
|
|
|
// Gather all the required date note IDs.
|
|
const dateRange = getMonthsInDateRange(e.startStr, e.endStr);
|
|
let allDateNoteIds: string[] = [];
|
|
for (const month of dateRange) {
|
|
// TODO: Deduplicate get type.
|
|
const dateNotesForMonth = await server.get<Record<string, string>>(`special-notes/notes-for-month/${month}?calendarRoot=${note.noteId}`);
|
|
const dateNoteIds = Object.values(dateNotesForMonth);
|
|
allDateNoteIds = [...allDateNoteIds, ...dateNoteIds];
|
|
}
|
|
|
|
// Request all the date notes.
|
|
const dateNotes = await froca.getNotes(allDateNoteIds);
|
|
const childNoteToDateMapping: Record<string, string> = {};
|
|
for (const dateNote of dateNotes) {
|
|
const startDate = dateNote.getLabelValue("dateNote");
|
|
if (!startDate) {
|
|
continue;
|
|
}
|
|
|
|
events.push(await buildEvent(dateNote, { startDate }));
|
|
|
|
|
|
if (dateNote.hasChildren()) {
|
|
const childNoteIds = await dateNote.getSubtreeNoteIds();
|
|
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 buildEvent(childNote, { startDate });
|
|
events.push(event);
|
|
}
|
|
|
|
return events.flat();
|
|
}
|
|
|
|
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, recurrence, isArchived }: Event) {
|
|
const customTitleAttributeName = note.getLabelValue("calendar:title");
|
|
const titles = await parseCustomTitle(customTitleAttributeName, note);
|
|
const colorClass = note.getColorClass();
|
|
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) {
|
|
if (endDate) {
|
|
endDate = dayjs(endDate).add(1, "day").format("YYYY-MM-DD");
|
|
} else if (startDate) {
|
|
endDate = dayjs(startDate).add(1, "day").format("YYYY-MM-DD");
|
|
}
|
|
}
|
|
|
|
endDate = (endTime ? `${endDate}T${endTime}:00` : endDate);
|
|
const eventData: EventInput = {
|
|
id: note.noteId,
|
|
title,
|
|
start: startDate,
|
|
url: `#${note.noteId}?popup`,
|
|
noteId: note.noteId,
|
|
iconClass: note.getLabelValue("iconClass"),
|
|
promotedAttributes: displayedAttributesData,
|
|
className: clsx({archived: isArchived}, colorClass)
|
|
};
|
|
if (endDate) {
|
|
eventData.end = endDate;
|
|
}
|
|
|
|
if (recurrence) {
|
|
// Generate rrule string
|
|
const rruleString = `DTSTART:${dayjs(startDate).format("YYYYMMDD[T]HHmmss")}\n${recurrence}`;
|
|
|
|
// Validate rrule string
|
|
let rruleValid = true;
|
|
try {
|
|
rruleLib.rrulestr(rruleString, { forceset: true }) as rruleLib.RRuleSet;
|
|
} catch {
|
|
rruleValid = false;
|
|
}
|
|
|
|
if (rruleValid) {
|
|
delete eventData.end;
|
|
eventData.rrule = rruleString;
|
|
if (endDate){
|
|
const duration = dayjs.duration(dayjs(endDate).diff(dayjs(startDate)));
|
|
eventData.duration = duration.format("HH:mm");
|
|
}
|
|
} else {
|
|
throw new Error(`Note "${note.noteId} ${note.title}" has an invalid #recurrence string ${recurrence}. Excluding...`);
|
|
}
|
|
}
|
|
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;
|
|
}
|