From 54f248218db4ff918d85c061e6c30bfb8ece584d Mon Sep 17 00:00:00 2001 From: Extra Chill Bot Date: Mon, 4 May 2026 20:01:25 +0000 Subject: [PATCH] fix: scope calendar progressive day-loader to caller date range (#227) When a caller passes explicit date_start/date_end to the calendar REST endpoint (the progressive day-loader's per-day fetch), CalendarAbilities ignored the caller's range for pagination boundaries. compute_unique_event_dates() built SQL without applying date_start/date_end, then PageBoundary returned "page 1" boundaries (e.g. May 3..May 7), and the progressive branch rewrote query_params['date_start']/'date_end'] to page_dates[0] (May 3). The actual event query returned May 3 events, the renderer emitted 5 date groups, and the frontend day-loader picked the first .data-machine-events-wrapper out of the response and injected May 3's events under May 4, May 5, May 6, May 7. Backend: when user_date_range is true, honor the caller's range as authoritative, skip pagination boundary computation and the progressive branch entirely, and scope events_per_date / total_event_count / date_boundaries to the requested range so the counter and pagination metadata reflect what was asked for. Non-progressive page-1 path is unchanged. Frontend: scope the wrapper lookup in injectDayHtml() to .data-machine-date-group[data-date="$date"] so the wrong day's wrapper can never be injected even if the backend regresses. --- inc/Abilities/CalendarAbilities.php | 128 ++++++++++++------ inc/Blocks/Calendar/src/modules/day-loader.ts | 13 +- 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/inc/Abilities/CalendarAbilities.php b/inc/Abilities/CalendarAbilities.php index 3037a48..4fcae26 100644 --- a/inc/Abilities/CalendarAbilities.php +++ b/inc/Abilities/CalendarAbilities.php @@ -214,59 +214,101 @@ public function executeGetCalendarPage( array $input ): array { $total_event_count = $date_data['total_events']; $events_per_date = $date_data['events_per_date']; - $date_boundaries = PageBoundary::get_date_boundaries_for_page( - $unique_dates, - $current_page, - $total_event_count, - $events_per_date - ); + $user_date_range = ! empty( $base_params['user_date_range'] ); + + $query_params = $base_params; + $range_start = ''; + $range_end = ''; + $progressive = $input['progressive'] ?? false; + $deferred_dates = array(); - $max_pages = $date_boundaries['max_pages']; - $current_page = max( 1, min( $current_page, max( 1, $max_pages ) ) ); + if ( $user_date_range ) { + // Caller passed an explicit date_start/date_end (e.g. progressive + // day-loader requesting a single deferred day). Honor the caller's + // range as authoritative — skip pagination boundary computation + // and the progressive branch entirely, and scope the counter / + // pagination metadata to the requested range. When only one of + // date_start/date_end is provided, use the populated value for + // both bounds so the counter still reflects a sensible window. + $effective_lower = '' !== $user_date_start ? $user_date_start : $user_date_end; + $effective_upper = '' !== $user_date_end ? $user_date_end : $user_date_start; + + $range_start = $effective_lower; + $range_end = $effective_upper; + + $query_params['date_start'] = $user_date_start; + $query_params['date_end'] = $user_date_end; + + // Restrict events_per_date and total_event_count to the requested + // range so the counter ("Viewing X – Y (N of M Events)") reflects + // what was actually asked for, not the full upcoming universe. + $filtered_per_date = array(); + $filtered_total = 0; + foreach ( $events_per_date as $date_key => $count ) { + if ( + ( '' === $effective_lower || $date_key >= $effective_lower ) + && ( '' === $effective_upper || $date_key <= $effective_upper ) + ) { + $filtered_per_date[ $date_key ] = $count; + $filtered_total += $count; + } + } + $events_per_date = $filtered_per_date; + $total_event_count = $filtered_total; + + $max_pages = 1; + $current_page = 1; + $date_boundaries = array( + 'start_date' => $effective_lower, + 'end_date' => $effective_upper, + 'max_pages' => 1, + ); + } else { + $date_boundaries = PageBoundary::get_date_boundaries_for_page( + $unique_dates, + $current_page, + $total_event_count, + $events_per_date + ); - $query_params = $base_params; - $range_start = ''; - $range_end = ''; + $max_pages = $date_boundaries['max_pages']; + $current_page = max( 1, min( $current_page, max( 1, $max_pages ) ) ); - if ( ! empty( $date_boundaries['start_date'] ) && ! empty( $date_boundaries['end_date'] ) ) { - $range_start = $show_past ? $date_boundaries['end_date'] : $date_boundaries['start_date']; - $range_end = $show_past ? $date_boundaries['start_date'] : $date_boundaries['end_date']; + if ( ! empty( $date_boundaries['start_date'] ) && ! empty( $date_boundaries['end_date'] ) ) { + $range_start = $show_past ? $date_boundaries['end_date'] : $date_boundaries['start_date']; + $range_end = $show_past ? $date_boundaries['start_date'] : $date_boundaries['end_date']; - if ( empty( $user_date_start ) ) { $query_params['date_start'] = $range_start; + $query_params['date_end'] = $range_end; } - if ( empty( $user_date_end ) ) { - $query_params['date_end'] = $range_end; - } - } - // Determine progressive rendering: only query the first day's events - // when the page has enough events to benefit from deferred loading. - $progressive = $input['progressive'] ?? false; - $deferred_dates = array(); + // Determine progressive rendering: only query the first day's events + // when the page has enough events to benefit from deferred loading. + // Gated on ! $user_date_range because a single-day request from the + // day-loader has nothing to defer. + if ( $progressive && $range_start && $range_end ) { + // Get the dates within this page's range. + $page_dates = array_filter( + $unique_dates, + function ( $d ) use ( $range_start, $range_end ) { + return $d >= $range_start && $d <= $range_end; + } + ); + $page_dates = array_values( $page_dates ); - if ( $progressive && $range_start && $range_end ) { - // Get the dates within this page's range. - $page_dates = array_filter( - $unique_dates, - function ( $d ) use ( $range_start, $range_end ) { - return $d >= $range_start && $d <= $range_end; + // Only go progressive if enough events on this page. + $page_event_total = 0; + foreach ( $page_dates as $d ) { + $page_event_total += $events_per_date[ $d ] ?? 0; } - ); - $page_dates = array_values( $page_dates ); - // Only go progressive if enough events on this page. - $page_event_total = 0; - foreach ( $page_dates as $d ) { - $page_event_total += $events_per_date[ $d ] ?? 0; - } - - if ( $page_event_total >= EventRenderer::PROGRESSIVE_THRESHOLD && count( $page_dates ) > 1 ) { - // Query only the first day. - $first_date = $page_dates[0]; - $query_params['date_start'] = $first_date; - $query_params['date_end'] = $first_date; - $deferred_dates = array_slice( $page_dates, 1 ); + if ( $page_event_total >= EventRenderer::PROGRESSIVE_THRESHOLD && count( $page_dates ) > 1 ) { + // Query only the first day. + $first_date = $page_dates[0]; + $query_params['date_start'] = $first_date; + $query_params['date_end'] = $first_date; + $deferred_dates = array_slice( $page_dates, 1 ); + } } } diff --git a/inc/Blocks/Calendar/src/modules/day-loader.ts b/inc/Blocks/Calendar/src/modules/day-loader.ts index 4daf1e0..cae3932 100644 --- a/inc/Blocks/Calendar/src/modules/day-loader.ts +++ b/inc/Blocks/Calendar/src/modules/day-loader.ts @@ -216,9 +216,16 @@ function injectDayHtml( ): void { const temp = document.createElement( 'div' ); temp.innerHTML = html; - const sourceWrapper = temp.querySelector( - '.data-machine-events-wrapper' - ); + + // Scope the source wrapper lookup to the matching date group so we never + // inject the wrong day's events even if the response contains multiple + // date groups (defensive against backend regressions). + const date = getDateFromWrapper( wrapper ); + const sourceWrapper = date + ? temp.querySelector( + `.data-machine-date-group[data-date="${ date }"] .data-machine-events-wrapper` + ) || temp.querySelector( '.data-machine-events-wrapper' ) + : temp.querySelector( '.data-machine-events-wrapper' ); if ( sourceWrapper ) { wrapper.innerHTML = sourceWrapper.innerHTML;