Frontend improvements

Reviewed-on: #1
This commit is contained in:
2025-12-23 10:28:03 +00:00
parent 3a19995008
commit 8c010e06e5
17 changed files with 896 additions and 167 deletions

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use validator::Validate;
use notes_domain::{Note, NoteFilter, Tag};
use notes_domain::{Note, Tag};
/// Request to create a new note
#[derive(Debug, Deserialize, Validate)]
@@ -47,17 +47,8 @@ pub struct UpdateNoteRequest {
pub struct ListNotesQuery {
pub pinned: Option<bool>,
pub archived: Option<bool>,
pub tag: Option<Uuid>,
}
impl From<ListNotesQuery> for NoteFilter {
fn from(query: ListNotesQuery) -> Self {
let mut filter = NoteFilter::new();
filter.is_pinned = query.pinned;
filter.is_archived = query.archived;
filter.tag_id = query.tag;
filter
}
/// Tag name to filter by (will be looked up by route handler)
pub tag: Option<String>,
}
/// Query parameters for search
@@ -119,6 +110,13 @@ pub struct CreateTagRequest {
pub name: String,
}
/// Request to rename a tag
#[derive(Debug, Deserialize, Validate)]
pub struct RenameTagRequest {
#[validate(length(min = 1, max = 50, message = "Tag name must be 1-50 characters"))]
pub name: String,
}
/// Login request
#[derive(Debug, Deserialize, Validate)]
pub struct LoginRequest {

View File

@@ -36,5 +36,8 @@ pub fn api_v1_router() -> Router<AppState> {
.route("/import", post(import_export::import_data))
// Tag routes
.route("/tags", get(tags::list_tags).post(tags::create_tag))
.route("/tags/{id}", delete(tags::delete_tag))
.route(
"/tags/{id}",
delete(tags::delete_tag).patch(tags::rename_tag),
)
}

View File

@@ -33,9 +33,23 @@ pub async fn list_notes(
)))?;
let user_id = user.id();
let service = NoteService::new(state.note_repo, state.tag_repo);
// Build the filter, looking up tag_id by name if needed
let mut filter = notes_domain::NoteFilter::new();
filter.is_pinned = query.pinned;
filter.is_archived = query.archived;
let notes = service.list_notes(user_id, query.into()).await?;
// Look up tag by name if provided
if let Some(ref tag_name) = query.tag {
if let Ok(Some(tag)) = state.tag_repo.find_by_name(user_id, tag_name).await {
filter.tag_id = Some(tag.id);
} else {
// Tag not found, return empty results
return Ok(Json(vec![]));
}
}
let service = NoteService::new(state.note_repo, state.tag_repo);
let notes = service.list_notes(user_id, filter).await?;
let response: Vec<NoteResponse> = notes.into_iter().map(NoteResponse::from).collect();
Ok(Json(response))

View File

@@ -1,9 +1,9 @@
//! Tag route handlers
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
Json,
};
use axum_login::{AuthSession, AuthUser};
use uuid::Uuid;
@@ -12,7 +12,7 @@ use validator::Validate;
use notes_domain::TagService;
use crate::auth::AuthBackend;
use crate::dto::{CreateTagRequest, TagResponse};
use crate::dto::{CreateTagRequest, RenameTagRequest, TagResponse};
use crate::error::{ApiError, ApiResult};
use crate::state::AppState;
@@ -22,14 +22,18 @@ pub async fn list_tags(
State(state): State<AppState>,
auth: AuthSession<AuthBackend>,
) -> ApiResult<Json<Vec<TagResponse>>> {
let user = auth.user.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized("Login required".to_string())))?;
let user = auth
.user
.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized(
"Login required".to_string(),
)))?;
let user_id = user.id();
let service = TagService::new(state.tag_repo);
let tags = service.list_tags(user_id).await?;
let response: Vec<TagResponse> = tags.into_iter().map(TagResponse::from).collect();
Ok(Json(response))
}
@@ -40,18 +44,50 @@ pub async fn create_tag(
auth: AuthSession<AuthBackend>,
Json(payload): Json<CreateTagRequest>,
) -> ApiResult<(StatusCode, Json<TagResponse>)> {
let user = auth.user.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized("Login required".to_string())))?;
let user = auth
.user
.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized(
"Login required".to_string(),
)))?;
let user_id = user.id();
payload.validate().map_err(|e| ApiError::validation(e.to_string()))?;
payload
.validate()
.map_err(|e| ApiError::validation(e.to_string()))?;
let service = TagService::new(state.tag_repo);
let tag = service.create_tag(user_id, &payload.name).await?;
Ok((StatusCode::CREATED, Json(TagResponse::from(tag))))
}
/// Rename a tag
/// PATCH /api/v1/tags/:id
pub async fn rename_tag(
State(state): State<AppState>,
auth: AuthSession<AuthBackend>,
Path(id): Path<Uuid>,
Json(payload): Json<RenameTagRequest>,
) -> ApiResult<Json<TagResponse>> {
let user = auth
.user
.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized(
"Login required".to_string(),
)))?;
let user_id = user.id();
payload
.validate()
.map_err(|e| ApiError::validation(e.to_string()))?;
let service = TagService::new(state.tag_repo);
let tag = service.rename_tag(id, user_id, &payload.name).await?;
Ok(Json(TagResponse::from(tag)))
}
/// Delete a tag
/// DELETE /api/v1/tags/:id
pub async fn delete_tag(
@@ -59,12 +95,16 @@ pub async fn delete_tag(
auth: AuthSession<AuthBackend>,
Path(id): Path<Uuid>,
) -> ApiResult<StatusCode> {
let user = auth.user.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized("Login required".to_string())))?;
let user = auth
.user
.ok_or(ApiError::Domain(notes_domain::DomainError::Unauthorized(
"Login required".to_string(),
)))?;
let user_id = user.id();
let service = TagService::new(state.tag_repo);
service.delete_tag(id, user_id).await?;
Ok(StatusCode::NO_CONTENT)
}