feat: implement collaboration module

- Backend (FastAPI):
  - Task comments with nested replies and soft delete
  - @mention parsing with 10-mention limit per comment
  - Notification system with read/unread tracking
  - Blocker management with project owner notification
  - WebSocket endpoint with JWT auth and keepalive
  - User search API for @mention autocomplete
  - Alembic migration for 4 new tables

- Frontend (React + Vite):
  - Comments component with @mention autocomplete
  - NotificationBell with real-time WebSocket updates
  - BlockerDialog for task blocking workflow
  - NotificationContext for state management

- OpenSpec:
  - 4 requirements with scenarios defined
  - add-collaboration change archived

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-12-29 20:45:07 +08:00
parent 61fe01cb6b
commit 3470428411
38 changed files with 3088 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import List
from app.core.database import get_db
@@ -16,6 +17,33 @@ from app.middleware.auth import (
router = APIRouter()
@router.get("/search", response_model=List[UserResponse])
async def search_users(
q: str = Query(..., min_length=1, max_length=100, description="Search query"),
limit: int = Query(10, ge=1, le=50, description="Max results"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Search users by name or email. Used for @mention autocomplete.
Returns users matching the query, limited to same department unless system admin.
"""
query = db.query(User).filter(
User.is_active == True,
or_(
User.name.ilike(f"%{q}%"),
User.email.ilike(f"%{q}%"),
)
)
# Filter by department if not system admin
if not current_user.is_system_admin and current_user.department_id:
query = query.filter(User.department_id == current_user.department_id)
users = query.limit(limit).all()
return users
@router.get("", response_model=List[UserResponse])
async def list_users(
skip: int = 0,