Compare commits
No commits in common. "d4b83023064b321a5c445269e28a29126fdad2e8" and "69f4a6f1caa1f76af2acdd5e68291b111ebadcd8" have entirely different histories.
d4b8302306
...
69f4a6f1ca
13 changed files with 11 additions and 911 deletions
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
|
|
@ -1,8 +1,5 @@
|
||||||
target
|
target
|
||||||
coverage
|
coverage
|
||||||
reports
|
|
||||||
|
|
||||||
.sqlx
|
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
|
|
||||||
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
|
|
@ -3500,11 +3500,9 @@ dependencies = [
|
||||||
"bitflags 2.9.3",
|
"bitflags 2.9.3",
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono", "migrate"] }
|
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono", "migrate"] }
|
||||||
tokio = { version = "1.47.1", features = ["rt-multi-thread", "tracing"] }
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "tracing"] }
|
||||||
tower-http = { version = "0.6.0", features = ["cors", "trace"] }
|
tower-http = { version = "0.6.0", features = ["cors"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
uuid = { version = "1.18.0", features = ["serde", "v4"] }
|
uuid = { version = "1.18.0", features = ["serde", "v4"] }
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
ALTER TABLE tasks RENAME TO tasks_old;
|
|
||||||
|
|
||||||
CREATE TABLE tasks (
|
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
status TEXT CHECK(status IN ('todo', 'done', 'backlog')) DEFAULT 'todo',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
completed_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tasks SELECT id, title, description, status, created_at, updated_at, completed_at FROM tasks_old;
|
|
||||||
|
|
||||||
DROP TABLE tasks_old;
|
|
||||||
DROP TABLE projects;
|
|
||||||
DROP TABLE folders;
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
CREATE TABLE folders (
|
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL UNIQUE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE projects (
|
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
color VARCHAR(7) NOT NULL, -- hex code
|
|
||||||
folder_id UUID,
|
|
||||||
sort_order INTEGER NOT NULL UNIQUE,
|
|
||||||
status TEXT CHECK(status IN ('active', 'done', 'backlog')) DEFAULT 'active',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
completed_at TIMESTAMP,
|
|
||||||
FOREIGN KEY(folder_id) REFERENCES folders(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE tasks RENAME TO tasks_old;
|
|
||||||
|
|
||||||
-- Add up migration script here
|
|
||||||
CREATE TABLE tasks (
|
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
status TEXT CHECK(status IN ('todo', 'done', 'backlog')) DEFAULT 'todo',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
completed_at TIMESTAMP,
|
|
||||||
project_id UUID,
|
|
||||||
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tasks SELECT *, NULL as project_id FROM tasks_old;
|
|
||||||
|
|
||||||
DROP TABLE tasks_old;
|
|
||||||
|
|
@ -32,7 +32,6 @@ async fn main() {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
.nest("/api/tasks", services::create_task_router())
|
.nest("/api/tasks", services::create_task_router())
|
||||||
.nest("/api/projects", services::create_project_router())
|
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.with_state(pool);
|
.with_state(pool);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
mod project;
|
|
||||||
mod task;
|
mod task;
|
||||||
|
|
||||||
pub use project::*;
|
|
||||||
pub use task::*;
|
pub use task::*;
|
||||||
|
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{Pool, Row, Sqlite};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::services::AppError;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, sqlx::Type)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[sqlx(rename_all = "lowercase")]
|
|
||||||
pub enum ProjectStatus {
|
|
||||||
Active,
|
|
||||||
Done,
|
|
||||||
Backlog,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Serialize)]
|
|
||||||
pub struct ProjectModel {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub title: String,
|
|
||||||
pub color: String,
|
|
||||||
pub folder_id: Option<Uuid>,
|
|
||||||
pub sort_order: i64,
|
|
||||||
pub status: ProjectStatus,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
pub completed_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProjectModel {
|
|
||||||
async fn get_new_sort_order(pool: &Pool<Sqlite>) -> anyhow::Result<i64> {
|
|
||||||
let result = sqlx::query!("SELECT MAX(sort_order) as max_order FROM projects;")
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match result.max_order {
|
|
||||||
Some(i) => Ok(i + 1000),
|
|
||||||
None => Ok(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub async fn insert(
|
|
||||||
pool: &Pool<Sqlite>,
|
|
||||||
title: String,
|
|
||||||
color: String,
|
|
||||||
) -> anyhow::Result<ProjectModel, AppError> {
|
|
||||||
if title.is_empty() {
|
|
||||||
return Err(AppError::Unprocessable(
|
|
||||||
"Title must not be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if color.len() != 7 || color.chars().next() != Some('#') {
|
|
||||||
return Err(AppError::Unprocessable(
|
|
||||||
"Color is not a valid hex".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = Uuid::new_v4();
|
|
||||||
let sort_order = ProjectModel::get_new_sort_order(pool).await?;
|
|
||||||
|
|
||||||
let result = sqlx::query_as(
|
|
||||||
"INSERT INTO projects(id, title, color, sort_order) VALUES($1, $2, $3, $4) RETURNING *",
|
|
||||||
)
|
|
||||||
.bind(id)
|
|
||||||
.bind(title)
|
|
||||||
.bind(color)
|
|
||||||
.bind(sort_order)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_by_id(
|
|
||||||
pool: &Pool<Sqlite>,
|
|
||||||
id: Uuid,
|
|
||||||
) -> anyhow::Result<ProjectModel, AppError> {
|
|
||||||
sqlx::query_as("SELECT * FROM projects WHERE id = $1")
|
|
||||||
.bind(id)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
sqlx::Error::RowNotFound => AppError::NotFound,
|
|
||||||
e => e.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list(pool: &Pool<Sqlite>) -> anyhow::Result<Vec<ProjectModel>> {
|
|
||||||
sqlx::query_as("SELECT * FROM projects;")
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(self, pool: &Pool<Sqlite>) -> anyhow::Result<ProjectModel, AppError> {
|
|
||||||
let now: DateTime<Utc> = Utc::now();
|
|
||||||
|
|
||||||
let _ = sqlx::query!(
|
|
||||||
"UPDATE projects
|
|
||||||
SET title=$1, color=$2, folder_id=$3, status=$4, sort_order=$5, updated_at=$6, completed_at=$7
|
|
||||||
WHERE id=$8",
|
|
||||||
self.title,
|
|
||||||
self.color,
|
|
||||||
self.folder_id,
|
|
||||||
self.status,
|
|
||||||
self.sort_order,
|
|
||||||
now,
|
|
||||||
self.completed_at,
|
|
||||||
self.id,
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ProjectModel::get_by_id(pool, self.id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete(pool: &Pool<Sqlite>, id: Uuid) -> anyhow::Result<()> {
|
|
||||||
sqlx::query!("DELETE FROM projects WHERE id = $1", id)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
mod projects;
|
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
|
@ -27,14 +26,11 @@ pub struct ErrorJson {
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
match self {
|
match self {
|
||||||
Self::InternalError(anyhow_error) => {
|
Self::InternalError(anyhow_error) => (
|
||||||
error!(err = ?anyhow_error, "Internal Server Error");
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
(
|
format!("Something went wrong {}", anyhow_error),
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
)
|
||||||
format!("Something went wrong {}", anyhow_error),
|
.into_response(),
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
Self::JsonExtractError(rej) => (
|
Self::JsonExtractError(rej) => (
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
Json(ErrorJson {
|
Json(ErrorJson {
|
||||||
|
|
@ -89,7 +85,5 @@ impl From<sqlx::Error> for AppError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use projects::*;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
pub use tasks::*;
|
pub use tasks::*;
|
||||||
use tracing::error;
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
use axum::{
|
|
||||||
Json, Router,
|
|
||||||
extract::{Path, State},
|
|
||||||
http::StatusCode,
|
|
||||||
routing::{get, post},
|
|
||||||
};
|
|
||||||
use axum_extra::extract::WithRejection;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{Pool, Sqlite};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::models::{ProjectModel, ProjectStatus};
|
|
||||||
|
|
||||||
use super::AppError;
|
|
||||||
|
|
||||||
pub fn create_project_router() -> Router<Pool<Sqlite>> {
|
|
||||||
Router::new()
|
|
||||||
.route("/", post(create_project).get(list_projects))
|
|
||||||
.route(
|
|
||||||
"/{project_id}",
|
|
||||||
get(get_project).put(update_project).delete(delete_project),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct CreateProjectRequest {
|
|
||||||
title: String,
|
|
||||||
color: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_project(
|
|
||||||
State(pool): State<Pool<Sqlite>>,
|
|
||||||
WithRejection(Json(input), _): WithRejection<Json<CreateProjectRequest>, AppError>,
|
|
||||||
) -> Result<(StatusCode, Json<ProjectModel>), AppError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::CREATED,
|
|
||||||
Json(ProjectModel::insert(&pool, input.title, input.color).await?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_projects(
|
|
||||||
State(pool): State<Pool<Sqlite>>,
|
|
||||||
) -> Result<(StatusCode, Json<Vec<ProjectModel>>), AppError> {
|
|
||||||
Ok((StatusCode::OK, Json(ProjectModel::list(&pool).await?)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_project(
|
|
||||||
State(pool): State<Pool<Sqlite>>,
|
|
||||||
WithRejection(Path(project_id), _): WithRejection<Path<Uuid>, AppError>,
|
|
||||||
) -> Result<(StatusCode, Json<ProjectModel>), AppError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
Json(ProjectModel::get_by_id(&pool, project_id).await?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct UpdateProjectRequest {
|
|
||||||
title: Option<String>,
|
|
||||||
color: Option<String>,
|
|
||||||
status: Option<ProjectStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_project(
|
|
||||||
State(pool): State<Pool<Sqlite>>,
|
|
||||||
WithRejection(Path(project_id), _): WithRejection<Path<Uuid>, AppError>,
|
|
||||||
WithRejection(Json(input), _): WithRejection<Json<UpdateProjectRequest>, AppError>,
|
|
||||||
) -> Result<(StatusCode, Json<ProjectModel>), AppError> {
|
|
||||||
let mut project = ProjectModel::get_by_id(&pool, project_id).await?;
|
|
||||||
|
|
||||||
if let Some(new_title) = input.title {
|
|
||||||
if new_title.len() == 0 {
|
|
||||||
return Err(AppError::Unprocessable(
|
|
||||||
"Title must not be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
project.title = new_title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(color) = input.color {
|
|
||||||
if color.len() != 7 || color.chars().next() != Some('#') {
|
|
||||||
return Err(AppError::Unprocessable(
|
|
||||||
"Color is not a valid hex".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
project.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(status) = input.status {
|
|
||||||
project.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(project.update(&pool).await?)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_project(
|
|
||||||
State(pool): State<Pool<Sqlite>>,
|
|
||||||
WithRejection(Path(project_id), _): WithRejection<Path<Uuid>, AppError>,
|
|
||||||
) -> Result<StatusCode, AppError> {
|
|
||||||
ProjectModel::get_by_id(&pool, project_id).await?;
|
|
||||||
ProjectModel::delete(&pool, project_id).await?;
|
|
||||||
|
|
||||||
Ok(StatusCode::NO_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use axum::{Router, routing::get};
|
use axum::{Router, routing::get};
|
||||||
use axum_test::{TestServer, TestServerConfig, Transport};
|
use axum_test::{TestServer, TestServerConfig, Transport};
|
||||||
use hurl::report::html::{Testcase, write_report};
|
|
||||||
use hurl::runner::{self, RunnerOptions, Value, VariableSet};
|
use hurl::runner::{self, RunnerOptions, Value, VariableSet};
|
||||||
use hurl::util::logger::LoggerOptionsBuilder;
|
use hurl::util::logger::LoggerOptionsBuilder;
|
||||||
use hurl_core::input::Input;
|
|
||||||
use tower_http::trace::TraceLayer;
|
|
||||||
|
|
||||||
async fn create_app() -> Router {
|
async fn create_app() -> Router {
|
||||||
tracing_subscriber::fmt::try_init();
|
|
||||||
use backend::database::{DatabaseConfig, create_pool};
|
use backend::database::{DatabaseConfig, create_pool};
|
||||||
use backend::services;
|
use backend::services;
|
||||||
|
|
||||||
|
|
@ -30,8 +24,6 @@ async fn create_app() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
.nest("/api/tasks", services::create_task_router())
|
.nest("/api/tasks", services::create_task_router())
|
||||||
.nest("/api/projects", services::create_project_router())
|
|
||||||
.layer(TraceLayer::new_for_http())
|
|
||||||
.with_state(pool)
|
.with_state(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,14 +49,11 @@ async fn run_hurl_test(hurl_file_path: &str) {
|
||||||
let logger_opts = LoggerOptionsBuilder::new().build();
|
let logger_opts = LoggerOptionsBuilder::new().build();
|
||||||
let result = runner::run(&content, None, &runner_opts, &variables, &logger_opts).unwrap();
|
let result = runner::run(&content, None, &runner_opts, &variables, &logger_opts).unwrap();
|
||||||
|
|
||||||
let input = Input::new(hurl_file_path);
|
assert!(
|
||||||
let test_case = Testcase::from(&result, &input);
|
result.success,
|
||||||
test_case
|
"Hurl test failed for {}: {:?}",
|
||||||
.write_html(&content, &result.entries, Path::new("reports/store"), &[])
|
hurl_file_path, result
|
||||||
.expect("Failed to write html files");
|
);
|
||||||
write_report(Path::new("reports/"), &vec![test_case]).expect("Failed to write report");
|
|
||||||
|
|
||||||
assert!(result.success, "Hurl test failed for {}", hurl_file_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
|
@ -86,8 +75,3 @@ async fn test_update_tasks_api() {
|
||||||
async fn test_delete_tasks_api() {
|
async fn test_delete_tasks_api() {
|
||||||
run_hurl_test("./tests/api/delete_tasks.hurl").await;
|
run_hurl_test("./tests/api/delete_tasks.hurl").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_projects_api() {
|
|
||||||
run_hurl_test("./tests/api/projects.hurl").await;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,387 +0,0 @@
|
||||||
# Project API Tests
|
|
||||||
|
|
||||||
# Test: Create a new project with all fields (POST /api/projects)
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Test Project Alpha",
|
|
||||||
"color": "#1976d2"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 201
|
|
||||||
[Captures]
|
|
||||||
project_id: jsonpath "$.id"
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.title" == "Test Project Alpha"
|
|
||||||
jsonpath "$.color" == "#1976d2"
|
|
||||||
jsonpath "$.status" == "active"
|
|
||||||
jsonpath "$.folder_id" == null
|
|
||||||
jsonpath "$.sort_order" == 0
|
|
||||||
jsonpath "$.id" exists
|
|
||||||
jsonpath "$.created_at" exists
|
|
||||||
jsonpath "$.updated_at" exists
|
|
||||||
|
|
||||||
# Test: Create another new project
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Test Project Beta",
|
|
||||||
"color": "#ffffff"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 201
|
|
||||||
[Captures]
|
|
||||||
project_id_beta: jsonpath "$.id"
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.title" == "Test Project Beta"
|
|
||||||
jsonpath "$.color" == "#ffffff"
|
|
||||||
jsonpath "$.status" == "active"
|
|
||||||
jsonpath "$.folder_id" == null
|
|
||||||
jsonpath "$.sort_order" == 1000
|
|
||||||
jsonpath "$.id" exists
|
|
||||||
jsonpath "$.created_at" exists
|
|
||||||
jsonpath "$.updated_at" exists
|
|
||||||
|
|
||||||
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Missing Color"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 422
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
|
|
||||||
# Test: Create project with invalid data (missing title)
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"color": "#ffffff"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 422
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Create project with invalid data (invalid color)
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Invalid Color",
|
|
||||||
"color": "#ffffffasdf"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 422
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
|
|
||||||
# Test: Get a specific project by ID (GET /api/projects/{id})
|
|
||||||
GET {{host}}/api/projects/{{project_id}}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id}}"
|
|
||||||
jsonpath "$.title" == "Test Project Alpha"
|
|
||||||
jsonpath "$.color" == "#1976d2"
|
|
||||||
jsonpath "$.status" == "active"
|
|
||||||
jsonpath "$.folder_id" == null
|
|
||||||
jsonpath "$.created_at" exists
|
|
||||||
jsonpath "$.updated_at" exists
|
|
||||||
|
|
||||||
# Test: Get a minimal project by ID
|
|
||||||
GET {{host}}/api/projects/{{project_id_beta}}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id_beta}}"
|
|
||||||
jsonpath "$.title" == "Test Project Beta"
|
|
||||||
jsonpath "$.color" == "#ffffff"
|
|
||||||
jsonpath "$.status" == "active"
|
|
||||||
|
|
||||||
# Test: Get non-existent project
|
|
||||||
GET {{host}}/api/projects/00000000-0000-0000-0000-000000000000
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Get project with invalid UUID format
|
|
||||||
GET {{host}}/api/projects/invalid-uuid
|
|
||||||
|
|
||||||
HTTP 400
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: List all projects (GET /api/projects)
|
|
||||||
GET {{host}}/api/projects
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Captures]
|
|
||||||
initial_count: jsonpath "$" count
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$" isCollection
|
|
||||||
jsonpath "$[*].id" contains "{{project_id}}"
|
|
||||||
jsonpath "$[*].id" contains "{{project_id_beta}}"
|
|
||||||
jsonpath "$[*].title" contains "Test Project Alpha"
|
|
||||||
jsonpath "$[*].title" contains "Test Project Beta"
|
|
||||||
|
|
||||||
# Test: Verify all projects have required fields
|
|
||||||
GET {{host}}/api/projects
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$[?(@.id=='{{project_id}}')].title" exists
|
|
||||||
jsonpath "$[?(@.id=='{{project_id}}')].status" exists
|
|
||||||
jsonpath "$[?(@.id=='{{project_id}}')].created_at" exists
|
|
||||||
jsonpath "$[?(@.id=='{{project_id}}')].updated_at" exists
|
|
||||||
jsonpath "$[?(@.id=='{{project_id}}')].sort_order" exists
|
|
||||||
|
|
||||||
# Test: Update project title only (PUT /api/projects/{id})
|
|
||||||
PUT {{host}}/api/projects/{{project_id}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Updated Project Alpha"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Captures]
|
|
||||||
first_updated_at: jsonpath "$.updated_at"
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id}}"
|
|
||||||
jsonpath "$.title" == "Updated Project Alpha"
|
|
||||||
jsonpath "$.color" == "#1976d2"
|
|
||||||
jsonpath "$.status" == "active"
|
|
||||||
|
|
||||||
|
|
||||||
# Test: Update status to done
|
|
||||||
PUT {{host}}/api/projects/{{project_id}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"status": "done"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id}}"
|
|
||||||
jsonpath "$.status" == "done"
|
|
||||||
|
|
||||||
# Test: Update status to backlog
|
|
||||||
PUT {{host}}/api/projects/{{project_id}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"status": "backlog"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id}}"
|
|
||||||
jsonpath "$.status" == "backlog"
|
|
||||||
|
|
||||||
# Test: Update color
|
|
||||||
PUT {{host}}/api/projects/{{project_id}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"color": "#388e3c"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id}}"
|
|
||||||
jsonpath "$.color" == "#388e3c"
|
|
||||||
|
|
||||||
|
|
||||||
# Test: Update multiple fields together
|
|
||||||
PUT {{host}}/api/projects/{{project_id}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Completely Updated Project",
|
|
||||||
"color": "#d32f2f",
|
|
||||||
"status": "active"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.id" == "{{project_id}}"
|
|
||||||
jsonpath "$.title" == "Completely Updated Project"
|
|
||||||
jsonpath "$.color" == "#d32f2f"
|
|
||||||
jsonpath "$.status" == "active"
|
|
||||||
|
|
||||||
|
|
||||||
# Test: Update non-existent project
|
|
||||||
PUT {{host}}/api/projects/00000000-0000-0000-0000-000000000000
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "This should fail"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Update with invalid UUID format
|
|
||||||
PUT {{host}}/api/projects/invalid-uuid-format
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "This should also fail"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 400
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Update with empty title
|
|
||||||
PUT {{host}}/api/projects/{{project_id_beta}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 422
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Update with invalid status
|
|
||||||
PUT {{host}}/api/projects/{{project_id_beta}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"status": "invalid_status"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 422
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Update with invalid color format
|
|
||||||
PUT {{host}}/api/projects/{{project_id_beta}}
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"color": "invalid-color"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 422
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Setup: Create a project to delete
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Project to Delete",
|
|
||||||
"color": "#222222"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 201
|
|
||||||
[Captures]
|
|
||||||
delete_project_id: jsonpath "$.id"
|
|
||||||
|
|
||||||
# Test: Delete project successfully (DELETE /api/projects/{id})
|
|
||||||
DELETE {{host}}/api/projects/{{delete_project_id}}
|
|
||||||
|
|
||||||
HTTP 204
|
|
||||||
|
|
||||||
# Test: Verify project is deleted (should return 404)
|
|
||||||
GET {{host}}/api/projects/{{delete_project_id}}
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Setup: Create another project for additional delete tests
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Another Project to Delete",
|
|
||||||
"color": "#333333"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 201
|
|
||||||
[Captures]
|
|
||||||
another_delete_project_id: jsonpath "$.id"
|
|
||||||
|
|
||||||
# Test: Verify project exists before deletion
|
|
||||||
GET {{host}}/api/projects/{{another_delete_project_id}}
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.title" == "Another Project to Delete"
|
|
||||||
|
|
||||||
# Test: Delete the project
|
|
||||||
DELETE {{host}}/api/projects/{{another_delete_project_id}}
|
|
||||||
|
|
||||||
HTTP 204
|
|
||||||
|
|
||||||
# Test: Confirm project no longer exists
|
|
||||||
GET {{host}}/api/projects/{{another_delete_project_id}}
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Delete non-existent project
|
|
||||||
DELETE {{host}}/api/projects/00000000-0000-0000-0000-000000000000
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Delete with invalid UUID format
|
|
||||||
DELETE {{host}}/api/projects/invalid-uuid-format
|
|
||||||
|
|
||||||
HTTP 400
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: Multiple deletions of same project (idempotency test)
|
|
||||||
POST {{host}}/api/projects
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"title": "Project for Idempotency Test",
|
|
||||||
"color": "#234567"
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP 201
|
|
||||||
[Captures]
|
|
||||||
idempotent_project_id: jsonpath "$.id"
|
|
||||||
|
|
||||||
# First deletion should succeed
|
|
||||||
DELETE {{host}}/api/projects/{{idempotent_project_id}}
|
|
||||||
|
|
||||||
HTTP 204
|
|
||||||
|
|
||||||
# Second deletion should return 404 (project already gone)
|
|
||||||
DELETE {{host}}/api/projects/{{idempotent_project_id}}
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$.error" exists
|
|
||||||
|
|
||||||
# Test: List shows remaining projects after deletions
|
|
||||||
GET {{host}}/api/projects
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$[*].id" contains "{{project_id}}"
|
|
||||||
jsonpath "$[*].id" contains "{{project_id_beta}}"
|
|
||||||
jsonpath "$[*].id" not contains "{{delete_project_id}}"
|
|
||||||
jsonpath "$[*].id" not contains "{{another_delete_project_id}}"
|
|
||||||
jsonpath "$[*].id" not contains "{{idempotent_project_id}}"
|
|
||||||
|
|
||||||
# Cleanup: Delete remaining test projects
|
|
||||||
DELETE {{host}}/api/projects/{{project_id}}
|
|
||||||
|
|
||||||
HTTP 204
|
|
||||||
|
|
||||||
DELETE {{host}}/api/projects/{{project_id_beta}}
|
|
||||||
|
|
||||||
HTTP 204
|
|
||||||
|
|
||||||
# Test: Verify all test projects are cleaned up
|
|
||||||
GET {{host}}/api/projects
|
|
||||||
|
|
||||||
HTTP 200
|
|
||||||
[Asserts]
|
|
||||||
jsonpath "$[*].id" not contains "{{project_id}}"
|
|
||||||
jsonpath "$[*].id" not contains "{{project_id_beta}}"
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
# Project-Based Organization Implementation Plan
|
|
||||||
|
|
||||||
## Phase Overview
|
|
||||||
This plan implements project-based organization with a clean folder/project hierarchy:
|
|
||||||
- **Folders**: Organizational containers for projects (can be reordered)
|
|
||||||
- **Projects**: Work containers that hold tasks (can belong to folders or exist at root)
|
|
||||||
- **Tasks**: Belong to zero or one project (never directly to folders)
|
|
||||||
|
|
||||||
## Backend Implementation
|
|
||||||
|
|
||||||
### 1. Database Schema Changes
|
|
||||||
- **New Tables:**
|
|
||||||
- `folders` table: id, name, color, sort_order, created_at, updated_at
|
|
||||||
- `projects` table: id, name, description, color, due_date, status, folder_id (nullable), sort_order, created_at, updated_at
|
|
||||||
- **Schema Updates:**
|
|
||||||
- Add `project_id` field to existing `tasks` table (nullable foreign key)
|
|
||||||
- Database indexes for efficient querying
|
|
||||||
|
|
||||||
### 2. Backend Models & Services
|
|
||||||
- **FolderModel** (backend/src/models/folder.rs):
|
|
||||||
- CRUD operations for folders
|
|
||||||
- Reordering methods for sort_order management
|
|
||||||
- **ProjectModel** (backend/src/models/project.rs):
|
|
||||||
- CRUD operations with folder relationship
|
|
||||||
- Methods for reordering within folders and at root level
|
|
||||||
- Query methods for folder contents
|
|
||||||
- **Updated TaskModel** (backend/src/models/task.rs):
|
|
||||||
- Add project_id field and foreign key constraint
|
|
||||||
- Project relationship methods
|
|
||||||
- **API Services:**
|
|
||||||
- folders.rs - Folder CRUD and reordering endpoints
|
|
||||||
- projects.rs - Project CRUD, folder assignment, reordering
|
|
||||||
- Updated tasks.rs - Project filtering and assignment
|
|
||||||
|
|
||||||
### 3. Database Migrations
|
|
||||||
- Create folders table
|
|
||||||
- Create projects table with folder foreign key
|
|
||||||
- Add project_id to tasks table
|
|
||||||
- Set up proper indexes and constraints
|
|
||||||
|
|
||||||
## Frontend Implementation
|
|
||||||
|
|
||||||
### 1. Data Models & Types
|
|
||||||
- **Folder Types** (frontend/app/types/folder.ts):
|
|
||||||
- Folder interface, CreateFolderRequest, UpdateFolderRequest
|
|
||||||
- **Project Types** (frontend/app/types/project.ts):
|
|
||||||
- Project interface with folder_id field
|
|
||||||
- ProjectStatus enum, CRUD request types
|
|
||||||
- **Updated Task Types** (frontend/app/types/task.ts):
|
|
||||||
- Add project_id field to Task interface
|
|
||||||
|
|
||||||
### 2. Sidebar & Navigation
|
|
||||||
- **ProjectSidebar** (frontend/app/components/ProjectSidebar.tsx):
|
|
||||||
- Hierarchical display: Folders → Projects → (Root Projects)
|
|
||||||
- Drag-and-drop for both folder and project reordering
|
|
||||||
- Inline creation forms for folders and projects
|
|
||||||
- Collapsible folder sections
|
|
||||||
- **FolderSection** (frontend/app/components/FolderSection.tsx):
|
|
||||||
- Individual folder with contained projects
|
|
||||||
- Folder editing and project management
|
|
||||||
|
|
||||||
### 3. Project Management
|
|
||||||
- **ProjectForm** (frontend/app/components/ProjectForm.tsx):
|
|
||||||
- Create/edit projects with folder assignment
|
|
||||||
- 16-color palette, due dates, status selection
|
|
||||||
- **FolderForm** (frontend/app/components/FolderForm.tsx):
|
|
||||||
- Simple folder creation/editing (name, color)
|
|
||||||
|
|
||||||
### 4. Updated Task Components
|
|
||||||
- **Enhanced TaskList** with Project column
|
|
||||||
- **Updated TaskForm** with project selection dropdown
|
|
||||||
- Project filtering throughout the UI
|
|
||||||
|
|
||||||
### 5. Routes & State Management
|
|
||||||
- **New Routes:**
|
|
||||||
- `/projects` - Projects overview
|
|
||||||
- `/projects/{id}` - Project detail page
|
|
||||||
- `/folders/{id}` - Folder contents view
|
|
||||||
- **State Hooks:**
|
|
||||||
- `useFolders.ts`, `useProjects.ts`, updated `useTasks.ts`
|
|
||||||
- **API Integration:**
|
|
||||||
- Folder and project API clients
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
### Phase 1: Backend Foundation
|
|
||||||
1. Create folders and projects tables (migrations)
|
|
||||||
2. Implement FolderModel and ProjectModel
|
|
||||||
3. Add project_id to TaskModel
|
|
||||||
4. Create API endpoints for folders and projects
|
|
||||||
5. Unit tests for new functionality
|
|
||||||
|
|
||||||
### Phase 2: Frontend Core
|
|
||||||
1. Implement folder and project types
|
|
||||||
2. Create basic ProjectSidebar (no drag-drop yet)
|
|
||||||
3. Add project column to TaskList
|
|
||||||
4. Update TaskForm with project selection
|
|
||||||
5. Basic project/folder CRUD forms
|
|
||||||
|
|
||||||
### Phase 3: Advanced Features
|
|
||||||
1. Drag-and-drop reordering in sidebar
|
|
||||||
2. Project and folder detail pages
|
|
||||||
3. Hierarchical project creation flows
|
|
||||||
4. Project filtering and search
|
|
||||||
5. Mobile responsive sidebar
|
|
||||||
|
|
||||||
### Phase 4: Polish & Integration
|
|
||||||
1. Project color theming throughout UI
|
|
||||||
2. Performance optimization
|
|
||||||
3. Enhanced UX for project management
|
|
||||||
4. Integration testing
|
|
||||||
5. Migration tools for existing data
|
|
||||||
|
|
||||||
## Data Relationships
|
|
||||||
```
|
|
||||||
folders (1) → (many) projects
|
|
||||||
projects (1) → (many) tasks
|
|
||||||
folders (no direct relationship) tasks
|
|
||||||
```
|
|
||||||
|
|
||||||
This clean separation ensures tasks only belong to projects, while folders serve purely as organizational containers for projects.
|
|
||||||
|
|
||||||
## Technical Implementation Details
|
|
||||||
|
|
||||||
### Database Schema
|
|
||||||
|
|
||||||
#### Folders Table
|
|
||||||
```sql
|
|
||||||
CREATE TABLE folders (
|
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Projects Table
|
|
||||||
```sql
|
|
||||||
CREATE TABLE projects (
|
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
color VARCHAR(7), -- hex color code
|
|
||||||
due_date DATE,
|
|
||||||
status TEXT CHECK(status IN ('active', 'done', 'backlog')) DEFAULT 'active',
|
|
||||||
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Tasks Table Update
|
|
||||||
```sql
|
|
||||||
ALTER TABLE tasks ADD COLUMN project_id UUID REFERENCES projects(id) ON DELETE SET NULL;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Color Palette
|
|
||||||
16-color palette for folders and projects:
|
|
||||||
- Primary Blues: #1976d2, #0288d1, #0097a7
|
|
||||||
- Greens: #388e3c, #689f38, #7cb342
|
|
||||||
- Oranges/Reds: #f57c00, #ff5722, #d32f2f
|
|
||||||
- Purples: #7b1fa2, #512da8, #303f9f
|
|
||||||
- Neutrals: #455a64, #616161, #757575, #424242
|
|
||||||
|
|
||||||
### API Endpoints
|
|
||||||
|
|
||||||
#### Folders
|
|
||||||
- `GET /api/folders` - List all folders with projects
|
|
||||||
- `POST /api/folders` - Create folder
|
|
||||||
- `PUT /api/folders/{id}` - Update folder
|
|
||||||
- `DELETE /api/folders/{id}` - Delete folder
|
|
||||||
- `PUT /api/folders/reorder` - Reorder folders
|
|
||||||
|
|
||||||
#### Projects
|
|
||||||
- `GET /api/projects` - List all projects
|
|
||||||
- `GET /api/projects?folder_id={id}` - List projects in folder
|
|
||||||
- `POST /api/projects` - Create project
|
|
||||||
- `PUT /api/projects/{id}` - Update project
|
|
||||||
- `DELETE /api/projects/{id}` - Delete project
|
|
||||||
- `PUT /api/projects/reorder` - Reorder projects within folder/root
|
|
||||||
|
|
||||||
#### Tasks (Updated)
|
|
||||||
- `GET /api/tasks?project_id={id}` - List tasks in project
|
|
||||||
- Task CRUD operations include project_id field
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
1. Existing tasks remain unassigned (project_id = NULL)
|
|
||||||
2. Users can create folders and projects
|
|
||||||
3. Users can assign existing tasks to projects
|
|
||||||
4. No data loss during migration
|
|
||||||
5. Backwards compatibility maintained
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
- Unit tests for all model methods
|
|
||||||
- API integration tests with Hurl
|
|
||||||
- Frontend component tests
|
|
||||||
- End-to-end project workflow tests
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue