Files
Trilium/src/services/date_notes.ts

385 lines
13 KiB
TypeScript
Raw Normal View History

import noteService from "./notes.js";
import attributeService from "./attributes.js";
import dateUtils from "./date_utils.js";
import sql from "./sql.js";
import protectedSessionService from "./protected_session.js";
import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import hoistedNoteService from "./hoisted_note.js";
import type BNote from "../becca/entities/bnote.js";
2025-04-01 17:25:03 +02:00
import optionService from "./options.js";
import { t } from "i18next";
import dayjs from "dayjs";
import type { Dayjs } from "dayjs";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
dayjs.extend(isSameOrAfter);
2025-01-09 18:07:02 +02:00
const CALENDAR_ROOT_LABEL = "calendarRoot";
const YEAR_LABEL = "yearNote";
const MONTH_LABEL = "monthNote";
const WEEK_LABEL = "weekNote";
2025-01-09 18:07:02 +02:00
const DATE_LABEL = "dateNote";
2025-01-09 18:07:02 +02:00
const WEEKDAY_TRANSLATION_IDS = ["weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"];
const MONTH_TRANSLATION_IDS = [
2025-01-09 18:07:02 +02:00
"months.january",
"months.february",
"months.march",
"months.april",
"months.may",
"months.june",
"months.july",
"months.august",
"months.september",
"months.october",
"months.november",
"months.december"
];
2024-02-18 13:42:05 +02:00
function createNote(parentNote: BNote, noteTitle: string) {
return noteService.createNewNote({
parentNoteId: parentNote.noteId,
2018-01-28 19:30:14 -05:00
title: noteTitle,
2025-01-09 18:07:02 +02:00
content: "",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
2025-01-09 18:07:02 +02:00
type: "text"
}).note;
}
2024-02-18 13:42:05 +02:00
function getRootCalendarNote(): BNote {
let rootNote;
2022-12-23 15:07:48 +01:00
const workspaceNote = hoistedNoteService.getWorkspaceNote();
2024-02-18 13:42:05 +02:00
if (!workspaceNote || !workspaceNote.isRoot()) {
2025-01-09 18:07:02 +02:00
rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false }));
}
if (!rootNote) {
rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
}
if (!rootNote) {
2020-08-18 21:32:45 +02:00
sql.transactional(() => {
rootNote = noteService.createNewNote({
2025-01-09 18:07:02 +02:00
parentNoteId: "root",
title: "Calendar",
target: "into",
2020-08-18 21:32:45 +02:00
isProtected: false,
2025-01-09 18:07:02 +02:00
type: "text",
content: ""
2020-08-18 21:32:45 +02:00
}).note;
attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
2025-01-09 18:07:02 +02:00
attributeService.createLabel(rootNote.noteId, "sorted");
2020-08-18 21:32:45 +02:00
});
}
2024-02-18 13:42:05 +02:00
return rootNote as BNote;
}
2024-02-18 13:42:05 +02:00
function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
2025-04-01 00:08:48 +02:00
const yearStr = dateStr.trim().substring(0, 4);
2025-01-09 18:07:02 +02:00
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
2021-07-24 11:28:47 +02:00
if (yearNote) {
return yearNote;
}
2021-07-24 11:28:47 +02:00
sql.transactional(() => {
yearNote = createNote(rootNote, yearStr);
2021-07-24 11:28:47 +02:00
attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
2025-01-09 18:07:02 +02:00
attributeService.createLabel(yearNote.noteId, "sorted");
2025-01-09 18:07:02 +02:00
const yearTemplateAttr = rootNote.getOwnedAttribute("relation", "yearTemplate");
2021-07-24 11:28:47 +02:00
if (yearTemplateAttr) {
2025-01-09 18:07:02 +02:00
attributeService.createRelation(yearNote.noteId, "template", yearTemplateAttr.value);
}
2021-07-24 11:28:47 +02:00
});
2024-02-18 13:42:05 +02:00
return yearNote as unknown as BNote;
}
2024-02-18 13:42:05 +02:00
function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date) {
2020-06-20 12:31:38 +02:00
const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}";
const monthName = t(MONTH_TRANSLATION_IDS[dateObj.getMonth()]);
return pattern
2025-01-09 18:07:02 +02:00
.replace(/{shortMonth3}/g, monthName.slice(0, 3))
.replace(/{shortMonth4}/g, monthName.slice(0, 4))
.replace(/{isoMonth}/g, dateUtils.utcDateStr(dateObj).slice(0, 7))
.replace(/{monthNumberPadded}/g, monthNumber)
.replace(/{month}/g, monthName);
}
2024-02-18 13:42:05 +02:00
function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
2025-04-01 00:08:48 +02:00
const monthStr = dateStr.substring(0, 7);
2025-04-01 15:33:10 +02:00
const monthNumber = dateStr.substring(5, 7);
2025-01-09 18:07:02 +02:00
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
2021-07-24 11:28:47 +02:00
if (monthNote) {
return monthNote;
}
const dateObj = dateUtils.parseLocalDate(dateStr);
2021-07-24 11:28:47 +02:00
const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj);
2021-12-11 14:15:38 +01:00
const yearNote = getYearNote(dateStr, rootNote);
2021-07-24 11:28:47 +02:00
sql.transactional(() => {
monthNote = createNote(yearNote, noteTitle);
2021-07-24 11:28:47 +02:00
attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
2025-01-09 18:07:02 +02:00
attributeService.createLabel(monthNote.noteId, "sorted");
2025-01-09 18:07:02 +02:00
const monthTemplateAttr = rootNote.getOwnedAttribute("relation", "monthTemplate");
2021-07-24 11:28:47 +02:00
if (monthTemplateAttr) {
2025-01-09 18:07:02 +02:00
attributeService.createRelation(monthNote.noteId, "template", monthTemplateAttr.value);
}
2021-07-24 11:28:47 +02:00
});
2024-02-18 13:42:05 +02:00
return monthNote as unknown as BNote;
}
2024-02-18 13:42:05 +02:00
function getDayNoteTitle(rootNote: BNote, dayNumber: string, dateObj: Date) {
2020-06-20 12:31:38 +02:00
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.getDay()]);
return pattern
.replace(/{ordinal}/g, ordinal(parseInt(dayNumber)))
.replace(/{dayInMonthPadded}/g, dayNumber)
.replace(/{isoDate}/g, dateUtils.utcDateStr(dateObj))
.replace(/{weekDay}/g, weekDay)
2025-04-01 00:08:48 +02:00
.replace(/{weekDay3}/g, weekDay.substring(0, 3))
.replace(/{weekDay2}/g, weekDay.substring(0, 2));
}
2023-11-03 13:49:18 +01:00
/** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */
2024-02-18 13:42:05 +02:00
function ordinal(dayNumber: number) {
2023-11-03 13:49:18 +01:00
const suffixes = ["th", "st", "nd", "rd"];
const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0];
return `${dayNumber}${suffix}`;
}
2024-02-18 13:42:05 +02:00
function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
2025-04-01 00:08:48 +02:00
dateStr = dateStr.trim().substring(0, 10);
2021-12-27 20:48:14 +01:00
2025-01-09 18:07:02 +02:00
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
2021-07-24 11:28:47 +02:00
if (dateNote) {
return dateNote;
}
let dateParentNote;
if (checkWeekNoteEnabled(rootNote)) {
dateParentNote = getWeekNote(getWeekNumberStr(dayjs(dateStr)), rootNote);
} else {
dateParentNote = getMonthNote(dateStr, rootNote);
}
2025-04-01 15:33:10 +02:00
const dayNumber = dateStr.substring(8, 10);
2021-07-24 11:28:47 +02:00
const dateObj = dateUtils.parseLocalDate(dateStr);
2022-01-10 17:09:20 +01:00
const noteTitle = getDayNoteTitle(rootNote, dayNumber, dateObj);
2021-07-24 11:28:47 +02:00
sql.transactional(() => {
dateNote = createNote(dateParentNote as BNote, noteTitle);
2025-04-01 00:08:48 +02:00
attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substring(0, 10));
2021-07-24 11:28:47 +02:00
2025-01-09 18:07:02 +02:00
const dateTemplateAttr = rootNote.getOwnedAttribute("relation", "dateTemplate");
2021-07-24 11:28:47 +02:00
if (dateTemplateAttr) {
2025-01-09 18:07:02 +02:00
attributeService.createRelation(dateNote.noteId, "template", dateTemplateAttr.value);
}
2021-07-24 11:28:47 +02:00
});
2024-02-18 13:42:05 +02:00
return dateNote as unknown as BNote;
}
function getTodayNote(rootNote: BNote | null = null) {
return getDayNote(dateUtils.localNowDate(), rootNote);
2019-11-27 23:07:10 +01:00
}
2025-04-01 17:25:03 +02:00
function getWeekStartDate(date: Date, startOfWeek: string): Date {
2025-04-01 16:57:52 +02:00
const day = date.getDay();
let diff;
if (startOfWeek === "monday") {
diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
} else if (startOfWeek === "sunday") {
diff = date.getDate() - day;
} else {
throw new Error(`Unrecognized start of the week ${startOfWeek}`);
}
const startDate = new Date(date);
startDate.setDate(diff);
return startDate;
}
// TODO: Duplicated with getWeekNumber in src/public/app/widgets/buttons/calendar.ts
// Maybe can be merged later in monorepo setup
function getWeekNumberStr(date: Dayjs): string {
const year = date.year();
const dayOfWeek = (day: number) => (day - parseInt(optionService.getOption("firstDayOfWeek")) + 7) % 7;
// Get first day of the year and adjust to first week start
const jan1 = date.clone().year(year).month(0).date(1);
const jan1Weekday = jan1.day();
const dayOffset = dayOfWeek(jan1Weekday);
let firstWeekStart = jan1.clone().subtract(dayOffset, 'day');
// Adjust based on week rule
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
case 1: { // ISO 8601: first week contains Thursday
const thursday = firstWeekStart.clone().add(3, 'day'); // Monday + 3 = Thursday
if (thursday.year() < year) {
firstWeekStart = firstWeekStart.add(7, 'day');
}
break;
}
case 2: { // minDaysInFirstWeek rule
const daysInFirstWeek = 7 - dayOffset;
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
firstWeekStart = firstWeekStart.add(7, 'day');
}
break;
}
// default case 0: week containing Jan 1 → already handled
}
const diffDays = date.startOf('day').diff(firstWeekStart.startOf('day'), 'day');
const weekNumber = Math.floor(diffDays / 7) + 1;
// Handle case when date is before first week start → belongs to last week of previous year
if (weekNumber <= 0) {
return getWeekNumberStr(date.subtract(1, 'day'));
}
// Handle case when date belongs to first week of next year
const nextYear = year + 1;
const jan1Next = date.clone().year(nextYear).month(0).date(1);
const jan1WeekdayNext = jan1Next.day();
const offsetNext = dayOfWeek(jan1WeekdayNext);
let nextYearWeekStart = jan1Next.clone().subtract(offsetNext, 'day');
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
case 1: {
const thursday = nextYearWeekStart.clone().add(3, 'day');
if (thursday.year() < nextYear) {
nextYearWeekStart = nextYearWeekStart.add(7, 'day');
}
break;
}
case 2: {
const daysInFirstWeek = 7 - offsetNext;
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
nextYearWeekStart = nextYearWeekStart.add(7, 'day');
}
break;
}
}
if (date.isSameOrAfter(nextYearWeekStart)) {
return `${nextYear}-W01`;
}
return `${year}-W${weekNumber.toString().padStart(2, '0')}`;
}
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
const startOfWeek = optionService.getOption("firstDayOfWeek") === '0' ? 'sunday' : 'monday';
const dateObj = getWeekStartDate(dateUtils.parseLocalDate(dateStr), startOfWeek);
dateStr = dateUtils.utcDateTimeStr(dateObj);
return getDayNote(dateStr, rootNote);
}
function checkWeekNoteEnabled(rootNote: BNote) {
if (!rootNote.hasLabel('enableWeekNote')) {
return false;
}
return true;
}
2025-04-01 16:57:52 +02:00
function getWeekNoteTitle(rootNote: BNote, weekNumber: number) {
const pattern = rootNote.getOwnedLabelValue("weekPattern") || "Week {weekNumber}";
return pattern
.replace(/{weekNumber}/g, weekNumber.toString());
}
2025-04-01 17:25:03 +02:00
function getWeekNote(weekStr: string, _rootNote: BNote | null = null): BNote | null {
2025-04-01 16:57:52 +02:00
const rootNote = _rootNote || getRootCalendarNote();
2025-04-01 17:25:03 +02:00
if (!checkWeekNoteEnabled(rootNote)) {
2025-04-01 16:57:52 +02:00
return null;
}
weekStr = weekStr.trim().substring(0, 8);
let weekNote = searchService.findFirstNoteWithQuery(`#${WEEK_LABEL}="${weekStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
if (weekNote) {
return weekNote;
}
const [yearStr, weekNumStr] = weekStr.trim().split('-W');
const year = parseInt(yearStr);
const weekNumber = parseInt(weekNumStr);
const firstDayOfYear = new Date(year, 0, 1);
const weekStartDate = new Date(firstDayOfYear);
weekStartDate.setDate(firstDayOfYear.getDate() + (weekNumber - 1) * 7);
2025-04-01 17:25:03 +02:00
const startDate = getWeekStartDate(weekStartDate, optionService.getOption("firstDayOfWeek") === '0' ? 'sunday' : 'monday');
2025-04-01 16:57:52 +02:00
const monthNote = getMonthNote(dateUtils.utcDateStr(startDate), rootNote);
const noteTitle = getWeekNoteTitle(rootNote, weekNumber);
sql.transactional(() => {
weekNote = createNote(monthNote, noteTitle);
attributeService.createLabel(weekNote.noteId, WEEK_LABEL, weekStr);
attributeService.createLabel(weekNote.noteId, "sorted");
const weekTemplateAttr = rootNote.getOwnedAttribute("relation", "weekTemplate");
if (weekTemplateAttr) {
attributeService.createRelation(weekNote.noteId, "template", weekTemplateAttr.value);
}
});
return weekNote as unknown as BNote;
}
export default {
getRootCalendarNote,
getYearNote,
getMonthNote,
2025-04-01 16:57:52 +02:00
getWeekNote,
getWeekFirstDayNote,
2022-01-10 17:09:20 +01:00
getDayNote,
2019-11-27 23:07:10 +01:00
getTodayNote
2020-06-20 12:31:38 +02:00
};