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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'] = []
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user