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:
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user