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,11 +1,13 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import api from '../services/api'
import { Comments } from './Comments'
import { TaskAttachments } from './TaskAttachments'
import { SubtaskList } from './SubtaskList'
import { UserSelect } from './UserSelect'
import { UserSearchResult } from '../services/collaboration'
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
import { CustomFieldInput } from './CustomFieldInput'
import { SkeletonList } from './Skeleton'
interface Task {
id: string
@@ -37,6 +39,7 @@ interface TaskDetailModalProps {
isOpen: boolean
onClose: () => void
onUpdate: () => void
onSubtaskClick?: (subtaskId: string) => void
}
export function TaskDetailModal({
@@ -45,6 +48,7 @@ export function TaskDetailModal({
isOpen,
onClose,
onUpdate,
onSubtaskClick,
}: TaskDetailModalProps) {
const [isEditing, setIsEditing] = useState(false)
const [saving, setSaving] = useState(false)
@@ -69,14 +73,7 @@ export function TaskDetailModal({
const [editCustomValues, setEditCustomValues] = useState<Record<string, string | number | null>>({})
const [loadingCustomFields, setLoadingCustomFields] = useState(false)
// Load custom fields for the project
useEffect(() => {
if (task.project_id) {
loadCustomFields()
}
}, [task.project_id])
const loadCustomFields = async () => {
const loadCustomFields = useCallback(async () => {
setLoadingCustomFields(true)
try {
const response = await customFieldsApi.getCustomFields(task.project_id)
@@ -86,7 +83,14 @@ export function TaskDetailModal({
} finally {
setLoadingCustomFields(false)
}
}
}, [task.project_id])
// Load custom fields for the project
useEffect(() => {
if (task.project_id) {
loadCustomFields()
}
}, [task.project_id, loadCustomFields])
// Initialize custom values from task
useEffect(() => {
@@ -120,6 +124,28 @@ export function TaskDetailModal({
setIsEditing(false)
}, [task])
// Reference to the modal overlay for focus management
const overlayRef = useRef<HTMLDivElement>(null)
// Handle Escape key to close modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
// Focus the overlay for keyboard accessibility
overlayRef.current?.focus()
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [isOpen, onClose])
if (!isOpen) return null
const handleSave = async () => {
@@ -204,7 +230,15 @@ export function TaskDetailModal({
}
return (
<div style={styles.overlay} onClick={handleOverlayClick}>
<div
ref={overlayRef}
style={styles.overlay}
onClick={handleOverlayClick}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby="task-detail-title"
>
<div style={styles.modal}>
<div style={styles.header}>
<div style={styles.headerLeft}>
@@ -223,7 +257,7 @@ export function TaskDetailModal({
autoFocus
/>
) : (
<h2 style={styles.title}>{task.title}</h2>
<h2 id="task-detail-title" style={styles.title}>{task.title}</h2>
)}
</div>
<div style={styles.headerActions}>
@@ -285,6 +319,16 @@ export function TaskDetailModal({
<div style={styles.section}>
<TaskAttachments taskId={task.id} />
</div>
{/* Subtasks Section */}
<div style={styles.section}>
<SubtaskList
taskId={task.id}
projectId={task.project_id}
onSubtaskClick={onSubtaskClick}
onSubtaskCreated={onUpdate}
/>
</div>
</div>
<div style={styles.sidebar}>
@@ -420,7 +464,7 @@ export function TaskDetailModal({
<div style={styles.customFieldsDivider} />
<div style={styles.customFieldsHeader}>Custom Fields</div>
{loadingCustomFields ? (
<div style={styles.loadingText}>Loading...</div>
<SkeletonList count={3} showAvatar={false} />
) : (
customFields.map((field) => {
// Get the value for this field