fix calendar history and recurring note mode

This commit is contained in:
callumalpass 2026-06-10 06:16:26 +10:00
parent 18dc25309d
commit ba8047cce5
22 changed files with 415 additions and 4 deletions

View file

@ -39,6 +39,8 @@ TaskNotes provides Calendar and Mini Calendar views that display tasks alongside
For detailed view documentation, see [Calendar Views](../views/calendar-views.md).
Notes linked from recurring external calendar events can be matched either across the loaded event series or only to the selected recurrence instance. Configure this in [Integrations settings](../settings/integrations.md#calendar-subscriptions-ics).
## Time Entry Editor
TaskNotes includes a time entry editor for tracking time spent on tasks. Time entries are created and managed through the Calendar View.

View file

@ -39,6 +39,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
- (#2000) Added separate settings for using the active note as a project during normal task creation and during inline task creation or instant conversion. Thanks to @BlueScreen32 for suggesting the split.
- (#2001) Added separate templates for materialized occurrence notes, including parent-level `occurrence_template` support and a global fallback setting. Thanks to @BrucePlumb for suggesting a separate occurrence-note template and @ak-42 for the related per-recurrence template request.
- (#2006) Added the projected local finish time beside the remaining duration while a Pomodoro is running. See [Pomodoro View](https://tasknotes.dev/views/pomodoro-view/) for the timer behavior. Thanks to @abbiefalls90 for suggesting this.
- (#2016) Added a setting for recurring external calendar event related notes, so Google, Microsoft, and ICS recurrences can either share notes across the series or keep notes limited to the selected instance. See [Integrations Settings](https://tasknotes.dev/settings/integrations/#calendar-subscriptions-ics) for the setting. Thanks to @akhtsang for reporting this, and to @cathywu and @mdbraber for the earlier series-linking discussion.
## Changed
@ -50,6 +51,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
## Fixed
- (#2015) Fixed Google Calendar events from the previous six months disappearing from Calendar and Mini Calendar views after a fresh sync. Thanks to @akhtsang for reporting this.
- (#2013) Fixed task-card status and priority dots becoming oversized on touch-capable desktop devices. Thanks to @RumiaKitinari for reporting this.
- (#2009) Restored Tab and Shift+Tab focus movement in the Create new task modal's natural language input instead of letting the markdown editor indent the title. Thanks to @prepare4robots for reporting this.
- (#2007) Fixed status autocomplete in the task creation modal leaving status value fragments in the task title when the editor auto-paired the status trigger. Thanks to @prepare4robots for reporting this.

View file

@ -79,6 +79,8 @@ When connected, displays:
ICS settings define how subscribed calendar events are represented in your vault. You can set a default template, destination folder, filename strategy, and custom filename template for generated notes. Use **Add Calendar Subscription** to register URLs or local files, and **Refresh all subscriptions** for manual synchronization.
**Recurring event related notes** controls how notes linked from recurring external calendar events are matched. **Series-wide** keeps the current behavior: a note linked to one loaded recurrence can appear on the other loaded recurrences from the same Google, Microsoft, or ICS event series. **Selected instance only** limits related notes and counts to the exact recurrence instance that was linked.
## Automatic ICS export
Automatic export keeps an ICS feed of your tasks updated on a schedule. Configure whether it is enabled, where the file is written (vault-relative path), the refresh interval, and use **Export now** for immediate output.

View file

@ -1045,6 +1045,10 @@
"settings.integrations.calendarSubscriptions.customTemplate.placeholder": "4f73737fff77e4d11521ac76908146aae2c48b47",
"settings.integrations.calendarSubscriptions.useICSEndAsDue.name": "cc33d3d9bf36ae654cb6aab11540d95a0a074169",
"settings.integrations.calendarSubscriptions.useICSEndAsDue.description": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"settings.integrations.subscriptionsList.header": "6570967667fd5103c370ee5c88bf816abf475510",
"settings.integrations.subscriptionsList.addSubscription.name": "5212dc2d205efd2d4f8852be2835f93cbf3b1d3a",
"settings.integrations.subscriptionsList.addSubscription.description": "d62e2b3643c816028fc916796a58089540e916eb",

View file

@ -4184,6 +4184,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "17e1b7f3eeaf4948018b4da3d03fa0765b6792b9"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "26d0431c6b1d4fbde9f6062494d8ce1dfd1d145c"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "e3d399f008163245766b550cb20b3f750effd01b"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "b106811bbd969115bbab49072fb313a2fbb61de2"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "1e6ebb63a22631a37592f40fd6862698c8a8e9c4"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "7f31c6604958a07514b1355ca291b6b149350385"
@ -13018,6 +13034,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "ef66f13fd51af802465a65417c4fbc968d8065df"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "583034ffb185fc2d2ff757e5a97a006027c008da"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "42920a060f0a27411a5b28434dcb808844358e46"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "3b553b7365215f5b74c816df5b8662b7c672fda3"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "fd19bf2e806c96dcde7144fcae74b03876339b08"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "4d26de6f3ee282cb5553a56bdb3cae7457f2edc6"
@ -21852,6 +21884,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "ebb2fa5e523d1d98620788691a3e9c99ab7254ff"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "fc21c4b8924a5d32e8b0baafbafbcd4d049ba8f7"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "1f2bbca60b16d50a05e90288b11b08d41235b040"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "cfce7e8626cb61701eb2adf02e1dab7ebd8050ad"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "4b738676018e0df4dbd0736e97368a66f6a3fe22"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "ab0ed02b841e3fc482b6cc2572bb0aacba50d4f6"
@ -30686,6 +30734,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "30c209e261393b735ff1cfd2b4dc5e182faf0fe1"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "d1e5974a90ebd6f426c900985f00cbb60d8e0984"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "1f7e25d4c07b6db8c6a222deb07638c62f9f1199"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "d587fb4b6d20b259a8c85199d56f7498c8b3c95a"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "4bf0381e78f0942e9cf163426714c4d8ba06c725"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "43f35958e9ac0488d54463bce2abc1a71917c4d6"
@ -39520,6 +39584,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "01db0cffbcf915132fa6c5a05956554cb1ccdc36"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "85a891d00b0ba323bfbf6efe1d5eb65e3a552c1d"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "bdac0b86d27cbe6310765af963e1e1ea04c9060d"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "0c7a2b75271be77e1590c8bb01606b5310fefe3c"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "5c4d5ce83dc4bfaf457ca13bc954f917253d9303"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "25d76c885285ef820cb2e3e9616129622f4425b6"
@ -48354,6 +48434,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "42f546280a9de71bf8f0846b854f3fe35444a076"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "28a808925d78ff1d90917e5acb65bed78305ac12"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "857a22fc68a17ead01f8f504291afe4488822514"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "6dfdcc5bbcb771ef3c54778abb8898e23beba79d"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "d1f4a52e2e017e018d988fc4b79057a5a7b58609"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "655ab4a08d400cc9957c26c469d6e576f7801cde"
@ -57188,6 +57284,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "8be26463cb3fa45cd1f2bcf953c22d5bd081e5de"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "fb686df12603cc5639d0f23d049c864a03168e4a"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "7bd05fe4c99552dcc5726f43fdb04d0c7b44e573"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "e190d46416683f9d74d56997f065665d0e6f59f3"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "3d5e4f21054ed48237dc9e74a349d9bbe81098bf"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "8345e1e1a85332b52e1e5e2aa6f7d1d2f2005047"
@ -66022,6 +66134,22 @@
"source": "3d5f1c326bfd35bac5b8f53cc3161712110705b5",
"translation": "53827ffca8bacbb13efc6ba42dd4e4dd981a6efa"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name": {
"source": "152192be2f5028cbd69e2ad0772976b3687fc6e4",
"translation": "8818b4618fea86d63bedd4bcae17a0180b68512d"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description": {
"source": "de41f93f21d40c04bfdeaa6fbabad847dad9e0e2",
"translation": "514bba3a44e1186a6206ee42103a4fbcf551ba3e"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series": {
"source": "eba4d7adff4490ca3d6d53a5ef9ab24c08ff75fd",
"translation": "bd50bd1607ce1226753e9ef878beecaed3057e9a"
},
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance": {
"source": "9e116bcc8c8d4134ef33bb9d73d2921396d16508",
"translation": "75989dacafa2e5b32f3dc2863d6280acf43d6acb"
},
"settings.integrations.subscriptionsList.header": {
"source": "6570967667fd5103c370ee5c88bf816abf475510",
"translation": "ec15c03c5d8fa61c8800948c57fb5d5871babede"

View file

@ -1706,6 +1706,14 @@ export const de: TranslationTree = {
useICSEndAsDue: {
name: "ICS-Ereignis-Endzeit als Fälligkeitsdatum verwenden",
description: "Wenn aktiviert, wird das Fälligkeitsdatum von Aufgaben aus Kalenderereignissen auf die Endzeit des Ereignisses gesetzt. Bei ganztägigen Ereignissen wird das Fälligkeitsdatum auf das Ereignisdatum gesetzt. Bei zeitgesteuerten Ereignissen enthält das Fälligkeitsdatum die Endzeit."
},
recurringEventRelatedNotesMode: {
name: "Verknüpfte Notizen für wiederkehrende Ereignisse",
description: "Wähle, ob Notizen, die mit einer Wiederholung eines externen Kalenderereignisses verknüpft sind, in der geladenen Serie oder nur in der ausgewählten Instanz erscheinen.",
options: {
series: "Gesamte Serie",
instance: "Nur ausgewählte Instanz"
}
}
},
subscriptionsList: {

View file

@ -1825,6 +1825,15 @@ export const en: TranslationTree = {
description:
"When enabled, tasks created from calendar events will have their due date set to the event's end time. For all-day events, the due date will be set to the event date. For timed events, the due date will include the end time.",
},
recurringEventRelatedNotesMode: {
name: "Recurring event related notes",
description:
"Choose whether notes linked to one recurrence of an external calendar event appear across the loaded series or only on the selected instance.",
options: {
series: "Series-wide",
instance: "Selected instance only",
},
},
},
subscriptionsList: {
header: "Calendar subscriptions list",

View file

@ -1706,6 +1706,14 @@ export const es: TranslationTree = {
useICSEndAsDue: {
name: "Usar hora de fin del evento ICS como fecha de vencimiento",
description: "Cuando está habilitado, las tareas creadas a partir de eventos de calendario tendrán su fecha de vencimiento establecida en la hora de fin del evento. Para eventos de todo el día, la fecha de vencimiento será la fecha del evento. Para eventos con hora, la fecha de vencimiento incluirá la hora de fin."
},
recurringEventRelatedNotesMode: {
name: "Notas relacionadas de eventos recurrentes",
description: "Elige si las notas vinculadas a una repetición de un evento de calendario externo aparecen en toda la serie cargada o solo en la instancia seleccionada.",
options: {
series: "Toda la serie",
instance: "Solo la instancia seleccionada"
}
}
},
subscriptionsList: {

View file

@ -1706,6 +1706,14 @@ export const fr: TranslationTree = {
useICSEndAsDue: {
name: "Utiliser l'heure de fin de l'événement ICS comme date d'échéance",
description: "Lorsqu'activé, les tâches créées à partir d'événements de calendrier auront leur date d'échéance définie sur l'heure de fin de l'événement. Pour les événements sur toute la journée, la date d'échéance sera la date de l'événement. Pour les événements avec horaire, la date d'échéance inclura l'heure de fin."
},
recurringEventRelatedNotesMode: {
name: "Notes liées aux événements récurrents",
description: "Choisissez si les notes liées à une occurrence d'un événement de calendrier externe apparaissent sur toute la série chargée ou seulement sur l'instance sélectionnée.",
options: {
series: "Toute la série",
instance: "Instance sélectionnée uniquement"
}
}
},
subscriptionsList: {

View file

@ -1706,6 +1706,14 @@ export const ja: TranslationTree = {
useICSEndAsDue: {
name: "ICSイベント終了時刻をタスク期日として使用",
description: "有効にすると、カレンダーイベントから作成されたタスクの期日がイベントの終了時刻に設定されます。終日イベントの場合、期日はイベントの日付に設定されます。時間指定イベントの場合、期日に終了時刻が含まれます。"
},
recurringEventRelatedNotesMode: {
name: "繰り返しイベントの関連ノート",
description: "外部カレンダーイベントの1回の繰り返しにリンクしたートを、読み込まれたシリーズ全体に表示するか、選択したインスタンスのみに表示するかを選択します。",
options: {
series: "シリーズ全体",
instance: "選択したインスタンスのみ"
}
}
},
subscriptionsList: {

View file

@ -1690,6 +1690,14 @@ export const ko: TranslationTree = {
useICSEndAsDue: {
name: "ICS 이벤트 종료 시간을 작업 마감일로 사용",
description: "활성화하면 캘린더 이벤트에서 생성된 작업의 마감일이 이벤트 종료 시간으로 설정됩니다. 종일 이벤트의 경우 마감일이 이벤트 날짜로 설정됩니다. 시간이 지정된 이벤트의 경우 마감일에 종료 시간이 포함됩니다."
},
recurringEventRelatedNotesMode: {
name: "반복 이벤트 관련 노트",
description: "외부 캘린더 이벤트의 한 반복 항목에 연결된 노트를 불러온 전체 시리즈에 표시할지, 선택한 인스턴스에만 표시할지 선택합니다.",
options: {
series: "전체 시리즈",
instance: "선택한 인스턴스만"
}
}
},
subscriptionsList: {

View file

@ -1708,6 +1708,14 @@ export const pt: TranslationTree = {
useICSEndAsDue: {
name: "Usar hora de término do evento ICS como data de vencimento",
description: "Quando ativado, as tarefas criadas a partir de eventos de calendário terão sua data de vencimento definida para a hora de término do evento. Para eventos de dia inteiro, a data de vencimento será a data do evento. Para eventos com horário, a data de vencimento incluirá a hora de término."
},
recurringEventRelatedNotesMode: {
name: "Notas relacionadas de eventos recorrentes",
description: "Escolha se notas vinculadas a uma ocorrência de um evento de calendário externo aparecem em toda a série carregada ou apenas na instância selecionada.",
options: {
series: "Série inteira",
instance: "Somente instância selecionada"
}
}
},
subscriptionsList: {

View file

@ -1706,6 +1706,14 @@ export const ru: TranslationTree = {
useICSEndAsDue: {
name: "Использовать время окончания события ICS как срок выполнения",
description: "При включении задачи, созданные из событий календаря, будут иметь срок выполнения, установленный на время окончания события. Для событий на весь день срок выполнения будет установлен на дату события. Для событий с указанным временем срок выполнения будет включать время окончания."
},
recurringEventRelatedNotesMode: {
name: "Связанные заметки повторяющихся событий",
description: "Выберите, будут ли заметки, связанные с одним повторением события внешнего календаря, отображаться для всей загруженной серии или только для выбранного экземпляра.",
options: {
series: "Вся серия",
instance: "Только выбранный экземпляр"
}
}
},
subscriptionsList: {

View file

@ -1706,6 +1706,14 @@ export const zh: TranslationTree = {
useICSEndAsDue: {
name: "使用ICS事件结束时间作为任务截止日期",
description: "启用后,从日历事件创建的任务将把截止日期设置为事件的结束时间。对于全天事件,截止日期将设置为事件日期。对于有时间的事件,截止日期将包含结束时间。"
},
recurringEventRelatedNotesMode: {
name: "重复事件的关联笔记",
description: "选择关联到外部日历事件某一次重复的笔记,是显示在已加载的整个系列中,还是仅显示在所选实例上。",
options: {
series: "整个系列",
instance: "仅所选实例"
}
}
},
subscriptionsList: {

View file

@ -353,9 +353,25 @@ export class GoogleCalendarService extends CalendarProvider {
// Full sync mode - use time range and orderBy
const now = new Date();
const defaultTimeMin =
timeMin || new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
timeMin ||
new Date(
now.getTime() -
GOOGLE_CALENDAR_CONSTANTS.VIEW_RANGE.DAYS_BEFORE *
24 *
60 *
60 *
1000
);
const defaultTimeMax =
timeMax || new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000);
timeMax ||
new Date(
now.getTime() +
GOOGLE_CALENDAR_CONSTANTS.VIEW_RANGE.DAYS_AFTER *
24 *
60 *
60 *
1000
);
params.set("timeMin", defaultTimeMin.toISOString());
params.set("timeMax", defaultTimeMax.toISOString());
params.set("orderBy", "startTime");

View file

@ -95,6 +95,12 @@ export class ICSNoteService {
return seriesId || null;
}
private getRecurringRelatedNotesMode(): "series" | "instance" {
return this.plugin.settings?.icsIntegration?.recurringEventRelatedNotesMode === "instance"
? "instance"
: "series";
}
private buildEventSeriesIndex(): EventSeriesIndex {
const candidates = new Map<string, Set<string>>();
@ -125,6 +131,10 @@ export class ICSNoteService {
private getRelatedEventIds(eventId: string, seriesIndex: EventSeriesIndex): Set<string> {
const relatedIds = new Set<string>([eventId]);
if (this.getRecurringRelatedNotesMode() === "instance") {
return relatedIds;
}
const seriesId = seriesIndex.seriesIdByEventId.get(eventId);
const storedSeriesId = seriesIndex.eventIdsBySeriesId.has(eventId) ? eventId : seriesId;

View file

@ -26,8 +26,8 @@ export const GOOGLE_CALENDAR_CONSTANTS = {
/** Calendar view time range */
VIEW_RANGE: {
/** Days to look back from today */
DAYS_BEFORE: 30,
/** Days to look back from today during a fresh sync */
DAYS_BEFORE: 180,
/** Days to look ahead from today */
DAYS_AFTER: 90,
},

View file

@ -186,6 +186,7 @@ export const DEFAULT_ICS_INTEGRATION_SETTINGS: ICSIntegrationSettings = {
requireScheduledDateForExport: false, // Preserve existing behavior: include tasks without scheduled dates
// Task creation defaults
useICSEndAsDue: false, // Preserve existing behavior: don't set due date from ICS events
recurringEventRelatedNotesMode: "series", // Preserve existing behavior: link recurring event notes across the series
};
export const DEFAULT_GOOGLE_CALENDAR_EXPORT: GoogleCalendarExportSettings = {

View file

@ -85,6 +85,7 @@ function getErrorMessage(error: unknown): string {
}
type ICSNoteFilenameFormat = ICSIntegrationSettings["icsNoteFilenameFormat"];
type RecurringEventRelatedNotesMode = ICSIntegrationSettings["recurringEventRelatedNotesMode"];
const ICS_NOTE_FILENAME_FORMATS: readonly ICSNoteFilenameFormat[] = [
"title",
@ -92,11 +93,21 @@ const ICS_NOTE_FILENAME_FORMATS: readonly ICSNoteFilenameFormat[] = [
"timestamp",
"custom",
];
const RECURRING_EVENT_RELATED_NOTES_MODES: readonly RecurringEventRelatedNotesMode[] = [
"series",
"instance",
];
function isICSNoteFilenameFormat(value: string): value is ICSNoteFilenameFormat {
return ICS_NOTE_FILENAME_FORMATS.some((format) => format === value);
}
function isRecurringEventRelatedNotesMode(
value: string
): value is RecurringEventRelatedNotesMode {
return RECURRING_EVENT_RELATED_NOTES_MODES.some((mode) => mode === value);
}
function formatDefaultReminderMinutes(value: number | number[] | null | undefined): string {
if (Array.isArray(value)) {
return value.filter((minutes) => minutes > 0).join(", ");
@ -1324,6 +1335,43 @@ export function renderIntegrationsTab(
},
})
);
group.addSetting(
(setting) =>
void configureDropdownSetting(setting, {
name: translate(
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.name"
),
desc: translate(
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.description"
),
options: [
{
value: "series",
label: translate(
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.series"
),
},
{
value: "instance",
label: translate(
"settings.integrations.calendarSubscriptions.recurringEventRelatedNotesMode.options.instance"
),
},
],
getValue: () =>
plugin.settings.icsIntegration.recurringEventRelatedNotesMode ??
"series",
setValue: async (value: string) => {
if (!isRecurringEventRelatedNotesMode(value)) {
return;
}
plugin.settings.icsIntegration.recurringEventRelatedNotesMode =
value;
save();
},
})
);
}
);

View file

@ -320,6 +320,7 @@ export interface ICSIntegrationSettings {
requireScheduledDateForExport: boolean; // Whether to export only tasks with scheduled dates
// Task creation from ICS events
useICSEndAsDue: boolean; // Whether to use ICS event end time as task due date
recurringEventRelatedNotesMode: "series" | "instance"; // How linked notes behave for recurring external calendar events
}
/**

View file

@ -227,6 +227,28 @@ describe('GoogleCalendarService', () => {
// Cancelled events should be filtered out or marked
expect(events.every(e => e.id !== 'deleted-event')).toBe(true);
});
test('should include six months of historical events on a fresh full sync', async () => {
jest.useFakeTimers().setSystemTime(new Date('2026-06-09T12:00:00Z'));
mockRequestUrl.mockResolvedValueOnce({
status: 200,
json: { items: [], nextSyncToken: 'sync-token-123' },
text: '',
arrayBuffer: new ArrayBuffer(0),
headers: {}
});
await service.getEvents('primary');
const request = mockRequestUrl.mock.calls[0]?.[0];
expect(request).toBeDefined();
const params = new URL(request.url).searchParams;
expect(params.get('timeMin')).toBe('2025-12-11T12:00:00.000Z');
expect(params.get('timeMax')).toBe('2026-09-07T12:00:00.000Z');
jest.useRealTimers();
});
});
describe('createEvent', () => {

View file

@ -0,0 +1,102 @@
import { TFile } from "obsidian";
import { ICSNoteService } from "../../../src/services/ICSNoteService";
import type { ICSEvent, TaskInfo } from "../../../src/types";
import type { ICSIntegrationSettings } from "../../../src/types/settings";
function createEvent(id: string, overrides: Partial<ICSEvent> = {}): ICSEvent {
return {
id,
subscriptionId: "google-primary",
title: "Recurring Review",
start: "2026-06-09T09:00:00Z",
end: "2026-06-09T10:00:00Z",
allDay: false,
...overrides,
};
}
function createPlugin(
events: ICSEvent[],
tasks: TaskInfo[] = [],
frontmatterByPath: Record<string, unknown> = {},
mode: ICSIntegrationSettings["recurringEventRelatedNotesMode"] = "series"
) {
const files = Object.keys(frontmatterByPath).map((path) => new TFile(path));
return {
settings: {
icsIntegration: {
recurringEventRelatedNotesMode: mode,
},
},
fieldMapper: {
toUserField: jest.fn((field: string) => field),
},
cacheManager: {
getAllTasks: jest.fn().mockResolvedValue(tasks),
},
app: {
vault: {
getMarkdownFiles: jest.fn(() => files),
},
metadataCache: {
getFileCache: jest.fn((file: TFile) => ({
frontmatter: frontmatterByPath[file.path],
})),
},
},
icsSubscriptionService: {
getAllEvents: jest.fn(() => []),
getSubscriptions: jest.fn(() => []),
},
googleCalendarService: {
getAllEvents: jest.fn(() => events),
getAvailableCalendars: jest.fn(() => [{ id: "primary", summary: "Primary" }]),
},
microsoftCalendarService: {
getAllEvents: jest.fn(() => []),
},
} as unknown as ConstructorParameters<typeof ICSNoteService>[0];
}
describe("Issue #2016: recurring external calendar related note mode", () => {
const events = [
createEvent("google-primary-review-0", { recurringEventId: "google-primary-review" }),
createEvent("google-primary-review-1", { recurringEventId: "google-primary-review" }),
createEvent("google-primary-review-2", { recurringEventId: "google-primary-review" }),
];
const linkedTask = {
path: "Tasks/Recurring Review.md",
title: "Recurring Review",
status: "open",
icsEventId: ["google-primary-review-1"],
} as TaskInfo;
it("keeps recurring event related notes series-wide by default", async () => {
const service = new ICSNoteService(createPlugin(events, [linkedTask]));
const related = await service.findRelatedNotes(events[2]);
const counts = await service.getRelatedNoteCountsByEventId();
expect(related).toEqual([linkedTask]);
expect(counts.get("google-primary-review-0")).toBe(1);
expect(counts.get("google-primary-review-1")).toBe(1);
expect(counts.get("google-primary-review-2")).toBe(1);
});
it("limits recurring event related notes to the selected instance when configured", async () => {
const service = new ICSNoteService(createPlugin(events, [linkedTask], {}, "instance"));
const siblingRelated = await service.findRelatedNotes(events[2]);
const linkedInstanceRelated = await service.findRelatedNotes(events[1]);
const counts = await service.getRelatedNoteCountsByEventId();
expect(siblingRelated).toEqual([]);
expect(linkedInstanceRelated).toEqual([linkedTask]);
expect(counts.get("google-primary-review-0")).toBeUndefined();
expect(counts.get("google-primary-review-1")).toBe(1);
expect(counts.get("google-primary-review-2")).toBeUndefined();
expect(counts.get("google-primary-review")).toBeUndefined();
});
});