mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
fix same-day calendar due duplicates
This commit is contained in:
parent
febf68728a
commit
d7a18401d6
3 changed files with 129 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue