diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index e16beba3..c031dbe3 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -35,3 +35,4 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l ## Fixed - (#2007) Fixed a remaining task creation autocomplete case where a status value containing another status label could leave status fragments in the task title. Thanks to @prepare4robots for the follow-up report. +- (#2031) Fixed quick task creation failing when natural language input created a recurring task with a scheduled time. Thanks to @rdefaccio for reporting this. diff --git a/src/core/recurrence.ts b/src/core/recurrence.ts index 642262fa..2bce38c0 100644 --- a/src/core/recurrence.ts +++ b/src/core/recurrence.ts @@ -2,15 +2,37 @@ export * from "@tasknotes/model/recurrence"; export type { TaskInfo } from "../types"; import { + addDTSTARTToRecurrenceRule as addDTSTARTToRecurrenceRuleModel, + addDTSTARTToRecurrenceRuleWithDraggedTime as addDTSTARTToRecurrenceRuleWithDraggedTimeModel, getNextUncompletedOccurrence as getNextUncompletedOccurrenceModel, updateToNextScheduledOccurrence as updateToNextScheduledOccurrenceModel, } from "@tasknotes/model/recurrence"; import { getTodayString } from "../utils/dateUtils"; +type AddDTSTARTTaskInput = Parameters[0]; +type AddDTSTARTWithDraggedTimeTaskInput = Parameters< + typeof addDTSTARTToRecurrenceRuleWithDraggedTimeModel +>[0]; type RecurringTaskInput = Parameters[0]; type RecurrenceUpdateTaskInput = Parameters[0]; type RecurrenceUpdateResult = ReturnType; +export function addDTSTARTToRecurrenceRule(task: AddDTSTARTTaskInput): string | null { + return addDTSTARTToRecurrenceRuleModel(normalizeMinutePrecisionDateInputs(task)); +} + +export function addDTSTARTToRecurrenceRuleWithDraggedTime( + task: AddDTSTARTWithDraggedTimeTaskInput, + draggedStart: Date, + allDay: boolean +): string | null { + return addDTSTARTToRecurrenceRuleWithDraggedTimeModel( + normalizeMinutePrecisionDateInputs(task), + draggedStart, + allDay + ); +} + export function getNextUncompletedOccurrence(task: RecurringTaskInput): Date | null { return getNextUncompletedOccurrenceModel(task, { today: getTodayString() }); } @@ -23,3 +45,35 @@ export function updateToNextScheduledOccurrence( today: getTodayString(), }); } + +function normalizeMinutePrecisionDateInputs( + task: T +): T { + const scheduled = normalizeMinutePrecisionDateTime(task.scheduled); + const dateCreated = normalizeMinutePrecisionDateTime(task.dateCreated); + + if (scheduled === task.scheduled && dateCreated === task.dateCreated) { + return task; + } + + return { + ...task, + scheduled, + dateCreated, + }; +} + +function normalizeMinutePrecisionDateTime(value: string | undefined): string | undefined { + if (!value) return value; + + const match = value + .trim() + .match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(Z|[+-]\d{2}:\d{2})?$/); + + if (!match) { + return value; + } + + const [, date, time, zone = ""] = match; + return `${date}T${time}:00${zone}`; +} diff --git a/tests/unit/issues/issue-2031-nlp-recurring-time.test.ts b/tests/unit/issues/issue-2031-nlp-recurring-time.test.ts new file mode 100644 index 00000000..2d9ac6a9 --- /dev/null +++ b/tests/unit/issues/issue-2031-nlp-recurring-time.test.ts @@ -0,0 +1,40 @@ +import type TaskNotesPlugin from "../../../src/main"; +import { addDTSTARTToRecurrenceRule } from "../../../src/core/recurrence"; +import { buildTaskCreationDataFromParsed } from "../../../src/services/buildTaskCreationDataFromParsed"; +import { DEFAULT_SETTINGS } from "../../../src/settings/defaults"; +import type { ParsedTaskData } from "../../../src/services/NaturalLanguageParser"; + +function createPlugin(): TaskNotesPlugin { + return { + settings: { + ...DEFAULT_SETTINGS, + }, + } as unknown as TaskNotesPlugin; +} + +describe("Issue #2031: NLP recurring task with time", () => { + it("adds DTSTART for NLP-created recurring tasks with minute-precision scheduled times", () => { + const parsed: ParsedTaskData = { + title: "this is a task", + scheduledDate: "2026-06-13", + scheduledTime: "15:00", + recurrence: "FREQ=DAILY", + }; + + const taskData = buildTaskCreationDataFromParsed(createPlugin(), parsed); + + expect(taskData.scheduled).toBe("2026-06-13T15:00"); + expect(taskData.recurrence).toBe("FREQ=DAILY"); + expect( + addDTSTARTToRecurrenceRule({ + title: taskData.title, + status: taskData.status, + priority: taskData.priority, + path: "", + archived: false, + scheduled: taskData.scheduled, + recurrence: taskData.recurrence, + }) + ).toBe("DTSTART:20260613T150000Z;FREQ=DAILY"); + }); +});