refactor: reuse ListQuery for timeline page

Replace the hand-rolled TimelineQuery struct with the shared ListQuery
type and its into_request_and_search_with_default method.
This commit is contained in:
Jon Seager 2026-02-13 13:05:56 +00:00
parent a1aefda860
commit 9d25873243
No known key found for this signature in database

View file

@ -2,13 +2,12 @@ use axum::extract::{Query, State};
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use crate::application::errors::{AppError, map_app_error};
use crate::application::routes::render_html;
use crate::application::routes::support::{is_datastar_request, normalize_request};
use crate::application::routes::support::{ListQuery, is_datastar_request, normalize_request};
use crate::application::state::AppState;
use crate::domain::listing::{ListRequest, PageSize, SortDirection, SortKey};
use crate::domain::listing::ListRequest;
use crate::domain::timeline::{TimelineEvent, TimelineSortKey};
use crate::presentation::web::templates::{TimelineChunkTemplate, TimelineTemplate};
use crate::presentation::web::views::{
@ -19,65 +18,15 @@ const TIMELINE_PAGE_PATH: &str = "/timeline";
const TIMELINE_FRAGMENT_PATH: &str = "/timeline";
const TIMELINE_DEFAULT_PAGE_SIZE: u32 = 20;
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum PageSizeParam {
Number(u32),
Text(String),
}
#[derive(Debug, Deserialize)]
pub struct TimelineQuery {
page: Option<u32>,
#[serde(default)]
page_size: Option<PageSizeParam>,
#[serde(default, rename = "sort")]
sort_key: Option<String>,
#[serde(default, rename = "dir")]
sort_dir: Option<String>,
}
impl TimelineQuery {
fn to_request(&self) -> ListRequest<TimelineSortKey> {
let page = self.page.unwrap_or(1);
let page_size = match &self.page_size {
Some(PageSizeParam::Number(value)) => PageSize::limited(*value),
Some(PageSizeParam::Text(text)) if text.eq_ignore_ascii_case("all") => PageSize::All,
Some(PageSizeParam::Text(text)) => text
.parse::<u32>()
.map(PageSize::limited)
.unwrap_or(PageSize::limited(TIMELINE_DEFAULT_PAGE_SIZE)),
None => PageSize::limited(TIMELINE_DEFAULT_PAGE_SIZE),
};
let sort_key = self
.sort_key
.as_deref()
.and_then(TimelineSortKey::from_query)
.unwrap_or_else(TimelineSortKey::default);
let sort_direction = self
.sort_dir
.as_deref()
.and_then(|dir| match dir.to_ascii_lowercase().as_str() {
"asc" => Some(SortDirection::Asc),
"desc" => Some(SortDirection::Desc),
_ => None,
})
.unwrap_or_else(|| sort_key.default_direction());
ListRequest::new(page, page_size, sort_key, sort_direction)
}
}
#[tracing::instrument(skip(state, cookies, headers, query))]
pub(crate) async fn timeline_page(
State(state): State<AppState>,
cookies: tower_cookies::Cookies,
headers: HeaderMap,
Query(query): Query<TimelineQuery>,
Query(query): Query<ListQuery>,
) -> Result<Response, StatusCode> {
let request = query.to_request();
let (request, _search) =
query.into_request_and_search_with_default::<TimelineSortKey>(TIMELINE_DEFAULT_PAGE_SIZE);
let is_authenticated = crate::application::routes::is_authenticated(&state, &cookies).await;
if is_datastar_request(&headers) {