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

@@ -10,6 +10,7 @@ import { UserSearchResult } from '../services/collaboration'
import { useProjectSync, TaskEvent } from '../contexts/ProjectSyncContext'
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
import { CustomFieldInput } from '../components/CustomFieldInput'
import { SkeletonTable, SkeletonKanban, Skeleton } from '../components/Skeleton'
interface Task {
id: string
@@ -98,6 +99,7 @@ export default function Tasks() {
})
const [showColumnMenu, setShowColumnMenu] = useState(false)
const columnMenuRef = useRef<HTMLDivElement>(null)
const createModalOverlayRef = useRef<HTMLDivElement>(null)
useEffect(() => {
loadData()
@@ -250,6 +252,25 @@ export default function Tasks() {
}
}, [showColumnMenu])
// Handle Escape key to close create modal - document-level listener
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && showCreateModal) {
setShowCreateModal(false)
}
}
if (showCreateModal) {
document.addEventListener('keydown', handleKeyDown)
// Focus the overlay for accessibility
createModalOverlayRef.current?.focus()
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [showCreateModal])
const loadData = async () => {
try {
const [projectRes, tasksRes, statusesRes] = await Promise.all([
@@ -386,6 +407,24 @@ export default function Tasks() {
loadData()
}
const handleSubtaskClick = async (subtaskId: string) => {
try {
const response = await api.get(`/tasks/${subtaskId}`)
const subtask = response.data
// Ensure subtask has project_id for custom fields loading
const subtaskWithProject = {
...subtask,
project_id: projectId!,
// Map API response fields to frontend Task interface
time_estimate: subtask.original_estimate,
}
setSelectedTask(subtaskWithProject)
// Modal is already open, just update the task
} catch (err) {
console.error('Failed to load subtask:', err)
}
}
const getPriorityStyle = (priority: string): React.CSSProperties => {
const colors: { [key: string]: string } = {
low: '#808080',
@@ -401,7 +440,18 @@ export default function Tasks() {
}
if (loading) {
return <div style={styles.loading}>Loading...</div>
return (
<div style={styles.container}>
<div style={styles.header}>
<Skeleton variant="text" width={200} height={32} />
<div style={{ display: 'flex', gap: '8px' }}>
<Skeleton variant="rect" width={100} height={36} />
<Skeleton variant="rect" width={100} height={36} />
</div>
</div>
{viewMode === 'kanban' ? <SkeletonKanban columns={4} /> : <SkeletonTable rows={8} columns={5} />}
</div>
)
}
return (
@@ -623,17 +673,33 @@ export default function Tasks() {
{/* Create Task Modal */}
{showCreateModal && (
<div style={styles.modalOverlay}>
<div
ref={createModalOverlayRef}
style={styles.modalOverlay}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby="create-task-title"
>
<div style={styles.modal}>
<h2 style={styles.modalTitle}>Create New Task</h2>
<h2 id="create-task-title" style={styles.modalTitle}>Create New Task</h2>
<label htmlFor="task-title" style={styles.visuallyHidden}>
Task title
</label>
<input
id="task-title"
type="text"
placeholder="Task title"
value={newTask.title}
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
style={styles.input}
autoFocus
/>
<label htmlFor="task-description" style={styles.visuallyHidden}>
Description
</label>
<textarea
id="task-description"
placeholder="Description (optional)"
value={newTask.description}
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
@@ -734,6 +800,7 @@ export default function Tasks() {
isOpen={showDetailModal}
onClose={handleCloseDetailModal}
onUpdate={handleTaskUpdate}
onSubtaskClick={handleSubtaskClick}
/>
)}
</div>
@@ -879,7 +946,7 @@ const styles: { [key: string]: React.CSSProperties } = {
color: '#0066cc',
},
subtaskCount: {
color: '#999',
color: '#767676', // WCAG AA compliant
},
statusSelect: {
padding: '6px 12px',
@@ -1069,4 +1136,15 @@ const styles: { [key: string]: React.CSSProperties } = {
color: '#888',
fontSize: '13px',
},
visuallyHidden: {
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0,
},
}