From c99465153d6bc2a002bf14933667a7126d61ad54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 27 Sep 2026 04:31:14 -0400 Subject: [PATCH] =?UTF-8?q?Calendar=20=C2=B7=20open=20times=20start=20over?= =?UTF-8?q?=20on=20new=20days?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AvailabilityPicker kept the day chosen in keyed state whatever days the owner passed next, and the slots lookup expected it among them: an owner paging to the next week crashed the app once a day was chosen. The choice is now seeded on the owner's days and pick, so a new run of days starts it over at the picked day or the first open one. --- TASKS.md | 1 + src/calendar/availability.rs | 10 ++++++---- src/calendar/tests/editing.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/TASKS.md b/TASKS.md index b091a9b..326b6f3 100644 --- a/TASKS.md +++ b/TASKS.md @@ -358,3 +358,4 @@ Per-component lines live in `tasks/`. Tags there: | T30b | 1 | FAIL | the week and day views froze the line at now and today: the moment read once went down as given and turned the ticker off | | T30b | 2 | PASS | | | T30c | 1 | PASS | | +| T30d | 1 | FAIL | a chosen day outlived the owner's days, and the next render panicked | diff --git a/src/calendar/availability.rs b/src/calendar/availability.rs index 667268d..343b873 100644 --- a/src/calendar/availability.rs +++ b/src/calendar/availability.rs @@ -8,6 +8,7 @@ use jiff::{Timestamp, civil::Date, tz::TimeZone}; use super::hours::moment; use crate::{ forms::{Choice, ChoiceChips}, + layout::seeded::use_seeded, theme::{ActiveTheme, TextSize}, typography::format, }; @@ -41,7 +42,7 @@ pub(crate) fn open_slots( starts } -/// Open times to meet over some days: each day with its count of open slots, then the slots of the one chosen. Busy stretches, the working hours and the past close the rest. A pick hands on the slot's start. +/// Open times to meet over some days: each day with its count of open slots, then the slots of the one chosen. Busy stretches, the working hours and the past close the rest. A pick hands on the slot's start; new days from the owner start the choice over. #[derive(IntoElement)] pub struct AvailabilityPicker { id: ElementId, @@ -149,8 +150,9 @@ impl RenderOnce for AvailabilityPicker { .iter() .find(|(_, slots)| !slots.is_empty()) .map(|(day, _)| *day); - let chosen = window.use_keyed_state((self.id.clone(), "day"), cx, |_, _| None::); - let shown = chosen.read(cx).or(picked_day).or(first_open); + let seed = (self.days.clone(), picked_day.or(first_open)); + let chosen = use_seeded((self.id.clone(), "day"), seed, window, cx); + let shown = chosen.read(cx).value.1; let theme = cx.theme(); let quiet = |text: &'static str| { div() @@ -184,7 +186,7 @@ impl RenderOnce for AvailabilityPicker { .expect("a day's value is its date"); log::info!("availability picker: {day}"); chosen.update(cx, |chosen, cx| { - *chosen = Some(day); + chosen.value.1 = Some(day); cx.notify(); }) }); diff --git a/src/calendar/tests/editing.rs b/src/calendar/tests/editing.rs index 9fe982a..ad3f0cd 100644 --- a/src/calendar/tests/editing.rs +++ b/src/calendar/tests/editing.rs @@ -161,3 +161,34 @@ fn a_slot_pick_hands_on_its_start(cx: &mut TestAppContext) { "09:00 to 10:00 is taken" ); } + +fn paged(host: &Planning, owner: Entity) -> gpui::AnyElement { + let first = host.month; + let dawn = date(2026, 8, 31).at(0, 0, 0, 0).to_zoned(TimeZone::UTC); + AvailabilityPicker::new("paged", [first, first.tomorrow().expect("a day ahead")]) + .zone(TimeZone::UTC) + .hours(9, 10) + .now(dawn.expect("a UTC time").timestamp()) + .on_pick(move |start, _, cx| note(&owner, format!("pick {start}"), cx)) + .into_any_element() +} + +#[gpui::test] +fn new_days_from_the_owner_start_the_choice_over(cx: &mut TestAppContext) { + let (host, cx) = planning(paged, cx); + tab_to(2, cx); + press("space", cx); + host.update(cx, |host, cx| { + host.month = date(2026, 10, 5); + cx.notify(); + }); + settle(cx); + tab_to(3, cx); + press("space", cx); + let nine = date(2026, 10, 5).at(9, 0, 0, 0).to_zoned(TimeZone::UTC); + assert_eq!( + heard(&host, cx), + [format!("pick {}", nine.expect("a UTC time").timestamp())], + "the chosen day of the old run is gone; the first open day shows" + ); +}