fix same-day calendar due duplicates

This commit is contained in:
callumalpass 2026-06-01 04:50:49 +10:00
parent febf68728a
commit d7a18401d6
3 changed files with 129 additions and 3 deletions

View file

@ -48,6 +48,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
## Fixed
- (#1976) Fixed embedded Calendar and Agenda Bases so "Navigate to date from property" can use the containing note's date property when the Base rows do not have that property. Thanks to @matesvecenik for reporting this.
- (#1977) Fixed Calendar and Agenda views showing duplicate all-day entries when a task's scheduled date and due date are the same day. Timed due dates on the same day still show as separate deadline markers. Thanks to @pdgBC for reporting this.
- (#216) Improved inline conversion for Tasks plugin task lines, including Dataview-style fields, priority markers, recurrence text, date aliases, block links, and safer trailing-field parsing. Thanks to @ksdavidc for the request, @natleahh for the Dataview example, and @hangryscribe3 and @nayatiuh for the discussion.
- (#1974) Stopped Calendar and Agenda property-based events from logging date parse errors for entries that do not have the selected date property. Thanks to @Jomo94 for suggesting completed-date Agenda events.
- (#1973) Reduced unnecessary Calendar view recreations when external calendar providers are reported in a different order, and preserved Calendar scroll position when a config-driven refresh has to recreate the view. Thanks to @e-zz for reporting this.

View file

@ -840,6 +840,15 @@ function replaceDatePartPreservingTime(value: string, datePart: string): string
return timePart ? `${datePart}T${timePart}` : datePart;
}
function hasDateOnlyDueOnScheduledDay(task: TaskInfo): boolean {
return Boolean(
task.scheduled &&
task.due &&
!hasTimeComponent(task.due) &&
getDatePart(task.scheduled) === getDatePart(task.due)
);
}
function createRecurringScheduledToDueSpanEvents(
task: TaskInfo,
instanceDate: string,
@ -1505,7 +1514,8 @@ export async function generateCalendarEvents(
task: TaskInfo,
includeScheduled: boolean,
allowScheduledToDueSpan: boolean,
includeDue = showDue
includeDue = showDue,
hasGeneratedScheduledLayer = false
): void => {
let showedSpan = false;
if (allowScheduledToDueSpan && showScheduledToDueSpan && task.scheduled && task.due) {
@ -1534,7 +1544,13 @@ export async function generateCalendarEvents(
}
}
if (includeDue && task.due) {
const shouldSuppressDateOnlyDue =
includeDue &&
(includeScheduled || hasGeneratedScheduledLayer) &&
hasDateOnlyDueOnScheduledDay(task);
const shouldShowDue = includeDue && !shouldSuppressDateOnlyDue;
if (shouldShowDue && task.due) {
if (isDateInVisibleRange(task.due, visibleStart, visibleEnd)) {
const dueEvent = createDueEvent(task, plugin);
if (dueEvent) {
@ -1551,6 +1567,7 @@ export async function generateCalendarEvents(
let includeStandaloneScheduled = showScheduled;
let includeStandaloneDue = showDue;
let allowScheduledToDueSpan = true;
let hasGeneratedScheduledLayer = false;
if (
(showRecurring ||
@ -1578,6 +1595,7 @@ export async function generateCalendarEvents(
);
events.push(...recurringEvents);
if (showRecurring) {
hasGeneratedScheduledLayer = recurringEvents.length > 0;
includeStandaloneScheduled = false;
allowScheduledToDueSpan = false;
if (
@ -1596,7 +1614,8 @@ export async function generateCalendarEvents(
task,
includeStandaloneScheduled,
allowScheduledToDueSpan,
includeStandaloneDue
includeStandaloneDue,
hasGeneratedScheduledLayer
);
} else {
// Handle non-recurring tasks with date range filtering

View file

@ -0,0 +1,106 @@
/**
* Issue #1977: Calendar and Agenda should not duplicate date-only tasks when
* the scheduled and due dates are the same day.
*
* @see https://github.com/callumalpass/tasknotes/issues/1977
*/
import { generateCalendarEvents, type CalendarEvent } from "../../../src/bases/calendar-core";
import type TaskNotesPlugin from "../../../src/main";
import { TaskFactory } from "../../helpers/mock-factories";
function createPlugin(): TaskNotesPlugin {
return {
priorityManager: {
getPriorityConfig: jest.fn().mockReturnValue({ color: "#6366f1" }),
},
statusManager: {
isCompletedStatus: jest.fn().mockReturnValue(false),
},
} as unknown as TaskNotesPlugin;
}
function eventTypes(events: CalendarEvent[]): string[] {
return events.map((event) => event.extendedProps.eventType).sort();
}
describe("Issue #1977: same-day scheduled and due Calendar duplicates", () => {
const plugin = createPlugin();
it("renders one task event when date-only scheduled and due values are the same day", async () => {
const task = TaskFactory.createTask({
title: "Same day task",
path: "Tasks/same-day.md",
scheduled: "2026-05-31",
due: "2026-05-31",
});
const events = await generateCalendarEvents([task], plugin, {
showScheduled: true,
showDue: true,
showRecurring: false,
showTimeEntries: false,
});
expect(eventTypes(events)).toEqual(["scheduled"]);
expect(events[0]).toMatchObject({
id: "scheduled-Tasks/same-day.md",
start: "2026-05-31",
allDay: true,
});
});
it("keeps separate due events when the due value adds a distinct time", async () => {
const task = TaskFactory.createTask({
title: "Timed deadline",
path: "Tasks/timed-deadline.md",
scheduled: "2026-05-31",
due: "2026-05-31T17:00",
});
const events = await generateCalendarEvents([task], plugin, {
showScheduled: true,
showDue: true,
showRecurring: false,
showTimeEntries: false,
});
expect(eventTypes(events)).toEqual(["due", "scheduled"]);
});
it("keeps the due event when scheduled events are hidden", async () => {
const task = TaskFactory.createTask({
title: "Due only display",
path: "Tasks/due-only-display.md",
scheduled: "2026-05-31",
due: "2026-05-31",
});
const events = await generateCalendarEvents([task], plugin, {
showScheduled: false,
showDue: true,
showRecurring: false,
showTimeEntries: false,
});
expect(eventTypes(events)).toEqual(["due"]);
});
it("keeps separate due events when scheduled and due dates differ", async () => {
const task = TaskFactory.createTask({
title: "Different day task",
path: "Tasks/different-day.md",
scheduled: "2026-05-31",
due: "2026-06-01",
});
const events = await generateCalendarEvents([task], plugin, {
showScheduled: true,
showDue: true,
showRecurring: false,
showTimeEntries: false,
});
expect(eventTypes(events)).toEqual(["due", "scheduled"]);
});
});