Add v0.1.3 backend features: metadata fields and search

Backend Changes:
- Add estimated_minutes field to tasks (stored as integer minutes)
- Add tags field (JSON array) for categorizing tasks
- Add flag_color field for visual priority indicators
- Add search endpoint (/api/search) with project filtering
- Update JSON import to handle new metadata fields

Frontend Changes:
- Display version v0.1.3 in header
- Add search API client function
- Add format utility for time display (30m, 1.5h, etc.)

Example Data:
- Update example-import.json with time estimates, tags, and flags
- Demonstrate nested metadata inheritance

Note: Frontend UI for displaying/editing these fields in progress
This commit is contained in:
Claude
2025-11-20 04:54:01 +00:00
parent f3fc87e715
commit 5d43dc6fd1
7 changed files with 156 additions and 20 deletions

View File

@@ -144,6 +144,47 @@ def delete_task(task_id: int, db: Session = Depends(get_db)):
return None
# ========== SEARCH ENDPOINT ==========
@app.get("/api/search", response_model=List[schemas.Task])
def search_tasks(
query: str,
project_ids: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
Search tasks across projects by title, description, and tags.
Args:
query: Search term to match against title, description, and tags
project_ids: Comma-separated list of project IDs to search in (optional, searches all if not provided)
"""
# Parse project IDs if provided
project_id_list = None
if project_ids:
try:
project_id_list = [int(pid.strip()) for pid in project_ids.split(',') if pid.strip()]
except ValueError:
raise HTTPException(status_code=400, detail="Invalid project_ids format")
# Build query
tasks_query = db.query(models.Task)
# Filter by project IDs if specified
if project_id_list:
tasks_query = tasks_query.filter(models.Task.project_id.in_(project_id_list))
# Search in title, description, and tags
search_term = f"%{query}%"
tasks = tasks_query.filter(
(models.Task.title.ilike(search_term)) |
(models.Task.description.ilike(search_term)) |
(models.Task.tags.contains([query])) # Exact tag match
).all()
return tasks
# ========== JSON IMPORT ENDPOINT ==========
def _import_tasks_recursive(
@@ -161,6 +202,9 @@ def _import_tasks_recursive(
title=task_data.title,
description=task_data.description,
status=task_data.status,
estimated_minutes=task_data.estimated_minutes,
tags=task_data.tags,
flag_color=task_data.flag_color,
sort_order=idx
)
db_task = crud.create_task(db, task)

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, JSON
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
@@ -34,6 +34,9 @@ class Task(Base):
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.BACKLOG, nullable=False)
sort_order = Column(Integer, default=0)
estimated_minutes = Column(Integer, nullable=True)
tags = Column(JSON, nullable=True)
flag_color = Column(String(50), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -11,6 +11,9 @@ class TaskBase(BaseModel):
status: TaskStatus = TaskStatus.BACKLOG
parent_task_id: Optional[int] = None
sort_order: int = 0
estimated_minutes: Optional[int] = None
tags: Optional[List[str]] = None
flag_color: Optional[str] = None
class TaskCreate(TaskBase):
@@ -23,6 +26,9 @@ class TaskUpdate(BaseModel):
status: Optional[TaskStatus] = None
parent_task_id: Optional[int] = None
sort_order: Optional[int] = None
estimated_minutes: Optional[int] = None
tags: Optional[List[str]] = None
flag_color: Optional[str] = None
class Task(TaskBase):
@@ -74,6 +80,9 @@ class ImportSubtask(BaseModel):
title: str
description: Optional[str] = None
status: TaskStatus = TaskStatus.BACKLOG
estimated_minutes: Optional[int] = None
tags: Optional[List[str]] = None
flag_color: Optional[str] = None
subtasks: List['ImportSubtask'] = []