fix materialized occurrences in bases calendar

This commit is contained in:
callumalpass 2026-06-05 10:30:50 +10:00
parent bf1428c959
commit df9bcd8286
3 changed files with 137 additions and 4 deletions

View file

@ -31,3 +31,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
```
-->
## Fixed
- (#1995) Fixed Bases Calendar views showing both a materialized occurrence note and its matching virtual recurring task instance. Thanks to @rdefaccio for reporting this.

View file

@ -1,5 +1,6 @@
import TaskNotesPlugin from "../main";
import { Reminder, TaskDependency, TaskInfo, TimeEntry } from "../types";
import type { OccurrenceMaterializationMode, OccurrenceNextTrigger } from "@tasknotes/model";
import { setIcon } from "obsidian";
import { calculateTotalTimeSpent } from "../utils/helpers";
import { format } from "date-fns";
@ -86,6 +87,20 @@ function toOptionalNumber(value: unknown): number | undefined {
return undefined;
}
function toOccurrenceMaterializationMode(
value: unknown
): OccurrenceMaterializationMode | undefined {
const mode = toOptionalString(value);
return mode === "manual" || mode === "on_completion" || mode === "rolling"
? mode
: undefined;
}
function toOccurrenceNextTrigger(value: unknown): OccurrenceNextTrigger | undefined {
const trigger = toOptionalString(value);
return trigger === "completion" || trigger === "completion_or_skip" ? trigger : undefined;
}
function toTimeEntries(value: unknown): TimeEntry[] | undefined {
return Array.isArray(value) ? (value as TimeEntry[]) : undefined;
}
@ -166,6 +181,13 @@ function createTaskInfoFromProperties(
"timeEstimate",
"completedDate",
"recurrence",
"recurrence_parent",
"occurrence_date",
"occurrence_materialization",
"occurrence_next_trigger",
"occurrence_template",
"occurrence_past_horizon",
"occurrence_future_horizon",
"dateCreated",
"dateModified",
"timeEntries",
@ -224,6 +246,15 @@ function createTaskInfoFromProperties(
timeEstimate: toOptionalNumber(props.timeEstimate),
completedDate: toOptionalString(props.completedDate),
recurrence: toOptionalString(props.recurrence),
recurrence_parent: toOptionalString(props.recurrence_parent),
occurrence_date: toOptionalString(props.occurrence_date),
occurrence_materialization: toOccurrenceMaterializationMode(
props.occurrence_materialization
),
occurrence_next_trigger: toOccurrenceNextTrigger(props.occurrence_next_trigger),
occurrence_template: toOptionalString(props.occurrence_template),
occurrence_past_horizon: toOptionalString(props.occurrence_past_horizon),
occurrence_future_horizon: toOptionalString(props.occurrence_future_horizon),
dateCreated: toOptionalString(props.dateCreated),
dateModified: toOptionalString(props.dateModified),
timeEntries,
@ -242,6 +273,33 @@ function createTaskInfoFromProperties(
};
}
function mergeCustomProperties(
first: Record<string, unknown> | undefined,
second: Record<string, unknown> | undefined
): Record<string, unknown> | undefined {
const merged = {
...(first ?? {}),
...(second ?? {}),
};
return Object.keys(merged).length > 0 ? merged : undefined;
}
function enrichTaskInfoFromCache(taskInfo: TaskInfo, plugin?: TaskNotesPlugin): TaskInfo {
const cachedTask = plugin?.cacheManager?.getCachedTaskInfoSync?.(taskInfo.path);
if (!cachedTask) {
return taskInfo;
}
return {
...cachedTask,
basesData: taskInfo.basesData,
customProperties: mergeCustomProperties(
cachedTask.customProperties,
taskInfo.customProperties
),
};
}
export function createTaskInfoFromBasesData(
basesItem: BasesDataItem,
plugin?: TaskNotesPlugin
@ -267,16 +325,16 @@ export function createTaskInfoFromBasesData(
});
// Merge file properties with existing custom properties
return {
return enrichTaskInfoFromCache({
...taskInfo,
customProperties: {
...mappedTaskInfo.customProperties,
...taskInfo.customProperties,
...fileProperties,
},
};
}, plugin);
} else {
return createTaskInfoFromProperties(props, basesItem, plugin);
return enrichTaskInfoFromCache(createTaskInfoFromProperties(props, basesItem, plugin), plugin);
}
}

View file

@ -1,4 +1,7 @@
import { createTaskInfoFromBasesData } from "../../../src/bases/helpers";
import {
createTaskInfoFromBasesData,
identifyTaskNotesFromBasesData,
} from "../../../src/bases/helpers";
import type TaskNotesPlugin from "../../../src/main";
import { FieldMapper } from "../../../src/services/FieldMapper";
import { DEFAULT_FIELD_MAPPING } from "../../../src/settings/defaults";
@ -16,6 +19,9 @@ function createPlugin(sortOrderField = "sort_order") {
storeTitleInFilename: false,
},
dependencyCache: undefined,
cacheManager: {
getCachedTaskInfoSync: jest.fn(),
},
};
}
@ -37,4 +43,69 @@ describe("Bases TaskInfo assembly", () => {
expect(task?.sortOrder).toBe("0|hzzzzz:");
expect(task?.customProperties?.sortOrder).toBeUndefined();
});
it("preserves materialized occurrence fields on TaskInfo", () => {
const task = createTaskInfoFromBasesData(
{
path: "Tasks/Occurrence.md",
name: "Occurrence",
properties: {
title: "Occurrence",
status: "open",
scheduled: "2026-06-05T10:30",
recurrence_parent: "[[Tasks/Parent]]",
occurrence_date: "2026-06-05",
},
},
createPlugin() as unknown as TaskNotesPlugin
);
expect(task?.recurrence_parent).toBe("[[Tasks/Parent]]");
expect(task?.occurrence_date).toBe("2026-06-05");
expect(task?.customProperties?.recurrence_parent).toBeUndefined();
expect(task?.customProperties?.occurrence_date).toBeUndefined();
});
it("enriches hidden materialized occurrence fields from the TaskNotes cache", async () => {
const plugin = createPlugin() as unknown as TaskNotesPlugin;
(plugin.cacheManager.getCachedTaskInfoSync as jest.Mock).mockReturnValue({
title: "Occurrence",
status: "open",
priority: "normal",
path: "Tasks/Occurrence.md",
archived: false,
scheduled: "2026-06-05T10:30",
recurrence_parent: "[[Tasks/Parent]]",
occurrence_date: "2026-06-05",
customProperties: {
cachedOnly: true,
},
});
const tasks = await identifyTaskNotesFromBasesData(
[
{
path: "Tasks/Occurrence.md",
name: "Occurrence",
properties: {
title: "Occurrence",
status: "open",
scheduled: "2026-06-05T10:30",
"file.path": "Tasks/Occurrence.md",
},
basesData: { source: "bases-entry" },
},
],
plugin
);
expect(tasks).toHaveLength(1);
expect(tasks[0].recurrence_parent).toBe("[[Tasks/Parent]]");
expect(tasks[0].occurrence_date).toBe("2026-06-05");
expect(tasks[0].basesData).toEqual({ source: "bases-entry" });
expect(tasks[0].customProperties).toMatchObject({
cachedOnly: true,
"file.path": "Tasks/Occurrence.md",
});
});
});