feat: complete LOW priority code quality improvements

Backend:
- LOW-002: Add Query validation with max page size limits (100)
- LOW-003: Replace magic strings with TaskStatus.is_done flag
- LOW-004: Add 'creation' trigger type validation
- Add action_executor.py with UpdateFieldAction and AutoAssignAction

Frontend:
- LOW-005: Replace TypeScript 'any' with 'unknown' + type guards
- LOW-006: Add ConfirmModal component with A11Y support
- LOW-007: Add ToastContext for user feedback notifications
- LOW-009: Add Skeleton components (17 loading states replaced)
- LOW-010: Setup Vitest with 21 tests for ConfirmModal and Skeleton

Components updated:
- App.tsx, ProtectedRoute.tsx, Spaces.tsx, Projects.tsx, Tasks.tsx
- ProjectSettings.tsx, AuditPage.tsx, WorkloadPage.tsx, ProjectHealthPage.tsx
- Comments.tsx, AttachmentList.tsx, TriggerList.tsx, TaskDetailModal.tsx
- NotificationBell.tsx, BlockerDialog.tsx, CalendarView.tsx, WorkloadUserDetail.tsx

🤖 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
2026-01-07 21:24:36 +08:00
parent 2d80a8384e
commit 4b5a9c1d0a
66 changed files with 7809 additions and 171 deletions

View File

@@ -1,5 +1,5 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Optional
@@ -11,6 +11,8 @@ from app.schemas.trigger import (
)
from app.middleware.auth import get_current_user, check_project_access, check_project_edit_access
from app.services.trigger_scheduler import TriggerSchedulerService
from app.services.trigger_service import TriggerService
from app.services.action_executor import ActionValidationError
router = APIRouter(tags=["triggers"])
@@ -60,10 +62,11 @@ async def create_trigger(
)
# Validate trigger type
if trigger_data.trigger_type not in ["field_change", "schedule"]:
valid_trigger_types = ["field_change", "schedule", "creation"]
if trigger_data.trigger_type not in valid_trigger_types:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid trigger type. Must be 'field_change' or 'schedule'",
detail=f"Invalid trigger type. Must be one of: {', '.join(valid_trigger_types)}",
)
# Validate conditions based on trigger type
@@ -111,6 +114,16 @@ async def create_trigger(
detail=error_msg or "Invalid cron expression",
)
# Validate actions configuration (FEAT-014, FEAT-015)
try:
actions_dicts = [a.model_dump(exclude_none=True) for a in trigger_data.actions]
TriggerService.validate_actions(actions_dicts, db)
except ActionValidationError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
# Create trigger
trigger = Trigger(
id=str(uuid.uuid4()),
@@ -119,7 +132,7 @@ async def create_trigger(
description=trigger_data.description,
trigger_type=trigger_data.trigger_type,
conditions=trigger_data.conditions.model_dump(),
actions=[a.model_dump() for a in trigger_data.actions],
actions=[a.model_dump(exclude_none=True) for a in trigger_data.actions],
is_active=trigger_data.is_active,
created_by=current_user.id,
)
@@ -239,7 +252,16 @@ async def update_trigger(
)
trigger.conditions = trigger_data.conditions.model_dump(exclude_none=True)
if trigger_data.actions is not None:
trigger.actions = [a.model_dump() for a in trigger_data.actions]
# Validate actions configuration (FEAT-014, FEAT-015)
try:
actions_dicts = [a.model_dump(exclude_none=True) for a in trigger_data.actions]
TriggerService.validate_actions(actions_dicts, db)
except ActionValidationError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
trigger.actions = [a.model_dump(exclude_none=True) for a in trigger_data.actions]
if trigger_data.is_active is not None:
trigger.is_active = trigger_data.is_active
@@ -278,8 +300,8 @@ async def delete_trigger(
@router.get("/api/triggers/{trigger_id}/logs", response_model=TriggerLogListResponse)
async def list_trigger_logs(
trigger_id: str,
limit: int = 50,
offset: int = 0,
limit: int = Query(50, ge=1, le=200, description="Number of logs to return"),
offset: int = Query(0, ge=0, description="Number of logs to skip"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):