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>
156 lines
3.4 KiB
TypeScript
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
|