- Foldable left panel (user profile) and right panel (task details) - Clicking a task in the list or graph node selects it and shows details - Both views (task list + graph) always mounted via absolute inset-0 for correct canvas dimensions; tabs toggle visibility with opacity - Graph node selection animation: other nodes repel outward (charge -600), then selected node smoothly slides to center (500ms cubic ease-out), then charge restores to -120 and graph stabilizes - Graph re-fits on tab switch and panel resize via ResizeObserver - Fix UUID string IDs throughout (backend returns UUIDs, not integers) - Add TaskDetailPanel, UserPanel components - Add CLAUDE.md project documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
1.9 KiB
Rust
78 lines
1.9 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct Task {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub completed: bool,
|
|
pub created_at: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GraphNode {
|
|
pub id: String,
|
|
pub label: String,
|
|
pub completed: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GraphEdge {
|
|
pub source: String,
|
|
pub target: String,
|
|
pub weight: f32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GraphData {
|
|
pub nodes: Vec<GraphNode>,
|
|
pub edges: Vec<GraphEdge>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateTask {
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateTask {
|
|
pub title: Option<String>,
|
|
pub description: Option<String>,
|
|
pub completed: Option<bool>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_task_serialization() {
|
|
let task = Task {
|
|
id: "abc-123".to_string(),
|
|
title: "Test task".to_string(),
|
|
description: Some("A description".to_string()),
|
|
completed: false,
|
|
created_at: 1234567890,
|
|
};
|
|
let json = serde_json::to_string(&task).unwrap();
|
|
let deserialized: Task = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(deserialized.id, task.id);
|
|
assert_eq!(deserialized.title, task.title);
|
|
assert_eq!(deserialized.completed, task.completed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_task_no_description() {
|
|
let task = Task {
|
|
id: "xyz".to_string(),
|
|
title: "No desc".to_string(),
|
|
description: None,
|
|
completed: true,
|
|
created_at: 0,
|
|
};
|
|
let json = serde_json::to_string(&task).unwrap();
|
|
assert!(json.contains("\"description\":null"));
|
|
}
|
|
}
|