pagnation

This commit is contained in:
Mateusz Gruszczyński
2026-08-02 14:26:32 +02:00
parent 464660a274
commit f71ebefa82
7 changed files with 290 additions and 20 deletions
+46 -3
View File
@@ -19,7 +19,7 @@ pub use pads_public::*;
use axum::{
Json,
extract::{Multipart, Path, State},
extract::{Multipart, Path, Query, State},
http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
@@ -283,6 +283,31 @@ pub struct WorkspaceInfo {
pub struct WorkspaceOpenResponse {
workspace: WorkspaceInfo,
notes: Vec<NoteListItem>,
pagination: ListPaginationMeta,
}
#[derive(Debug, Deserialize)]
pub struct WorkspaceNotesQuery {
#[serde(default)]
q: String,
#[serde(default = "default_list_page")]
page: usize,
#[serde(default = "default_list_per_page")]
per_page: usize,
}
#[derive(Debug, Serialize)]
pub struct ListPaginationMeta {
page: usize,
per_page: usize,
total: usize,
total_pages: usize,
}
fn default_list_page() -> usize { 1 }
fn default_list_per_page() -> usize { 25 }
fn normalize_list_per_page(value: usize) -> usize {
match value { 25 | 50 | 100 => value, _ => 25 }
}
#[derive(Debug, Serialize)]
@@ -552,6 +577,7 @@ pub async fn open_workspace(
State(state): State<SharedState>,
headers: HeaderMap,
Path(workspace_slug): Path<String>,
Query(query): Query<WorkspaceNotesQuery>,
Json(payload): Json<PasswordRequest>,
) -> Result<Json<WorkspaceOpenResponse>, ApiError> {
let workspace = authorized_workspace(
@@ -573,9 +599,16 @@ pub async fn open_workspace(
.into_iter()
.map(|stats| (stats.note_id, stats))
.collect::<std::collections::HashMap<_, _>>();
let notes = db::list_notes(&state.db, workspace.id)
let search = query.q.trim().to_lowercase();
let mut notes = db::list_notes(&state.db, workspace.id)
.await?
.into_iter()
.filter(|note| {
search.is_empty()
|| note.title.to_lowercase().contains(&search)
|| note.slug.to_lowercase().contains(&search)
|| note.created_by.as_deref().unwrap_or_default().to_lowercase().contains(&search)
})
.map(|note| {
let stats = stats.get(&note.id);
NoteListItem {
@@ -592,11 +625,21 @@ pub async fn open_workspace(
revision_count: stats.map_or(0, |value| value.revision_count),
}
})
.collect();
.collect::<Vec<_>>();
notes.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
let page = query.page.max(1);
let per_page = normalize_list_per_page(query.per_page);
let total = notes.len();
let total_pages = ((total + per_page - 1) / per_page).max(1);
let page = page.min(total_pages);
let start = (page - 1) * per_page;
let notes = notes.into_iter().skip(start).take(per_page).collect();
Ok(Json(WorkspaceOpenResponse {
workspace: workspace_info_from(&workspace),
notes,
pagination: ListPaginationMeta { page, per_page, total, total_pages },
}))
}
+74 -11
View File
@@ -16,7 +16,7 @@ use argon2::{
};
use axum::{
Json,
extract::{Path as AxumPath, State},
extract::{Path as AxumPath, Query, State},
http::{HeaderMap, StatusCode, header},
response::{IntoResponse, Response},
};
@@ -116,6 +116,8 @@ pub struct ResourceActionRequest {
}
#[derive(Serialize)]
pub struct ResourceItem {
#[serde(default)]
kind: String,
slug: String,
title: String,
protected: i64,
@@ -201,6 +203,7 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for User {
impl<'r> sqlx::FromRow<'r, AnyRow> for ResourceItem {
fn from_row(row: &'r AnyRow) -> Result<Self, sqlx::Error> {
Ok(Self {
kind: String::new(),
slug: crate::row_decode::text(row, "slug")?,
title: crate::row_decode::text(row, "title")?,
protected: row.try_get("protected")?,
@@ -257,8 +260,32 @@ pub struct RevokeShareLinkRequest {
#[derive(Serialize)]
pub struct ResourceList {
workspaces: Vec<ResourceItem>,
pads: Vec<ResourceItem>,
items: Vec<ResourceItem>,
pagination: PaginationMeta,
}
#[derive(Debug, Deserialize)]
pub struct ResourceListQuery {
#[serde(default)]
q: String,
#[serde(default = "default_page")]
page: usize,
#[serde(default = "default_per_page")]
per_page: usize,
}
#[derive(Debug, Serialize)]
pub struct PaginationMeta {
page: usize,
per_page: usize,
total: usize,
total_pages: usize,
}
fn default_page() -> usize { 1 }
fn default_per_page() -> usize { 25 }
fn normalized_per_page(value: usize) -> usize {
match value { 25 | 50 | 100 => value, _ => 25 }
}
#[derive(Serialize)]
pub struct SessionResponse {
@@ -1142,6 +1169,7 @@ async fn send_account_action(
pub async fn resources(
State(state): State<SharedState>,
headers: HeaderMap,
Query(query): Query<ResourceListQuery>,
) -> Result<Json<ResourceList>, AuthError> {
let user = require_user(&state, &headers).await?;
let workspaces = sqlx::query_as::<_, ResourceItem>(queries::get(
@@ -1153,14 +1181,49 @@ pub async fn resources(
.fetch_all(state.db.pool())
.await
.map_err(AuthError::database)?;
let pads =
sqlx::query_as::<_, ResourceItem>(queries::get(state.db.kind(), queries::USER_LIST_PADS))
.bind(user.id)
.bind(user.id)
.fetch_all(state.db.pool())
.await
.map_err(AuthError::database)?;
Ok(Json(ResourceList { workspaces, pads }))
let pads = sqlx::query_as::<_, ResourceItem>(
queries::get(state.db.kind(), queries::USER_LIST_PADS),
)
.bind(user.id)
.bind(user.id)
.fetch_all(state.db.pool())
.await
.map_err(AuthError::database)?;
let search = query.q.trim().to_lowercase();
let mut items = workspaces
.into_iter()
.map(|item| (item, "workspace"))
.chain(pads.into_iter().map(|item| (item, "pad")))
.filter(|(item, kind)| {
search.is_empty()
|| item.title.to_lowercase().contains(&search)
|| item.slug.to_lowercase().contains(&search)
|| kind.contains(&search)
})
.collect::<Vec<_>>();
items.sort_by(|(left, _), (right, _)| right.updated_at.cmp(&left.updated_at));
let page = query.page.max(1);
let per_page = normalized_per_page(query.per_page);
let total = items.len();
let total_pages = ((total + per_page - 1) / per_page).max(1);
let page = page.min(total_pages);
let start = (page - 1) * per_page;
let items = items
.into_iter()
.skip(start)
.take(per_page)
.map(|(mut item, kind)| {
item.kind = kind.to_string();
item
})
.collect();
Ok(Json(ResourceList {
items,
pagination: PaginationMeta { page, per_page, total, total_pages },
}))
}
pub async fn update_resource(