From a17ba2e118dae465e4943797360be0c8ecba44c1 Mon Sep 17 00:00:00 2001 From: martin-forge <228563004+martin-forge@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:26:23 +0100 Subject: [PATCH] fix(calendar): keep event placement ahead of Bases rank --- docs/releases/unreleased.md | 2 ++ src/bases/CalendarView.ts | 4 ++- .../issue-1411-agenda-bases-sort.test.ts | 36 +++++++++++++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 21617927f..2f8bde999 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -16,6 +16,8 @@ Example: ``` ## Fixed +- Keep agenda events chronological when a Base sort is configured; use Base order at the same calendar time. Follow-up to #1411, reported by @ky1ejs. + - (#768) Fixed calendar view appearing empty in week and day views due to invalid time configuration values - Added time validation in settings UI with proper error messages and debouncing - Prevents "Cannot read properties of null (reading 'years')" error from FullCalendar diff --git a/src/bases/CalendarView.ts b/src/bases/CalendarView.ts index 9ca0bf0b6..d5f33d8a0 100644 --- a/src/bases/CalendarView.ts +++ b/src/bases/CalendarView.ts @@ -179,7 +179,9 @@ export function getTaskNotesCalendarEventOrder(sortConfig: unknown): string { if (!hasBasesCalendarSortConfig(sortConfig)) { return DEFAULT_CALENDAR_EVENT_ORDER; } - return `${TASKNOTES_CALENDAR_SORT_INDEX},${DEFAULT_CALENDAR_EVENT_ORDER}`; + // Bases ranks break ties at the same placement. They must not put a timed + // task ahead of an earlier appointment that has no Bases result index. + return `start,allDay,${TASKNOTES_CALENDAR_SORT_INDEX},-duration,title`; } function getCalendarEventSortPath(event: EventInput): string | null { diff --git a/tests/unit/issues/issue-1411-agenda-bases-sort.test.ts b/tests/unit/issues/issue-1411-agenda-bases-sort.test.ts index e549a214b..e0643f4e0 100644 --- a/tests/unit/issues/issue-1411-agenda-bases-sort.test.ts +++ b/tests/unit/issues/issue-1411-agenda-bases-sort.test.ts @@ -1,4 +1,5 @@ import type { EventInput } from "@fullcalendar/core"; +import { execFileSync } from "node:child_process"; import { applyBasesSortIndexesToCalendarEvents, getTaskNotesCalendarEventOrder, @@ -13,15 +14,46 @@ describe("Issue #1411: Agenda Calendar Bases respect Bases sort order", () => { expect(getTaskNotesCalendarEventOrder(undefined)).toBe("start,-duration,allDay,title"); }); - it("puts the TaskNotes sort index first when the Base has a sort config", () => { + it("uses Bases order after event time, before duration and title", () => { const sortConfig = [{ column: "note.status", direction: "ASC" }]; expect(hasBasesCalendarSortConfig(sortConfig)).toBe(true); expect(getTaskNotesCalendarEventOrder(sortConfig)).toBe( - `${TASKNOTES_CALENDAR_SORT_INDEX},start,-duration,allDay,title` + `start,allDay,${TASKNOTES_CALENDAR_SORT_INDEX},-duration,title` ); }); + it("interleaves timed tasks with appointments while sorting untimed tasks by Bases rank", () => { + const order = getTaskNotesCalendarEventOrder([{ column: "status" }]); + const events = [ + { title: "Evening task", start: 20, tasknotesSortIndex: 0 }, + { title: "Morning appointment", start: 9 }, + { title: "Morning task", start: 8, tasknotesSortIndex: 3 }, + { title: "Ready", start: 0, tasknotesSortIndex: 2 }, + { title: "Doing", start: 0, tasknotesSortIndex: 1 }, + ]; + // The suite mocks FullCalendar. Exercise its real comparator in Node. + const sorted = JSON.parse( + execFileSync( + process.execPath, + [ + "-e", + "const {parseFieldSpecs,compareByFieldSpecs}=require('@fullcalendar/core/internal'); const events=JSON.parse(process.argv[2]); const specs=parseFieldSpecs(process.argv[1]); console.log(JSON.stringify(events.sort((a,b)=>compareByFieldSpecs(a,b,specs)).map(e=>e.title)));", + order, + JSON.stringify(events), + ], + { encoding: "utf8" } + ) + ); + expect(sorted).toEqual([ + "Doing", + "Ready", + "Morning task", + "Morning appointment", + "Evening task", + ]); + }); + it("adds Bases result indexes to task and property-based events", () => { const events: EventInput[] = [ {