Add calendar daily note link option

This commit is contained in:
callumalpass 2026-05-17 22:00:30 +10:00
parent c2bdcfda36
commit 821586f1a3
17 changed files with 179 additions and 394 deletions

View file

@ -86,6 +86,7 @@ Example:
- ([#1318](https://github.com/callumalpass/tasknotes/issues/1318)) Made aliased inline task links use the alias text inside the task widget, so `[[Task title|short label]]` can display as `short label`. Thanks to @3zra47 for suggesting this and @kazerniel for the follow-up.
- ([#1325](https://github.com/callumalpass/tasknotes/issues/1325)) Added `./` and `../` support to Include folders filters for project and file autosuggest, resolved from the active note's folder. Thanks to @EpolPers for suggesting this.
- ([#1313](https://github.com/callumalpass/tasknotes/issues/1313)) Added generated `llms.txt` and `llms-full.txt` files to the documentation site, so the primary docs can be shared with LLM tools more easily. Thanks to @JacksonMcDonaldDev for suggesting this.
- ([#1308](https://github.com/callumalpass/tasknotes/issues/1308)) Added a Calendar view option to stop date links from creating missing daily notes, while still opening existing daily notes. Thanks to @Arachnidai for suggesting this.
## Fixed

View file

@ -130,6 +130,7 @@ The Calendar View provides several display options that control what types of ev
- **Show ICS events**: Display events from imported ICS calendars
- **Show time entries**: Display time tracking entries
- **All-day slot**: Show or hide the all-day event area at the top of time grid views (Week, Day, and Custom views)
- **Create daily notes from date links**: Let date header links create a missing daily note before opening it. Turn this off if date links should only open existing daily notes.
- **Span tasks between scheduled and due dates**: Display tasks as multi-day bars spanning from their scheduled date to their due date (see below)
Multiple saved calendar views can store different option sets (for example planning vs focus), avoiding repeated manual toggles.

View file

@ -1,7 +1,7 @@
---
title: Default Base Templates
description: Default base file templates for TaskNotes views
dateModified: 2026-05-17T15:55:06+1000
dateModified: 2026-05-17T21:57:48+1000
---
# Default Base Templates
@ -520,6 +520,7 @@ views:
showTimeEntries: true
showTimeblocks: true
showPropertyBasedEvents: true
createDailyNotesFromDateLinks: true
calendarView: "timeGridWeek"
customDayCount: 3
firstDay: 0
@ -562,6 +563,7 @@ views:
- file.tasks
options:
showPropertyBasedEvents: false
createDailyNotesFromDateLinks: true
calendarView: "listWeek"
startDateProperty: file.ctime
listDayCount: 7

View file

@ -612,6 +612,7 @@ export class CalendarView extends BasesViewBase {
initialDate: string;
initialDateProperty: string | null;
initialDateStrategy: "first" | "earliest" | "latest";
createDailyNotesFromDateLinks: boolean;
// Layout
calendarView: string;
@ -676,6 +677,7 @@ export class CalendarView extends BasesViewBase {
initialDate: "",
initialDateProperty: null as string | null,
initialDateStrategy: "first",
createDailyNotesFromDateLinks: true,
// Layout
calendarView: calendarSettings.defaultView,
@ -855,6 +857,7 @@ export class CalendarView extends BasesViewBase {
read("initialDate"),
read("initialDateProperty"),
read("initialDateStrategy"),
read("createDailyNotesFromDateLinks"),
];
// Include ICS calendar toggles
@ -1107,6 +1110,10 @@ export class CalendarView extends BasesViewBase {
"initialDateStrategy",
this.viewOptions.initialDateStrategy
);
this.viewOptions.createDailyNotesFromDateLinks = this.getConfigOption(
"createDailyNotesFromDateLinks",
this.viewOptions.createDailyNotesFromDateLinks
);
// Layout
this.viewOptions.calendarView = this.getConfigOption(
@ -1438,10 +1445,12 @@ export class CalendarView extends BasesViewBase {
eventMaxStack: this.viewOptions.eventMaxStack ?? undefined,
navLinks: true,
navLinkDayClick: (date: Date) => {
void handleDateTitleClick(date, this.plugin);
this.openDailyNoteFromDateLink(date);
},
dayHeaderDidMount: (arg) => {
attachDailyNoteHeaderLink(arg.el, arg.date, arg.view.type, this.plugin);
attachDailyNoteHeaderLink(arg.el, arg.date, arg.view.type, this.plugin, (date) =>
this.openDailyNoteFromDateLink(date)
);
},
editable: true,
droppable: true,
@ -1572,7 +1581,15 @@ export class CalendarView extends BasesViewBase {
const date = headerCell.dataset.date
? parseDateToLocal(headerCell.dataset.date)
: fallbackDate;
attachDailyNoteHeaderLink(headerCell, date, "timeGridDay", this.plugin);
attachDailyNoteHeaderLink(headerCell, date, "timeGridDay", this.plugin, (date) =>
this.openDailyNoteFromDateLink(date)
);
});
}
private openDailyNoteFromDateLink(date: Date): void {
void handleDateTitleClick(date, this.plugin, {
createIfMissing: this.viewOptions.createDailyNotesFromDateLinks,
});
}

View file

@ -1671,11 +1671,21 @@ export function addTaskHoverPreview(
});
}
export interface DateTitleClickOptions {
createIfMissing?: boolean;
}
/**
* Handle clicking on a date title to open/create daily note
*/
export async function handleDateTitleClick(date: Date, plugin: TaskNotesPlugin): Promise<void> {
export async function handleDateTitleClick(
date: Date,
plugin: TaskNotesPlugin,
options: DateTitleClickOptions = {}
): Promise<void> {
try {
const { createIfMissing = true } = options;
// Check if Daily Notes plugin is enabled
if (!appHasDailyNotesPluginLoaded()) {
new Notice(
@ -1692,6 +1702,13 @@ export async function handleDateTitleClick(date: Date, plugin: TaskNotesPlugin):
let dailyNote = getDailyNote(moment, allDailyNotes);
if (!dailyNote) {
if (!createIfMissing) {
new Notice(
plugin.i18n.translate("views.basesCalendar.notices.noDailyNoteForDate")
);
return;
}
// Daily note doesn't exist, create it
try {
dailyNote = await createDailyNote(moment);

View file

@ -248,6 +248,12 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise<vo
"latest": t("dateNavigation.strategies.latest"),
},
},
{
type: "toggle",
key: "createDailyNotesFromDateLinks",
displayName: t("dateNavigation.createDailyNotesFromDateLinks"),
default: true,
},
],
},
{

View file

@ -219,6 +219,7 @@ export const de: TranslationTree = {
navigateToDateFromProperty: "Zum Datum aus Eigenschaft navigieren",
navigateToDateFromPropertyPlaceholder: "Datumseigenschaft auswählen (optional)",
propertyNavigationStrategy: "Eigenschaftsnavigationsstrategie",
createDailyNotesFromDateLinks: "Tägliche Notizen über Datumslinks erstellen",
strategies: {
first: "Erstes Ergebnis",
earliest: "Frühestes Datum",
@ -273,6 +274,9 @@ export const de: TranslationTree = {
titlePropertyPlaceholder: "Eigenschaft für Ereignistitel auswählen",
},
},
notices: {
noDailyNoteForDate: "Für dieses Datum ist keine tägliche Notiz vorhanden.",
},
errors: {
failedToInitialize: "Fehler beim Initialisieren des Kalenders",
},

View file

@ -228,6 +228,7 @@ export const en: TranslationTree = {
navigateToDateFromProperty: "Navigate to date from property",
navigateToDateFromPropertyPlaceholder: "Select a date property (optional)",
propertyNavigationStrategy: "Property navigation strategy",
createDailyNotesFromDateLinks: "Create daily notes from date links",
strategies: {
first: "First result",
earliest: "Earliest date",
@ -287,6 +288,9 @@ export const en: TranslationTree = {
titlePropertyPlaceholder: "Select property for event title",
},
},
notices: {
noDailyNoteForDate: "No daily note exists for this date.",
},
errors: {
failedToInitialize: "Failed to initialize calendar",
},

View file

@ -219,6 +219,7 @@ export const es: TranslationTree = {
navigateToDateFromProperty: "Navegar a la fecha desde la propiedad",
navigateToDateFromPropertyPlaceholder: "Seleccionar una propiedad de fecha (opcional)",
propertyNavigationStrategy: "Estrategia de navegación por propiedad",
createDailyNotesFromDateLinks: "Crear notas diarias desde enlaces de fecha",
strategies: {
first: "Primer resultado",
earliest: "Fecha más antigua",
@ -273,6 +274,9 @@ export const es: TranslationTree = {
titlePropertyPlaceholder: "Seleccionar propiedad para título del evento",
},
},
notices: {
noDailyNoteForDate: "No existe una nota diaria para esta fecha.",
},
errors: {
failedToInitialize: "Error al inicializar el calendario",
},

View file

@ -219,6 +219,7 @@ export const fr: TranslationTree = {
navigateToDateFromProperty: "Naviguer vers la date depuis la propriété",
navigateToDateFromPropertyPlaceholder: "Sélectionner une propriété de date (facultatif)",
propertyNavigationStrategy: "Stratégie de navigation par propriété",
createDailyNotesFromDateLinks: "Créer des notes quotidiennes depuis les liens de date",
strategies: {
first: "Premier résultat",
earliest: "Date la plus ancienne",
@ -273,6 +274,9 @@ export const fr: TranslationTree = {
titlePropertyPlaceholder: "Sélectionner une propriété pour le titre de l'événement",
},
},
notices: {
noDailyNoteForDate: "Aucune note quotidienne n'existe pour cette date.",
},
errors: {
failedToInitialize: "Échec de l'initialisation du calendrier",
},

View file

@ -219,6 +219,7 @@ export const ja: TranslationTree = {
navigateToDateFromProperty: "プロパティから日付に移動",
navigateToDateFromPropertyPlaceholder: "日付プロパティを選択(オプション)",
propertyNavigationStrategy: "プロパティナビゲーション戦略",
createDailyNotesFromDateLinks: "日付リンクからデイリーノートを作成",
strategies: {
first: "最初の結果",
earliest: "最も古い日付",
@ -273,6 +274,9 @@ export const ja: TranslationTree = {
titlePropertyPlaceholder: "イベントタイトルのプロパティを選択",
},
},
notices: {
noDailyNoteForDate: "この日付のデイリーノートはありません。",
},
errors: {
failedToInitialize: "カレンダーの初期化に失敗しました",
},

View file

@ -219,6 +219,7 @@ export const ko: TranslationTree = {
navigateToDateFromProperty: "속성에서 날짜로 이동",
navigateToDateFromPropertyPlaceholder: "날짜 속성 선택 (선택사항)",
propertyNavigationStrategy: "속성 탐색 전략",
createDailyNotesFromDateLinks: "날짜 링크에서 데일리 노트 만들기",
strategies: {
first: "첫 번째 결과",
earliest: "가장 이른 날짜",
@ -273,6 +274,9 @@ export const ko: TranslationTree = {
titlePropertyPlaceholder: "이벤트 제목 속성 선택",
},
},
notices: {
noDailyNoteForDate: "이 날짜의 데일리 노트가 없습니다.",
},
errors: {
failedToInitialize: "캘린더 초기화 실패",
},

View file

@ -219,6 +219,7 @@ export const pt: TranslationTree = {
navigateToDateFromProperty: "Navegar para data da propriedade",
navigateToDateFromPropertyPlaceholder: "Selecione uma propriedade de data (opcional)",
propertyNavigationStrategy: "Estratégia de navegação da propriedade",
createDailyNotesFromDateLinks: "Criar notas diárias a partir de links de data",
strategies: {
first: "Primeiro resultado",
earliest: "Data mais antiga",
@ -273,6 +274,9 @@ export const pt: TranslationTree = {
titlePropertyPlaceholder: "Selecione a propriedade para o título do evento"
}
},
notices: {
noDailyNoteForDate: "Não existe nota diária para esta data.",
},
errors: {
failedToInitialize: "Falha ao inicializar o calendário"
}

View file

@ -219,6 +219,7 @@ export const ru: TranslationTree = {
navigateToDateFromProperty: "Перейти к дате из свойства",
navigateToDateFromPropertyPlaceholder: "Выберите свойство даты (необязательно)",
propertyNavigationStrategy: "Стратегия навигации по свойству",
createDailyNotesFromDateLinks: "Создавать ежедневные заметки из ссылок дат",
strategies: {
first: "Первый результат",
earliest: "Самая ранняя дата",
@ -273,6 +274,9 @@ export const ru: TranslationTree = {
titlePropertyPlaceholder: "Выберите свойство для заголовка события",
},
},
notices: {
noDailyNoteForDate: "Для этой даты нет ежедневной заметки.",
},
errors: {
failedToInitialize: "Не удалось инициализировать календарь",
},

View file

@ -219,6 +219,7 @@ export const zh: TranslationTree = {
navigateToDateFromProperty: "从属性导航到日期",
navigateToDateFromPropertyPlaceholder: "选择日期属性(可选)",
propertyNavigationStrategy: "属性导航策略",
createDailyNotesFromDateLinks: "通过日期链接创建每日笔记",
strategies: {
first: "第一个结果",
earliest: "最早日期",
@ -273,6 +274,9 @@ export const zh: TranslationTree = {
titlePropertyPlaceholder: "选择事件标题的属性",
},
},
notices: {
noDailyNoteForDate: "此日期没有每日笔记。",
},
errors: {
failedToInitialize: "初始化日历失败",
},

View file

@ -790,6 +790,7 @@ ${orderYaml}
showTimeEntries: true
showTimeblocks: true
showPropertyBasedEvents: true
createDailyNotesFromDateLinks: true
calendarView: "timeGridWeek"
customDayCount: 3
firstDay: 0
@ -812,6 +813,7 @@ views:
${orderYaml}
options:
showPropertyBasedEvents: false
createDailyNotesFromDateLinks: true
calendarView: "listWeek"
startDateProperty: file.ctime
listDayCount: 7

View file

@ -1,410 +1,113 @@
/**
* Issue #1308: [FR] Improving inconvenient links in calendar view
*
* Feature Request:
* In the calendar view, date headers are clickable links. Clicking them creates
* daily notes even when no note exists for that date. Users want an option to
* disable this behavior - either preventing note creation entirely or making
* date links only work when a note already exists.
*
* Current behavior:
* - CalendarView.ts line 734-735: navLinks: true, navLinkDayClick: handleDateTitleClick
* - handleDateTitleClick (calendar-core.ts:1314-1352) unconditionally creates daily notes
* - MiniCalendarView has different behavior - only Ctrl/Cmd+click creates notes
*
* Expected behavior (with new option):
* - Add a viewOption to control date link behavior (e.g., 'navLinksCreateNote')
* - When disabled: clicking date without existing note should NOT create a note
* - Could show a notice or just do nothing when no note exists
*/
import { Notice, type TFile } from "obsidian";
import {
appHasDailyNotesPluginLoaded,
createDailyNote,
getAllDailyNotes,
getDailyNote,
} from "obsidian-daily-notes-interface";
import { handleDateTitleClick } from "../../../src/bases/calendar-core";
import type { TFile } from "obsidian";
jest.mock("obsidian");
jest.mock("obsidian-daily-notes-interface", () => ({
appHasDailyNotesPluginLoaded: jest.fn(),
createDailyNote: jest.fn(),
getAllDailyNotes: jest.fn(),
getDailyNote: jest.fn(),
}));
describe("Issue #1308 - Calendar date links creating unwanted notes", () => {
/**
* Mock of daily notes plugin interface
*/
interface MockDailyNotesApi {
getAllDailyNotes: () => Record<string, TFile>;
getDailyNote: (moment: unknown, allNotes: Record<string, TFile>) => TFile | null;
createDailyNote: (moment: unknown) => Promise<TFile>;
appHasDailyNotesPluginLoaded: () => boolean;
}
/**
* Simulates the current handleDateTitleClick behavior
* This creates notes unconditionally when they don't exist
*/
async function handleDateTitleClickCurrent(
date: Date,
dailyNotesApi: MockDailyNotesApi,
openFile: (file: TFile) => Promise<void>
): Promise<{ noteCreated: boolean; noteOpened: boolean }> {
if (!dailyNotesApi.appHasDailyNotesPluginLoaded()) {
return { noteCreated: false, noteOpened: false };
}
const moment = { toDate: () => date };
const allDailyNotes = dailyNotesApi.getAllDailyNotes();
let dailyNote = dailyNotesApi.getDailyNote(moment, allDailyNotes);
let noteCreated = false;
if (!dailyNote) {
// Current behavior: unconditionally creates the note
dailyNote = await dailyNotesApi.createDailyNote(moment);
noteCreated = true;
}
if (dailyNote) {
await openFile(dailyNote);
return { noteCreated, noteOpened: true };
}
return { noteCreated: false, noteOpened: false };
}
/**
* Simulates the desired handleDateTitleClick behavior with new option
* When navLinksCreateNote is false, it should not create notes
*/
async function handleDateTitleClickFixed(
date: Date,
dailyNotesApi: MockDailyNotesApi,
openFile: (file: TFile) => Promise<void>,
options: { navLinksCreateNote: boolean }
): Promise<{ noteCreated: boolean; noteOpened: boolean; noNoteNotice: boolean }> {
if (!dailyNotesApi.appHasDailyNotesPluginLoaded()) {
return { noteCreated: false, noteOpened: false, noNoteNotice: false };
}
const moment = { toDate: () => date };
const allDailyNotes = dailyNotesApi.getAllDailyNotes();
let dailyNote = dailyNotesApi.getDailyNote(moment, allDailyNotes);
let noteCreated = false;
let noNoteNotice = false;
if (!dailyNote) {
if (options.navLinksCreateNote) {
// Create note only if option is enabled
dailyNote = await dailyNotesApi.createDailyNote(moment);
noteCreated = true;
} else {
// Don't create note, optionally show notice
noNoteNotice = true;
return { noteCreated: false, noteOpened: false, noNoteNotice: true };
}
}
if (dailyNote) {
await openFile(dailyNote);
return { noteCreated, noteOpened: true, noNoteNotice };
}
return { noteCreated: false, noteOpened: false, noNoteNotice };
}
/**
* Creates a mock daily notes API
*/
function createMockDailyNotesApi(existingNotes: Record<string, TFile>): MockDailyNotesApi {
let notesStore = { ...existingNotes };
return {
getAllDailyNotes: () => notesStore,
getDailyNote: (_moment: unknown, allNotes: Record<string, TFile>) => {
const keys = Object.keys(allNotes);
return keys.length > 0 ? allNotes[keys[0]] : null;
},
createDailyNote: async (_moment: unknown): Promise<TFile> => {
const newNote = {
path: "daily/2024-01-15.md",
basename: "2024-01-15",
name: "2024-01-15.md",
extension: "md",
} as TFile;
notesStore["2024-01-15"] = newNote;
return newNote;
},
appHasDailyNotesPluginLoaded: () => true,
type MockPlugin = {
app: {
workspace: {
getLeaf: jest.Mock;
};
}
};
i18n: {
translate: jest.Mock;
};
};
describe("Bug reproduction - current behavior creates unwanted notes", () => {
test("clicking date with no existing note creates a new note (current unwanted behavior)", async () => {
const dailyNotesApi = createMockDailyNotesApi({});
const openedFiles: TFile[] = [];
const openFile = async (file: TFile) => {
openedFiles.push(file);
};
const mockedAppHasDailyNotesPluginLoaded =
appHasDailyNotesPluginLoaded as jest.MockedFunction<typeof appHasDailyNotesPluginLoaded>;
const mockedCreateDailyNote = createDailyNote as jest.MockedFunction<typeof createDailyNote>;
const mockedGetAllDailyNotes = getAllDailyNotes as jest.MockedFunction<typeof getAllDailyNotes>;
const mockedGetDailyNote = getDailyNote as jest.MockedFunction<typeof getDailyNote>;
const date = new Date(2024, 0, 15); // Jan 15, 2024 - no note exists
function createDailyNoteFile(path: string): TFile {
return {
path,
name: path.split("/").pop() ?? path,
basename: path.split("/").pop()?.replace(/\.md$/, "") ?? path,
extension: "md",
} as TFile;
}
const result = await handleDateTitleClickCurrent(date, dailyNotesApi, openFile);
// Current behavior: note is created even though user might not want it
expect(result.noteCreated).toBe(true);
expect(result.noteOpened).toBe(true);
// This is the unwanted behavior - a note was created just from clicking the date
expect(Object.keys(dailyNotesApi.getAllDailyNotes())).toHaveLength(1);
});
test("no option exists to prevent note creation on date click", () => {
// This test documents that there's currently no option to control this behavior
const viewOptions = {
showScheduled: true,
showDue: true,
showScheduledToDueSpan: false,
showRecurring: true,
showTimeEntries: false,
showTimeblocks: false,
showPropertyBasedEvents: false,
initialDate: "",
initialDateProperty: null,
initialDateStrategy: "first" as const,
calendarView: "dayGridMonth",
customDayCount: 3,
listDayCount: 30,
slotMinTime: "00:00:00",
slotMaxTime: "24:00:00",
slotDuration: "00:30:00",
firstDay: 0,
weekNumbers: false,
nowIndicator: true,
showWeekends: true,
showAllDaySlot: true,
showTodayHighlight: true,
selectMirror: true,
timeFormat: "24",
scrollTime: "08:00:00",
eventMinHeight: 0,
slotEventOverlap: true,
eventMaxStack: null,
dayMaxEvents: false,
dayMaxEventRows: false,
locale: "en",
startDateProperty: null,
endDateProperty: null,
titleProperty: null,
};
// Currently there is no navLinksCreateNote option
// @ts-expect-error - Property doesn't exist yet
expect(viewOptions.navLinksCreateNote).toBeUndefined();
// This test should fail when the option is added
expect("navLinksCreateNote" in viewOptions).toBe(false);
});
});
describe("Expected behavior - with navLinksCreateNote option", () => {
test("clicking date with navLinksCreateNote=true should create note (backwards compatible)", async () => {
const dailyNotesApi = createMockDailyNotesApi({});
const openedFiles: TFile[] = [];
const openFile = async (file: TFile) => {
openedFiles.push(file);
};
const date = new Date(2024, 0, 15);
const result = await handleDateTitleClickFixed(date, dailyNotesApi, openFile, {
navLinksCreateNote: true,
});
// With option enabled, should still create notes (backwards compatible)
expect(result.noteCreated).toBe(true);
expect(result.noteOpened).toBe(true);
expect(result.noNoteNotice).toBe(false);
});
test("clicking date with navLinksCreateNote=false should NOT create note", async () => {
const dailyNotesApi = createMockDailyNotesApi({});
const openedFiles: TFile[] = [];
const openFile = async (file: TFile) => {
openedFiles.push(file);
};
const date = new Date(2024, 0, 15);
const result = await handleDateTitleClickFixed(date, dailyNotesApi, openFile, {
navLinksCreateNote: false,
});
// With option disabled, should NOT create notes
expect(result.noteCreated).toBe(false);
expect(result.noteOpened).toBe(false);
expect(result.noNoteNotice).toBe(true);
// No note should have been created
expect(Object.keys(dailyNotesApi.getAllDailyNotes())).toHaveLength(0);
});
test("clicking date with existing note should open it regardless of option", async () => {
const existingNote = {
path: "daily/2024-01-15.md",
basename: "2024-01-15",
name: "2024-01-15.md",
extension: "md",
} as TFile;
const dailyNotesApi = createMockDailyNotesApi({ "2024-01-15": existingNote });
const openedFiles: TFile[] = [];
const openFile = async (file: TFile) => {
openedFiles.push(file);
};
const date = new Date(2024, 0, 15);
// Even with navLinksCreateNote=false, should open existing notes
const result = await handleDateTitleClickFixed(date, dailyNotesApi, openFile, {
navLinksCreateNote: false,
});
expect(result.noteCreated).toBe(false);
expect(result.noteOpened).toBe(true);
expect(result.noNoteNotice).toBe(false);
expect(openedFiles).toHaveLength(1);
expect(openedFiles[0].path).toBe("daily/2024-01-15.md");
});
});
describe("Alternative approaches", () => {
/**
* Alternative: Disable navLinks entirely when option is off
* This would make dates non-clickable instead of clickable-but-no-action
*/
test("alternative: disable navLinks when navLinksDisableWithoutNote=true", () => {
// This tests an alternative approach: conditionally disable navLinks
// based on whether notes exist for visible dates
interface CalendarConfig {
navLinks: boolean;
navLinksDisableWithoutNote: boolean;
}
function computeNavLinks(
config: CalendarConfig,
hasAnyDailyNotes: boolean
): boolean {
if (config.navLinksDisableWithoutNote && !hasAnyDailyNotes) {
return false;
function createPlugin(openFile = jest.fn().mockResolvedValue(undefined)): MockPlugin {
return {
app: {
workspace: {
getLeaf: jest.fn(() => ({ openFile })),
},
},
i18n: {
translate: jest.fn((key: string) => {
if (key === "views.basesCalendar.notices.noDailyNoteForDate") {
return "No daily note exists for this date.";
}
return config.navLinks;
}
return key;
}),
},
};
}
// Default behavior: navLinks enabled
expect(
computeNavLinks({ navLinks: true, navLinksDisableWithoutNote: false }, false)
).toBe(true);
// With option: navLinks disabled when no daily notes exist
expect(
computeNavLinks({ navLinks: true, navLinksDisableWithoutNote: true }, false)
).toBe(false);
// With option but notes exist: navLinks still enabled
expect(
computeNavLinks({ navLinks: true, navLinksDisableWithoutNote: true }, true)
).toBe(true);
});
/**
* Alternative: Only show visual link styling on dates with notes
* This requires per-date evaluation which is more complex
*/
test("alternative: per-date navLink styling based on note existence", () => {
const existingNoteDates = new Set(["2024-01-10", "2024-01-15", "2024-01-20"]);
function shouldShowDateAsLink(dateStr: string): boolean {
return existingNoteDates.has(dateStr);
}
// Dates with notes should be styled as links
expect(shouldShowDateAsLink("2024-01-15")).toBe(true);
// Dates without notes should not be styled as links
expect(shouldShowDateAsLink("2024-01-16")).toBe(false);
// This would require modifying FullCalendar's rendering, which may be complex
});
describe("Issue #1308: Calendar date links creating unwanted daily notes", () => {
beforeEach(() => {
jest.clearAllMocks();
mockedAppHasDailyNotesPluginLoaded.mockReturnValue(true);
mockedGetAllDailyNotes.mockReturnValue({});
(window as unknown as { moment: jest.Mock }).moment = jest.fn((input: unknown) => ({
input,
}));
});
describe("Integration with MiniCalendarView", () => {
/**
* MiniCalendarView already has different behavior:
* - Regular click: shows note selector if notes exist, does nothing otherwise
* - Ctrl/Cmd+click: opens/creates daily note
*
* This could be a model for CalendarView behavior
*/
test("MiniCalendarView only creates notes on Ctrl/Cmd+click, not regular click", () => {
// This documents the existing MiniCalendarView behavior as a reference
it("keeps the existing behavior by creating missing daily notes by default", async () => {
const dailyNote = createDailyNoteFile("Daily/2026-03-16.md");
const openFile = jest.fn().mockResolvedValue(undefined);
const plugin = createPlugin(openFile);
mockedGetDailyNote.mockReturnValue(null);
mockedCreateDailyNote.mockResolvedValue(dailyNote);
interface ClickEvent {
ctrlKey: boolean;
metaKey: boolean;
}
await handleDateTitleClick(new Date(2026, 2, 16), plugin as never);
function miniCalendarShouldCreateNote(event: ClickEvent): boolean {
// MiniCalendarView only creates notes with modifier keys
return event.ctrlKey || event.metaKey;
}
expect(mockedCreateDailyNote).toHaveBeenCalledTimes(1);
expect(openFile).toHaveBeenCalledWith(dailyNote);
expect(Notice).not.toHaveBeenCalledWith("No daily note exists for this date.");
});
// Regular click: should NOT create note
expect(miniCalendarShouldCreateNote({ ctrlKey: false, metaKey: false })).toBe(false);
it("does not create a missing daily note when date-link creation is disabled", async () => {
const openFile = jest.fn().mockResolvedValue(undefined);
const plugin = createPlugin(openFile);
mockedGetDailyNote.mockReturnValue(null);
// Ctrl+click: should create note
expect(miniCalendarShouldCreateNote({ ctrlKey: true, metaKey: false })).toBe(true);
// Cmd+click (macOS): should create note
expect(miniCalendarShouldCreateNote({ ctrlKey: false, metaKey: true })).toBe(true);
await handleDateTitleClick(new Date(2026, 2, 16), plugin as never, {
createIfMissing: false,
});
test("CalendarView should consider adopting MiniCalendarView's click behavior", () => {
// Feature request: CalendarView could adopt similar behavior
// where regular clicks don't create notes but modifier clicks do
expect(mockedCreateDailyNote).not.toHaveBeenCalled();
expect(openFile).not.toHaveBeenCalled();
expect(Notice).toHaveBeenCalledWith("No daily note exists for this date.");
});
interface CalendarViewOptions {
navLinksCreateNote: "always" | "never" | "modifierOnly";
}
it("opens existing daily notes even when date-link creation is disabled", async () => {
const dailyNote = createDailyNoteFile("Daily/2026-03-16.md");
const openFile = jest.fn().mockResolvedValue(undefined);
const plugin = createPlugin(openFile);
mockedGetDailyNote.mockReturnValue(dailyNote);
function shouldCreateNoteOnClick(
options: CalendarViewOptions,
event: { ctrlKey: boolean; metaKey: boolean }
): boolean {
switch (options.navLinksCreateNote) {
case "always":
return true;
case "never":
return false;
case "modifierOnly":
return event.ctrlKey || event.metaKey;
}
}
const regularClick = { ctrlKey: false, metaKey: false };
const ctrlClick = { ctrlKey: true, metaKey: false };
// always: create on any click
expect(shouldCreateNoteOnClick({ navLinksCreateNote: "always" }, regularClick)).toBe(
true
);
expect(shouldCreateNoteOnClick({ navLinksCreateNote: "always" }, ctrlClick)).toBe(true);
// never: never create
expect(shouldCreateNoteOnClick({ navLinksCreateNote: "never" }, regularClick)).toBe(
false
);
expect(shouldCreateNoteOnClick({ navLinksCreateNote: "never" }, ctrlClick)).toBe(false);
// modifierOnly: only create with Ctrl/Cmd
expect(
shouldCreateNoteOnClick({ navLinksCreateNote: "modifierOnly" }, regularClick)
).toBe(false);
expect(
shouldCreateNoteOnClick({ navLinksCreateNote: "modifierOnly" }, ctrlClick)
).toBe(true);
await handleDateTitleClick(new Date(2026, 2, 16), plugin as never, {
createIfMissing: false,
});
expect(mockedCreateDailyNote).not.toHaveBeenCalled();
expect(openFile).toHaveBeenCalledWith(dailyNote);
});
});