Merge consistent event ordering (#291)
This commit is contained in:
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **Split view opens with the month's agenda.** It starts at today in the current month; tapping a day filters the list, and the filter chip restores the full month. Swiping the list moves by day when filtered and by month otherwise ([#142]).
|
- **Split view opens with the month's agenda.** It starts at today in the current month; tapping a day filters the list, and the filter chip restores the full month. Swiping the list moves by day when filtered and by month otherwise ([#142]).
|
||||||
- **Today stands out in the month grid.** Its whole cell has a distinct background; tapping Today briefly expands that cell and nudges its neighbours aside ([#241]).
|
- **Today stands out in the month grid.** Its whole cell has a distinct background; tapping Today briefly expands that cell and nudges its neighbours aside ([#241]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Multi-day events stay above shorter events.** Month and Week bars, Split day lists, and Agenda now place multi-day events before one-day all-day events, then timed events; declined events remain last ([#291]).
|
||||||
|
|
||||||
## [2.21.0] — 2026-09-23
|
## [2.21.0] — 2026-09-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.domain
|
||||||
|
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
|
||||||
|
/** Lower values appear first in lists and get the upper visible lanes in grids. */
|
||||||
|
fun EventInstance.displayPriority(zone: TimeZone): Int = when {
|
||||||
|
isDeclined -> 3
|
||||||
|
spansMultipleDays(zone) -> 0
|
||||||
|
isAllDay -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared order for agenda and split day lists. Grid bars add their lane geometry. */
|
||||||
|
fun eventDisplayComparator(zone: TimeZone): Comparator<EventInstance> =
|
||||||
|
compareBy<EventInstance> { it.displayPriority(zone) }
|
||||||
|
.thenBy { it.start }
|
||||||
|
.thenBy { it.title }
|
||||||
|
.thenBy { it.instanceId }
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.agenda
|
package de.jeanlucmakiola.calendula.ui.agenda
|
||||||
|
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.domain.eventDisplayComparator
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
import de.jeanlucmakiola.calendula.domain.spanFirstDay
|
import de.jeanlucmakiola.calendula.domain.spanFirstDay
|
||||||
import de.jeanlucmakiola.calendula.domain.spanLastDay
|
import de.jeanlucmakiola.calendula.domain.spanLastDay
|
||||||
@@ -86,11 +87,7 @@ fun groupAgendaDays(
|
|||||||
return byDay.map { (date, dayEvents) ->
|
return byDay.map { (date, dayEvents) ->
|
||||||
AgendaDay(
|
AgendaDay(
|
||||||
date = date,
|
date = date,
|
||||||
events = dayEvents.sortedWith(
|
events = dayEvents.sortedWith(eventDisplayComparator(zone)),
|
||||||
compareByDescending<EventInstance> { it.isAllDay }
|
|
||||||
.thenBy { it.start }
|
|
||||||
.thenBy { it.title },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import de.jeanlucmakiola.calendula.domain.FailureReason
|
|||||||
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.common.PageStateCache
|
import de.jeanlucmakiola.calendula.ui.common.PageStateCache
|
||||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
import de.jeanlucmakiola.calendula.domain.isDeclined
|
||||||
|
import de.jeanlucmakiola.calendula.domain.eventDisplayComparator
|
||||||
import de.jeanlucmakiola.calendula.ui.week.coversDay
|
import de.jeanlucmakiola.calendula.ui.week.coversDay
|
||||||
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
|
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
|
||||||
import de.jeanlucmakiola.calendula.ui.week.spansMultipleDays
|
import de.jeanlucmakiola.calendula.ui.week.spansMultipleDays
|
||||||
@@ -506,11 +507,7 @@ internal fun instancesByDay(
|
|||||||
days.associateWith { day ->
|
days.associateWith { day ->
|
||||||
instances
|
instances
|
||||||
.filter { it.coversDay(day, zone) }
|
.filter { it.coversDay(day, zone) }
|
||||||
.sortedWith(
|
.sortedWith(eventDisplayComparator(zone))
|
||||||
compareBy<EventInstance> { it.isDeclined }
|
|
||||||
.thenByDescending { it.isAllDay }
|
|
||||||
.thenBy { it.start },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
|||||||
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.domain.displayPriority
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.PageStateCache
|
import de.jeanlucmakiola.calendula.ui.common.PageStateCache
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -228,7 +228,13 @@ internal fun layoutAllDay(
|
|||||||
}
|
}
|
||||||
// Declined bars are packed after every other one, so on any day they
|
// Declined bars are packed after every other one, so on any day they
|
||||||
// cover they land in a lane below it (#230).
|
// cover they land in a lane below it (#230).
|
||||||
.sortedWith(compareBy({ it.event.isDeclined }, { it.startCol }, { it.endCol }))
|
.sortedWith(
|
||||||
|
compareBy<Raw> { it.event.displayPriority(zone) }
|
||||||
|
.thenBy { it.startCol }
|
||||||
|
.thenBy { it.endCol }
|
||||||
|
.thenBy { it.event.start }
|
||||||
|
.thenBy { it.event.instanceId },
|
||||||
|
)
|
||||||
|
|
||||||
// What each lane already holds, rather than just how far right it reaches.
|
// What each lane already holds, rather than just how far right it reaches.
|
||||||
// A single "last occupied column" only answers correctly while spans arrive
|
// A single "last occupied column" only answers correctly while spans arrive
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ class GroupAgendaDaysTest {
|
|||||||
.containsExactly(LocalDate(2026, 7, 2))
|
.containsExactly(LocalDate(2026, 7, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `multi-day events precede single all-day and timed events`() {
|
||||||
|
val multi = event(1, "Multi", at(2026, 7, 1), at(2026, 7, 4), isAllDay = true)
|
||||||
|
val single = event(2, "Single", at(2026, 7, 2), at(2026, 7, 3), isAllDay = true)
|
||||||
|
val timed = event(3, "Timed", at(2026, 7, 2, 8), at(2026, 7, 2, 9))
|
||||||
|
val second = days(listOf(timed, single, multi)).first { it.date == LocalDate(2026, 7, 2) }
|
||||||
|
assertThat(second.events.map { it.title })
|
||||||
|
.containsExactly("Multi", "Single", "Timed").inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `an event ending exactly at midnight does not reach the next day`() {
|
fun `an event ending exactly at midnight does not reach the next day`() {
|
||||||
val e = event(1, "late", at(2026, 7, 1, 22, 0), at(2026, 7, 2, 0, 0))
|
val e = event(1, "late", at(2026, 7, 1, 22, 0), at(2026, 7, 2, 0, 0))
|
||||||
|
|||||||
@@ -163,6 +163,18 @@ class MonthLayoutTest {
|
|||||||
assertThat(week.spans.map { it.lane }).containsExactly(0, 1)
|
assertThat(week.spans.map { it.lane }).containsExactly(0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `multi-day bar is above a one-day all-day bar on the same date`() {
|
||||||
|
val day = LocalDate(2026, 6, 10)
|
||||||
|
val single = allDay(day, id = 1L, title = "Single")
|
||||||
|
val multi = allDay(day, day.plus(2, DateTimeUnit.DAY), id = 2L, title = "Multi")
|
||||||
|
val week = layoutCalendarWeek(weekOf8th, listOf(single, multi), zone)
|
||||||
|
val lanes = week.spans.associate { it.event.title to it.lane }
|
||||||
|
assertThat(lanes.getValue("Multi")).isLessThan(lanes.getValue("Single"))
|
||||||
|
assertThat(instancesByDay(listOf(day), listOf(single, multi), zone).getValue(day).map { it.title })
|
||||||
|
.containsExactly("Multi", "Single").inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `single-day timed events stay pills and sort by start`() {
|
fun `single-day timed events stay pills and sort by start`() {
|
||||||
val late = timed(LocalDate(2026, 6, 10), 14, 15, id = 1L)
|
val late = timed(LocalDate(2026, 6, 10), 14, 15, id = 1L)
|
||||||
|
|||||||
Reference in New Issue
Block a user