mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Add calendar daily note link option
This commit is contained in:
parent
c2bdcfda36
commit
821586f1a3
17 changed files with 179 additions and 394 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: "カレンダーの初期化に失敗しました",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: "캘린더 초기화 실패",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: "Не удалось инициализировать календарь",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: "初始化日历失败",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue