Files
PROJECT-CONTORL/frontend/src/components/ConfirmModal.tsx
beabigegg 4b5a9c1d0a 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>
2026-01-07 21:24:36 +08:00

156 lines
3.4 KiB
TypeScript

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