feat(search): add search to ListQuery, ListNavigator, and route handlers
Add q parameter to ListQuery with into_request_and_search(). Extend ListNavigator with search state, search_value(), has_search(), path(), and search_query_base() for template URL generation. Thread search through all route handlers and update backup tests.
This commit is contained in:
parent
1e151123a0
commit
2edf102b41
10 changed files with 168 additions and 83 deletions
|
|
@ -33,11 +33,12 @@ struct BagPageData {
|
||||||
async fn load_bag_page(
|
async fn load_bag_page(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request: ListRequest<BagSortKey>,
|
request: ListRequest<BagSortKey>,
|
||||||
|
search: Option<&str>,
|
||||||
) -> Result<BagPageData, AppError> {
|
) -> Result<BagPageData, AppError> {
|
||||||
let open_request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc);
|
let open_request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc);
|
||||||
let open_page = state
|
let open_page = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
.list(BagFilter::open(), &open_request)
|
.list(BagFilter::open(), &open_request, None)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
let open_bags_view = open_page
|
let open_bags_view = open_page
|
||||||
|
|
@ -48,7 +49,7 @@ async fn load_bag_page(
|
||||||
|
|
||||||
let page = state
|
let page = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
.list(BagFilter::closed(), &request)
|
.list(BagFilter::closed(), &request, search)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|
@ -58,6 +59,7 @@ async fn load_bag_page(
|
||||||
BagView::from_domain,
|
BagView::from_domain,
|
||||||
BAG_PAGE_PATH,
|
BAG_PAGE_PATH,
|
||||||
BAG_FRAGMENT_PATH,
|
BAG_FRAGMENT_PATH,
|
||||||
|
search.map(String::from),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(BagPageData {
|
Ok(BagPageData {
|
||||||
|
|
@ -74,11 +76,11 @@ pub(crate) async fn bags_page(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let request = query.into_request::<BagSortKey>();
|
let (request, search) = query.into_request_and_search::<BagSortKey>();
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||||
return render_bag_list_fragment(state, request, is_authenticated)
|
return render_bag_list_fragment(state, request, search, is_authenticated)
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error);
|
.map_err(map_app_error);
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +97,7 @@ pub(crate) async fn bags_page(
|
||||||
open_bags,
|
open_bags,
|
||||||
bags,
|
bags,
|
||||||
navigator,
|
navigator,
|
||||||
} = load_bag_page(&state, request)
|
} = load_bag_page(&state, request, search.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error)?;
|
.map_err(map_app_error)?;
|
||||||
|
|
||||||
|
|
@ -121,7 +123,7 @@ pub(crate) async fn create_bag(
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
payload: FlexiblePayload<NewBagSubmission>,
|
payload: FlexiblePayload<NewBagSubmission>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<BagSortKey>();
|
let (request, search) = query.into_request_and_search::<BagSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let new_bag = submission.into_new_bag().map_err(ApiError::from)?;
|
let new_bag = submission.into_new_bag().map_err(ApiError::from)?;
|
||||||
|
|
||||||
|
|
@ -168,11 +170,12 @@ pub(crate) async fn create_bag(
|
||||||
let _ = state.timeline_repo.insert(event).await;
|
let _ = state.timeline_repo.insert(event).await;
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_bag_list_fragment(state, request, true)
|
render_bag_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else if matches!(source, PayloadSource::Form) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
let target = ListNavigator::new(BAG_PAGE_PATH, BAG_FRAGMENT_PATH, request).page_href(1);
|
let target =
|
||||||
|
ListNavigator::new(BAG_PAGE_PATH, BAG_FRAGMENT_PATH, request, search).page_href(1);
|
||||||
Ok(Redirect::to(&target).into_response())
|
Ok(Redirect::to(&target).into_response())
|
||||||
} else {
|
} else {
|
||||||
let enriched = state
|
let enriched = state
|
||||||
|
|
@ -196,7 +199,7 @@ pub(crate) async fn list_bags(
|
||||||
let request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc);
|
let request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc);
|
||||||
let page = state
|
let page = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
.list(filter, &request)
|
.list(filter, &request, None)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
Ok(Json(page.items))
|
Ok(Json(page.items))
|
||||||
|
|
@ -214,7 +217,7 @@ pub(crate) async fn update_bag(
|
||||||
Query(update_params): Query<UpdateBag>,
|
Query(update_params): Query<UpdateBag>,
|
||||||
payload: Option<Json<UpdateBag>>,
|
payload: Option<Json<UpdateBag>>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<BagSortKey>();
|
let (request, search) = query.into_request_and_search::<BagSortKey>();
|
||||||
|
|
||||||
let body_update = payload.map_or(
|
let body_update = payload.map_or(
|
||||||
UpdateBag {
|
UpdateBag {
|
||||||
|
|
@ -274,7 +277,7 @@ pub(crate) async fn update_bag(
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_bag_list_fragment(state, request, true)
|
render_bag_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -337,13 +340,14 @@ impl NewBagSubmission {
|
||||||
async fn render_bag_list_fragment(
|
async fn render_bag_list_fragment(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request: ListRequest<BagSortKey>,
|
request: ListRequest<BagSortKey>,
|
||||||
|
search: Option<String>,
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let BagPageData {
|
let BagPageData {
|
||||||
open_bags,
|
open_bags,
|
||||||
bags,
|
bags,
|
||||||
navigator,
|
navigator,
|
||||||
} = load_bag_page(&state, request).await?;
|
} = load_bag_page(&state, request, search.as_deref()).await?;
|
||||||
|
|
||||||
let template = BagListTemplate {
|
let template = BagListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,11 @@ struct BrewPageData {
|
||||||
async fn load_brew_page(
|
async fn load_brew_page(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request: ListRequest<BrewSortKey>,
|
request: ListRequest<BrewSortKey>,
|
||||||
|
search: Option<&str>,
|
||||||
) -> Result<BrewPageData, AppError> {
|
) -> Result<BrewPageData, AppError> {
|
||||||
let page = state
|
let page = state
|
||||||
.brew_repo
|
.brew_repo
|
||||||
.list(BrewFilter::all(), &request)
|
.list(BrewFilter::all(), &request, search)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|
@ -48,6 +49,7 @@ async fn load_brew_page(
|
||||||
BrewView::from_domain,
|
BrewView::from_domain,
|
||||||
BREW_PAGE_PATH,
|
BREW_PAGE_PATH,
|
||||||
BREW_FRAGMENT_PATH,
|
BREW_FRAGMENT_PATH,
|
||||||
|
search.map(String::from),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(BrewPageData { brews, navigator })
|
Ok(BrewPageData { brews, navigator })
|
||||||
|
|
@ -60,11 +62,11 @@ pub(crate) async fn brews_page(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let request = query.into_request::<BrewSortKey>();
|
let (request, search) = query.into_request_and_search::<BrewSortKey>();
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||||
return render_brew_list_fragment(state, request, is_authenticated)
|
return render_brew_list_fragment(state, request, search, is_authenticated)
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error);
|
.map_err(map_app_error);
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +78,7 @@ pub(crate) async fn brews_page(
|
||||||
);
|
);
|
||||||
let open_bags = state
|
let open_bags = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
.list(BagFilter::open(), &open_bags_request)
|
.list(BagFilter::open(), &open_bags_request, None)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||||
let bag_options: Vec<BagOptionView> = open_bags
|
let bag_options: Vec<BagOptionView> = open_bags
|
||||||
|
|
@ -92,6 +94,7 @@ pub(crate) async fn brews_page(
|
||||||
.list(
|
.list(
|
||||||
GearFilter::for_category(GearCategory::Grinder),
|
GearFilter::for_category(GearCategory::Grinder),
|
||||||
&grinder_request,
|
&grinder_request,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||||
|
|
@ -108,6 +111,7 @@ pub(crate) async fn brews_page(
|
||||||
.list(
|
.list(
|
||||||
GearFilter::for_category(GearCategory::Brewer),
|
GearFilter::for_category(GearCategory::Brewer),
|
||||||
&brewer_request,
|
&brewer_request,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||||
|
|
@ -124,6 +128,7 @@ pub(crate) async fn brews_page(
|
||||||
.list(
|
.list(
|
||||||
GearFilter::for_category(GearCategory::FilterPaper),
|
GearFilter::for_category(GearCategory::FilterPaper),
|
||||||
&filter_paper_request,
|
&filter_paper_request,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||||
|
|
@ -133,7 +138,7 @@ pub(crate) async fn brews_page(
|
||||||
.map(GearOptionView::from)
|
.map(GearOptionView::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let BrewPageData { brews, navigator } = load_brew_page(&state, request)
|
let BrewPageData { brews, navigator } = load_brew_page(&state, request, search.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error)?;
|
.map_err(map_app_error)?;
|
||||||
|
|
||||||
|
|
@ -225,7 +230,7 @@ pub(crate) async fn create_brew(
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
payload: FlexiblePayload<NewBrewSubmission>,
|
payload: FlexiblePayload<NewBrewSubmission>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<BrewSortKey>();
|
let (request, search) = query.into_request_and_search::<BrewSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let new_brew = submission.into_new_brew().map_err(ApiError::from)?;
|
let new_brew = submission.into_new_brew().map_err(ApiError::from)?;
|
||||||
|
|
||||||
|
|
@ -267,12 +272,13 @@ pub(crate) async fn create_brew(
|
||||||
.insert("datastar-mode", HeaderValue::from_static("append"));
|
.insert("datastar-mode", HeaderValue::from_static("append"));
|
||||||
Ok(response)
|
Ok(response)
|
||||||
} else {
|
} else {
|
||||||
render_brew_list_fragment(state, request, true)
|
render_brew_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
}
|
}
|
||||||
} else if matches!(source, PayloadSource::Form) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
let target = ListNavigator::new(BREW_PAGE_PATH, BREW_FRAGMENT_PATH, request).page_href(1);
|
let target =
|
||||||
|
ListNavigator::new(BREW_PAGE_PATH, BREW_FRAGMENT_PATH, request, search).page_href(1);
|
||||||
Ok(Redirect::to(&target).into_response())
|
Ok(Redirect::to(&target).into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok((StatusCode::CREATED, Json(enriched)).into_response())
|
Ok((StatusCode::CREATED, Json(enriched)).into_response())
|
||||||
|
|
@ -373,7 +379,7 @@ pub(crate) async fn list_brews(
|
||||||
let request = ListRequest::show_all(BrewSortKey::CreatedAt, SortDirection::Desc);
|
let request = ListRequest::show_all(BrewSortKey::CreatedAt, SortDirection::Desc);
|
||||||
let page = state
|
let page = state
|
||||||
.brew_repo
|
.brew_repo
|
||||||
.list(filter, &request)
|
.list(filter, &request, None)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
Ok(Json(page.items))
|
Ok(Json(page.items))
|
||||||
|
|
@ -398,9 +404,11 @@ define_delete_handler!(
|
||||||
async fn render_brew_list_fragment(
|
async fn render_brew_list_fragment(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request: ListRequest<BrewSortKey>,
|
request: ListRequest<BrewSortKey>,
|
||||||
|
search: Option<String>,
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let BrewPageData { brews, navigator } = load_brew_page(&state, request).await?;
|
let BrewPageData { brews, navigator } =
|
||||||
|
load_brew_page(&state, request, search.as_deref()).await?;
|
||||||
|
|
||||||
let template = BrewListTemplate {
|
let template = BrewListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,11 @@ const GEAR_FRAGMENT_PATH: &str = "/gear#gear-list";
|
||||||
async fn load_gear_page(
|
async fn load_gear_page(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request: ListRequest<GearSortKey>,
|
request: ListRequest<GearSortKey>,
|
||||||
|
search: Option<&str>,
|
||||||
) -> Result<Paginated<GearView>, AppError> {
|
) -> Result<Paginated<GearView>, AppError> {
|
||||||
let page = state
|
let page = state
|
||||||
.gear_repo
|
.gear_repo
|
||||||
.list(GearFilter::all(), &request)
|
.list(GearFilter::all(), &request, search)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@ async fn load_gear_page(
|
||||||
GearView::from_domain,
|
GearView::from_domain,
|
||||||
GEAR_PAGE_PATH,
|
GEAR_PAGE_PATH,
|
||||||
GEAR_FRAGMENT_PATH,
|
GEAR_FRAGMENT_PATH,
|
||||||
|
search.map(String::from),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(gear)
|
Ok(gear)
|
||||||
|
|
@ -53,21 +55,21 @@ pub(crate) async fn gear_page(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let request = query.into_request::<GearSortKey>();
|
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||||
return render_gear_list_fragment(state, request, is_authenticated)
|
return render_gear_list_fragment(state, request, search, is_authenticated)
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error);
|
.map_err(map_app_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gear = load_gear_page(&state, request)
|
let gear = load_gear_page(&state, request, search.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error)?;
|
.map_err(map_app_error)?;
|
||||||
|
|
||||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||||
let navigator = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request);
|
let navigator = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request, search);
|
||||||
|
|
||||||
let template = GearTemplate {
|
let template = GearTemplate {
|
||||||
nav_active: "gear",
|
nav_active: "gear",
|
||||||
|
|
@ -87,7 +89,7 @@ pub(crate) async fn create_gear(
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
payload: FlexiblePayload<NewGearSubmission>,
|
payload: FlexiblePayload<NewGearSubmission>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<GearSortKey>();
|
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let new_gear = submission.into_new_gear().map_err(ApiError::from)?;
|
let new_gear = submission.into_new_gear().map_err(ApiError::from)?;
|
||||||
|
|
||||||
|
|
@ -126,11 +128,12 @@ pub(crate) async fn create_gear(
|
||||||
let _ = state.timeline_repo.insert(event).await;
|
let _ = state.timeline_repo.insert(event).await;
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_gear_list_fragment(state, request, true)
|
render_gear_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else if matches!(source, PayloadSource::Form) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
let target = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request).page_href(1);
|
let target =
|
||||||
|
ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request, search).page_href(1);
|
||||||
Ok(Redirect::to(&target).into_response())
|
Ok(Redirect::to(&target).into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok((StatusCode::CREATED, Json(gear)).into_response())
|
Ok((StatusCode::CREATED, Json(gear)).into_response())
|
||||||
|
|
@ -153,7 +156,7 @@ pub(crate) async fn list_gear(
|
||||||
let request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc);
|
let request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc);
|
||||||
let page = state
|
let page = state
|
||||||
.gear_repo
|
.gear_repo
|
||||||
.list(filter, &request)
|
.list(filter, &request, None)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
Ok(Json(page.items))
|
Ok(Json(page.items))
|
||||||
|
|
@ -170,7 +173,7 @@ pub(crate) async fn update_gear(
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
payload: Json<UpdateGear>,
|
payload: Json<UpdateGear>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<GearSortKey>();
|
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
||||||
|
|
||||||
let gear = state
|
let gear = state
|
||||||
.gear_repo
|
.gear_repo
|
||||||
|
|
@ -179,7 +182,7 @@ pub(crate) async fn update_gear(
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_gear_list_fragment(state, request, true)
|
render_gear_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -231,10 +234,11 @@ impl NewGearSubmission {
|
||||||
async fn render_gear_list_fragment(
|
async fn render_gear_list_fragment(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request: ListRequest<GearSortKey>,
|
request: ListRequest<GearSortKey>,
|
||||||
|
search: Option<String>,
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let gear = load_gear_page(&state, request).await?;
|
let gear = load_gear_page(&state, request, search.as_deref()).await?;
|
||||||
let navigator = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request);
|
let navigator = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request, search);
|
||||||
|
|
||||||
let template = GearListTemplate {
|
let template = GearListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ macro_rules! define_delete_handler {
|
||||||
crate::application::routes::support::ListQuery,
|
crate::application::routes::support::ListQuery,
|
||||||
>,
|
>,
|
||||||
) -> Result<axum::response::Response, crate::application::errors::ApiError> {
|
) -> Result<axum::response::Response, crate::application::errors::ApiError> {
|
||||||
let request = query.into_request::<$sort_key>();
|
let (request, search) = query.into_request_and_search::<$sort_key>();
|
||||||
state
|
state
|
||||||
.$repo_field
|
.$repo_field
|
||||||
.delete(id)
|
.delete(id)
|
||||||
|
|
@ -96,7 +96,7 @@ macro_rules! define_delete_handler {
|
||||||
.map_err(crate::application::errors::AppError::from)?;
|
.map_err(crate::application::errors::AppError::from)?;
|
||||||
|
|
||||||
if crate::application::routes::support::is_datastar_request(&headers) {
|
if crate::application::routes::support::is_datastar_request(&headers) {
|
||||||
$render_fragment(state, request, true)
|
$render_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(crate::application::errors::ApiError::from)
|
.map_err(crate::application::errors::ApiError::from)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ const ROASTER_FRAGMENT_PATH: &str = "/roasters#roaster-list";
|
||||||
async fn load_roaster_page(
|
async fn load_roaster_page(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request: ListRequest<RoasterSortKey>,
|
request: ListRequest<RoasterSortKey>,
|
||||||
|
search: Option<&str>,
|
||||||
) -> Result<(Paginated<RoasterView>, ListNavigator<RoasterSortKey>), AppError> {
|
) -> Result<(Paginated<RoasterView>, ListNavigator<RoasterSortKey>), AppError> {
|
||||||
let page = state
|
let page = state
|
||||||
.roaster_repo
|
.roaster_repo
|
||||||
.list(&request)
|
.list(&request, search)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ async fn load_roaster_page(
|
||||||
RoasterView::from,
|
RoasterView::from,
|
||||||
ROASTER_PAGE_PATH,
|
ROASTER_PAGE_PATH,
|
||||||
ROASTER_FRAGMENT_PATH,
|
ROASTER_FRAGMENT_PATH,
|
||||||
|
search.map(String::from),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,16 +51,16 @@ pub(crate) async fn roasters_page(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let request = query.into_request::<RoasterSortKey>();
|
let (request, search) = query.into_request_and_search::<RoasterSortKey>();
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||||
return render_roaster_list_fragment(state, request, is_authenticated)
|
return render_roaster_list_fragment(state, request, search, is_authenticated)
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error);
|
.map_err(map_app_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (roasters, navigator) = load_roaster_page(&state, request)
|
let (roasters, navigator) = load_roaster_page(&state, request, search.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error)?;
|
.map_err(map_app_error)?;
|
||||||
|
|
||||||
|
|
@ -124,7 +126,7 @@ pub(crate) async fn create_roaster(
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
payload: FlexiblePayload<NewRoaster>,
|
payload: FlexiblePayload<NewRoaster>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<RoasterSortKey>();
|
let (request, search) = query.into_request_and_search::<RoasterSortKey>();
|
||||||
let (new_roaster, source) = payload.into_parts();
|
let (new_roaster, source) = payload.into_parts();
|
||||||
let new_roaster = new_roaster.normalize();
|
let new_roaster = new_roaster.normalize();
|
||||||
let roaster = state
|
let roaster = state
|
||||||
|
|
@ -134,12 +136,12 @@ pub(crate) async fn create_roaster(
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_roaster_list_fragment(state, request, true)
|
render_roaster_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else if matches!(source, PayloadSource::Form) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
let target =
|
let target = ListNavigator::new(ROASTER_PAGE_PATH, ROASTER_FRAGMENT_PATH, request, search)
|
||||||
ListNavigator::new(ROASTER_PAGE_PATH, ROASTER_FRAGMENT_PATH, request).page_href(1);
|
.page_href(1);
|
||||||
Ok(Redirect::to(&target).into_response())
|
Ok(Redirect::to(&target).into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok((StatusCode::CREATED, Json(roaster)).into_response())
|
Ok((StatusCode::CREATED, Json(roaster)).into_response())
|
||||||
|
|
@ -184,9 +186,10 @@ define_delete_handler!(
|
||||||
async fn render_roaster_list_fragment(
|
async fn render_roaster_list_fragment(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request: ListRequest<RoasterSortKey>,
|
request: ListRequest<RoasterSortKey>,
|
||||||
|
search: Option<String>,
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let (roasters, navigator) = load_roaster_page(&state, request).await?;
|
let (roasters, navigator) = load_roaster_page(&state, request, search.as_deref()).await?;
|
||||||
|
|
||||||
let template = RoasterListTemplate {
|
let template = RoasterListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ const ROAST_FRAGMENT_PATH: &str = "/roasts#roast-list";
|
||||||
async fn load_roast_page(
|
async fn load_roast_page(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request: ListRequest<RoastSortKey>,
|
request: ListRequest<RoastSortKey>,
|
||||||
|
search: Option<&str>,
|
||||||
) -> Result<(Paginated<RoastView>, ListNavigator<RoastSortKey>), AppError> {
|
) -> Result<(Paginated<RoastView>, ListNavigator<RoastSortKey>), AppError> {
|
||||||
let page = state
|
let page = state
|
||||||
.roast_repo
|
.roast_repo
|
||||||
.list(&request)
|
.list(&request, search)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|
@ -42,6 +43,7 @@ async fn load_roast_page(
|
||||||
RoastView::from_list_item,
|
RoastView::from_list_item,
|
||||||
ROAST_PAGE_PATH,
|
ROAST_PAGE_PATH,
|
||||||
ROAST_FRAGMENT_PATH,
|
ROAST_FRAGMENT_PATH,
|
||||||
|
search.map(String::from),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,11 +54,11 @@ pub(crate) async fn roasts_page(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let request = query.into_request::<RoastSortKey>();
|
let (request, search) = query.into_request_and_search::<RoastSortKey>();
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||||
return render_roast_list_fragment(state, request, is_authenticated)
|
return render_roast_list_fragment(state, request, search, is_authenticated)
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error);
|
.map_err(map_app_error);
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +71,7 @@ pub(crate) async fn roasts_page(
|
||||||
|
|
||||||
let roaster_options = roasters.into_iter().map(RoasterOptionView::from).collect();
|
let roaster_options = roasters.into_iter().map(RoasterOptionView::from).collect();
|
||||||
|
|
||||||
let (roasts, navigator) = load_roast_page(&state, request)
|
let (roasts, navigator) = load_roast_page(&state, request, search.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(map_app_error)?;
|
.map_err(map_app_error)?;
|
||||||
|
|
||||||
|
|
@ -107,7 +109,7 @@ pub(crate) async fn roast_page(
|
||||||
let bag_request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc);
|
let bag_request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc);
|
||||||
let bags_page = state
|
let bags_page = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
.list(BagFilter::for_roast(roast.id), &bag_request)
|
.list(BagFilter::for_roast(roast.id), &bag_request, None)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||||
|
|
||||||
|
|
@ -137,7 +139,7 @@ pub(crate) async fn create_roast(
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
payload: FlexiblePayload<NewRoastSubmission>,
|
payload: FlexiblePayload<NewRoastSubmission>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let request = query.into_request::<RoastSortKey>();
|
let (request, search) = query.into_request_and_search::<RoastSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let new_roast = submission.into_new_roast().map_err(ApiError::from)?;
|
let new_roast = submission.into_new_roast().map_err(ApiError::from)?;
|
||||||
|
|
||||||
|
|
@ -154,11 +156,12 @@ pub(crate) async fn create_roast(
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_roast_list_fragment(state, request, true)
|
render_roast_list_fragment(state, request, search, true)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else if matches!(source, PayloadSource::Form) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
let target = ListNavigator::new(ROAST_PAGE_PATH, ROAST_FRAGMENT_PATH, request).page_href(1);
|
let target =
|
||||||
|
ListNavigator::new(ROAST_PAGE_PATH, ROAST_FRAGMENT_PATH, request, search).page_href(1);
|
||||||
Ok(Redirect::to(&target).into_response())
|
Ok(Redirect::to(&target).into_response())
|
||||||
} else {
|
} else {
|
||||||
let enriched = state
|
let enriched = state
|
||||||
|
|
@ -343,9 +346,10 @@ impl TastingNotesInput {
|
||||||
async fn render_roast_list_fragment(
|
async fn render_roast_list_fragment(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request: ListRequest<RoastSortKey>,
|
request: ListRequest<RoastSortKey>,
|
||||||
|
search: Option<String>,
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let (roasts, navigator) = load_roast_page(&state, request).await?;
|
let (roasts, navigator) = load_roast_page(&state, request, search.as_deref()).await?;
|
||||||
|
|
||||||
let template = RoastListTemplate {
|
let template = RoastListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ pub(crate) struct ListQuery {
|
||||||
sort_key: Option<String>,
|
sort_key: Option<String>,
|
||||||
#[serde(default, rename = "dir")]
|
#[serde(default, rename = "dir")]
|
||||||
sort_dir: Option<String>,
|
sort_dir: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
q: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -48,31 +50,42 @@ enum PageSizeParam {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListQuery {
|
impl ListQuery {
|
||||||
pub fn into_request<K: SortKey>(self) -> ListRequest<K> {
|
pub fn into_request_and_search<K: SortKey>(self) -> (ListRequest<K>, Option<String>) {
|
||||||
self.into_request_with_default::<K>(DEFAULT_PAGE_SIZE)
|
self.into_request_and_search_with_default::<K>(DEFAULT_PAGE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_request_with_default<K: SortKey>(self, default_page_size: u32) -> ListRequest<K> {
|
pub fn into_request_and_search_with_default<K: SortKey>(
|
||||||
let page = self.page.unwrap_or(1);
|
self,
|
||||||
let page_size = match self.page_size {
|
default_page_size: u32,
|
||||||
|
) -> (ListRequest<K>, Option<String>) {
|
||||||
|
let ListQuery {
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
sort_key,
|
||||||
|
sort_dir,
|
||||||
|
q,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let search = q.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
||||||
|
|
||||||
|
let page = page.unwrap_or(1);
|
||||||
|
let page_size = match page_size {
|
||||||
Some(PageSizeParam::Number(value)) => PageSize::limited(value),
|
Some(PageSizeParam::Number(value)) => PageSize::limited(value),
|
||||||
Some(PageSizeParam::Text(text)) => page_size_from_text(&text),
|
Some(PageSizeParam::Text(text)) => page_size_from_text(&text),
|
||||||
None => PageSize::limited(default_page_size.max(1)),
|
None => PageSize::limited(default_page_size.max(1)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sort_key = self
|
let sk = sort_key
|
||||||
.sort_key
|
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(K::from_query)
|
.and_then(K::from_query)
|
||||||
.unwrap_or_else(K::default);
|
.unwrap_or_else(K::default);
|
||||||
|
|
||||||
let sort_direction = self
|
let sd = sort_dir
|
||||||
.sort_dir
|
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(parse_direction)
|
.and_then(parse_direction)
|
||||||
.unwrap_or_else(|| sort_key.default_direction());
|
.unwrap_or_else(|| sk.default_direction());
|
||||||
|
|
||||||
ListRequest::new(page, page_size, sort_key, sort_direction)
|
(ListRequest::new(page, page_size, sk, sd), search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,13 +113,14 @@ pub fn build_page_view<K, T, V>(
|
||||||
view_mapper: impl FnMut(T) -> V,
|
view_mapper: impl FnMut(T) -> V,
|
||||||
base_path: &'static str,
|
base_path: &'static str,
|
||||||
fragment_path: &'static str,
|
fragment_path: &'static str,
|
||||||
|
search: Option<String>,
|
||||||
) -> (Paginated<V>, ListNavigator<K>)
|
) -> (Paginated<V>, ListNavigator<K>)
|
||||||
where
|
where
|
||||||
K: SortKey,
|
K: SortKey,
|
||||||
{
|
{
|
||||||
let normalized_request = normalize_request(request, &page);
|
let normalized_request = normalize_request(request, &page);
|
||||||
let view_page = Paginated::from_page(page, view_mapper);
|
let view_page = Paginated::from_page(page, view_mapper);
|
||||||
let navigator = ListNavigator::new(base_path, fragment_path, normalized_request);
|
let navigator = ListNavigator::new(base_path, fragment_path, normalized_request, search);
|
||||||
(view_page, navigator)
|
(view_page, navigator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ async fn load_timeline_page(
|
||||||
TIMELINE_PAGE_PATH,
|
TIMELINE_PAGE_PATH,
|
||||||
TIMELINE_FRAGMENT_PATH,
|
TIMELINE_FRAGMENT_PATH,
|
||||||
normalized_request,
|
normalized_request,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(TimelinePageData {
|
Ok(TimelinePageData {
|
||||||
|
|
|
||||||
|
|
@ -108,11 +108,12 @@ impl<T> Paginated<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ListNavigator<K: SortKey> {
|
pub struct ListNavigator<K: SortKey> {
|
||||||
base_path: &'static str,
|
base_path: &'static str,
|
||||||
fragment_path: &'static str,
|
fragment_path: &'static str,
|
||||||
request: ListRequest<K>,
|
request: ListRequest<K>,
|
||||||
|
search: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: SortKey> ListNavigator<K> {
|
impl<K: SortKey> ListNavigator<K> {
|
||||||
|
|
@ -120,11 +121,13 @@ impl<K: SortKey> ListNavigator<K> {
|
||||||
base_path: &'static str,
|
base_path: &'static str,
|
||||||
fragment_path: &'static str,
|
fragment_path: &'static str,
|
||||||
request: ListRequest<K>,
|
request: ListRequest<K>,
|
||||||
|
search: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base_path,
|
base_path,
|
||||||
fragment_path,
|
fragment_path,
|
||||||
request,
|
request,
|
||||||
|
search,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,27 +200,50 @@ impl<K: SortKey> ListNavigator<K> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query(&self) -> String {
|
pub fn query(&self) -> String {
|
||||||
Self::query_string(self.request)
|
self.build_query_string(self.request)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_for_page(&self, page: u32) -> String {
|
pub fn query_for_page(&self, page: u32) -> String {
|
||||||
Self::query_string(self.request.with_page(page))
|
self.build_query_string(self.request.with_page(page))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_for_rows(&self, value: &str) -> String {
|
pub fn query_for_rows(&self, value: &str) -> String {
|
||||||
Self::query_string(Self::request_for_rows(self.request, value))
|
self.build_query_string(Self::request_for_rows(self.request, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_for_sort(&self, key: &str) -> String {
|
pub fn query_for_sort(&self, key: &str) -> String {
|
||||||
Self::query_string(Self::request_for_sort(self.request, key))
|
self.build_query_string(Self::request_for_sort(self.request, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_value(&self) -> &str {
|
||||||
|
self.search.as_deref().unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_search(&self) -> bool {
|
||||||
|
self.search.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the base path (e.g., "/roasters") without query or fragment.
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
self.base_path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns query params for search actions (page reset to 1, preserves `sort/page_size`).
|
||||||
|
/// Does NOT include the `q` parameter — the template appends it dynamically from JS.
|
||||||
|
pub fn search_query_base(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"page=1&page_size={}&sort={}&dir={}",
|
||||||
|
self.request.page_size().to_query_value(),
|
||||||
|
self.request.sort_key().query_value(),
|
||||||
|
self.request.sort_direction().as_str()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unused_self)] // Keeps consistent method interface
|
|
||||||
fn build_href(&self, path: &str, request: ListRequest<K>) -> String {
|
fn build_href(&self, path: &str, request: ListRequest<K>) -> String {
|
||||||
if let Some((base, fragment)) = path.split_once('#') {
|
if let Some((base, fragment)) = path.split_once('#') {
|
||||||
format!("{}?{}#{}", base, Self::query_string(request), fragment)
|
format!("{}?{}#{}", base, self.build_query_string(request), fragment)
|
||||||
} else {
|
} else {
|
||||||
format!("{}?{}", path, Self::query_string(request))
|
format!("{}?{}", path, self.build_query_string(request))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,15 +257,36 @@ impl<K: SortKey> ListNavigator<K> {
|
||||||
request.with_page(1).with_sort(sort_key)
|
request.with_page(1).with_sort(sort_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_string(request: ListRequest<K>) -> String {
|
fn build_query_string(&self, request: ListRequest<K>) -> String {
|
||||||
format!(
|
let mut qs = format!(
|
||||||
"page={}&page_size={}&sort={}&dir={}",
|
"page={}&page_size={}&sort={}&dir={}",
|
||||||
request.page(),
|
request.page(),
|
||||||
request.page_size().to_query_value(),
|
request.page_size().to_query_value(),
|
||||||
request.sort_key().query_value(),
|
request.sort_key().query_value(),
|
||||||
request.sort_direction().as_str()
|
request.sort_direction().as_str()
|
||||||
)
|
);
|
||||||
|
if let Some(ref q) = self.search {
|
||||||
|
qs.push_str("&q=");
|
||||||
|
qs.push_str(&encode_uri_component(q));
|
||||||
}
|
}
|
||||||
|
qs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_uri_component(s: &str) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut result = String::with_capacity(s.len() * 3);
|
||||||
|
for byte in s.as_bytes() {
|
||||||
|
match byte {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||||
|
result.push(*byte as char);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = write!(result, "%{byte:02X}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn page_size_from_text(value: &str) -> PageSize {
|
fn page_size_from_text(value: &str) -> PageSize {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ fn list_all_request<K: brewlog::domain::listing::SortKey>() -> ListRequest<K> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_all_roasters(repo: &dyn RoasterRepository) -> Vec<Roaster> {
|
async fn list_all_roasters(repo: &dyn RoasterRepository) -> Vec<Roaster> {
|
||||||
repo.list(&list_all_request::<RoasterSortKey>())
|
repo.list(&list_all_request::<RoasterSortKey>(), None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to list roasters")
|
.expect("failed to list roasters")
|
||||||
.items
|
.items
|
||||||
|
|
@ -66,7 +66,7 @@ async fn list_all_roasters(repo: &dyn RoasterRepository) -> Vec<Roaster> {
|
||||||
|
|
||||||
async fn list_all_roasts(repo: &dyn RoastRepository) -> Vec<Roast> {
|
async fn list_all_roasts(repo: &dyn RoastRepository) -> Vec<Roast> {
|
||||||
let page = repo
|
let page = repo
|
||||||
.list(&list_all_request::<RoastSortKey>())
|
.list(&list_all_request::<RoastSortKey>(), None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to list roasts");
|
.expect("failed to list roasts");
|
||||||
page.items.into_iter().map(|rwr| rwr.roast).collect()
|
page.items.into_iter().map(|rwr| rwr.roast).collect()
|
||||||
|
|
@ -74,14 +74,14 @@ async fn list_all_roasts(repo: &dyn RoastRepository) -> Vec<Roast> {
|
||||||
|
|
||||||
async fn list_all_bags(repo: &dyn BagRepository) -> Vec<Bag> {
|
async fn list_all_bags(repo: &dyn BagRepository) -> Vec<Bag> {
|
||||||
let page = repo
|
let page = repo
|
||||||
.list(BagFilter::all(), &list_all_request::<BagSortKey>())
|
.list(BagFilter::all(), &list_all_request::<BagSortKey>(), None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to list bags");
|
.expect("failed to list bags");
|
||||||
page.items.into_iter().map(|bwr| bwr.bag).collect()
|
page.items.into_iter().map(|bwr| bwr.bag).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_all_gear(repo: &dyn GearRepository) -> Vec<Gear> {
|
async fn list_all_gear(repo: &dyn GearRepository) -> Vec<Gear> {
|
||||||
repo.list(GearFilter::all(), &list_all_request::<GearSortKey>())
|
repo.list(GearFilter::all(), &list_all_request::<GearSortKey>(), None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to list gear")
|
.expect("failed to list gear")
|
||||||
.items
|
.items
|
||||||
|
|
@ -89,7 +89,7 @@ async fn list_all_gear(repo: &dyn GearRepository) -> Vec<Gear> {
|
||||||
|
|
||||||
async fn list_all_brews(repo: &dyn BrewRepository) -> Vec<Brew> {
|
async fn list_all_brews(repo: &dyn BrewRepository) -> Vec<Brew> {
|
||||||
let page = repo
|
let page = repo
|
||||||
.list(BrewFilter::all(), &list_all_request::<BrewSortKey>())
|
.list(BrewFilter::all(), &list_all_request::<BrewSortKey>(), None)
|
||||||
.await
|
.await
|
||||||
.expect("failed to list brews");
|
.expect("failed to list brews");
|
||||||
page.items.into_iter().map(|bwd| bwd.brew).collect()
|
page.items.into_iter().map(|bwd| bwd.brew).collect()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue