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:
155
frontend/src/components/ConfirmModal.tsx
Normal file
155
frontend/src/components/ConfirmModal.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
interface ConfirmModalProps {
|
||||
isOpen: boolean
|
||||
title: string
|
||||
message: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmStyle?: 'danger' | 'primary'
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function ConfirmModal({
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
confirmStyle = 'danger',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmModalProps) {
|
||||
const modalOverlayRef = useRef<HTMLDivElement>(null)
|
||||
const confirmButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
// A11Y: Handle Escape key to close modal
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
// Focus confirm button when modal opens
|
||||
confirmButtonRef.current?.focus()
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [isOpen, onCancel])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={modalOverlayRef}
|
||||
style={styles.overlay}
|
||||
onClick={handleOverlayClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-modal-title"
|
||||
aria-describedby="confirm-modal-message"
|
||||
>
|
||||
<div style={styles.modal}>
|
||||
<h2 id="confirm-modal-title" style={styles.title}>
|
||||
{title}
|
||||
</h2>
|
||||
<p id="confirm-modal-message" style={styles.message}>
|
||||
{message}
|
||||
</p>
|
||||
<div style={styles.actions}>
|
||||
<button onClick={onCancel} style={styles.cancelButton}>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
ref={confirmButtonRef}
|
||||
onClick={onConfirm}
|
||||
style={{
|
||||
...styles.confirmButton,
|
||||
...(confirmStyle === 'danger' ? styles.dangerButton : styles.primaryButton),
|
||||
}}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
overlay: {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1200,
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
width: '400px',
|
||||
maxWidth: '90%',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
title: {
|
||||
margin: '0 0 12px 0',
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
color: '#212529',
|
||||
},
|
||||
message: {
|
||||
margin: '0 0 24px 0',
|
||||
fontSize: '14px',
|
||||
color: '#495057',
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '12px',
|
||||
},
|
||||
cancelButton: {
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#495057',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
confirmButton: {
|
||||
padding: '10px 20px',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
dangerButton: {
|
||||
backgroundColor: '#dc3545',
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: '#0066cc',
|
||||
},
|
||||
}
|
||||
|
||||
export default ConfirmModal
|
||||
Reference in New Issue
Block a user