diff --git a/web/eslint.config.js b/web/eslint.config.js index 113c262d8..2650ee116 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -26,6 +26,11 @@ export default tseslint.config( '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], // Warn on patterns that often indicate unbatched state updates (#3049) // Encourages useReducer or single-object setState for related state + 'no-restricted-globals': ['error', + { name: 'alert', message: 'Use ConfirmDialog or Toast instead of browser alert().' }, + { name: 'confirm', message: 'Use ConfirmDialog instead of browser confirm().' }, + { name: 'prompt', message: 'Use a styled input modal instead of browser prompt().' }, + ], 'no-restricted-syntax': ['warn', { selector: 'CallExpression[callee.name=/^set[A-Z]/] + CallExpression[callee.name=/^set[A-Z]/]', diff --git a/web/src/components/alerts/AlertRuleEditor.tsx b/web/src/components/alerts/AlertRuleEditor.tsx index 68df99d6d..139a06991 100644 --- a/web/src/components/alerts/AlertRuleEditor.tsx +++ b/web/src/components/alerts/AlertRuleEditor.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Trash2, Server, Bell, BellOff, Bot, Slack, Webhook, Siren, ShieldAlert } from 'lucide-react' import { useClusters } from '../../hooks/useMCP' -import { BaseModal } from '../../lib/modals' +import { BaseModal, ConfirmDialog } from '../../lib/modals' import type { AlertRule, AlertCondition, @@ -80,10 +80,17 @@ export function AlertRuleEditor({ isOpen = true, rule, onSave, onCancel }: Alert // Validation const [errors, setErrors] = useState>({}) + const [showDiscardConfirm, setShowDiscardConfirm] = useState(false) + + const forceClose = () => { + setShowDiscardConfirm(false) + onCancel() + } const handleClose = () => { const hasChanges = name.trim() !== '' || description.trim() !== '' - if (hasChanges && !window.confirm(t('common:common.discardUnsavedChanges', 'Discard unsaved changes?'))) { + if (hasChanges) { + setShowDiscardConfirm(true) return } onCancel() @@ -176,6 +183,16 @@ export function AlertRuleEditor({ isOpen = true, rule, onSave, onCancel }: Alert return ( + setShowDiscardConfirm(false)} + onConfirm={forceClose} + title={t('common:common.discardUnsavedChanges', 'Discard unsaved changes?')} + message={t('common:common.discardUnsavedChangesMessage', 'You have unsaved changes that will be lost.')} + confirmLabel={t('common:common.discard', 'Discard')} + cancelLabel={t('common:common.keepEditing', 'Keep editing')} + variant="warning" + /> (null) const [success, setSuccess] = useState<{ issueUrl?: string } | null>(null) const [confirmClose, setConfirmClose] = useState(null) // request ID to confirm close + const [showDiscardConfirm, setShowDiscardConfirm] = useState(false) const [actionLoading, setActionLoading] = useState(null) // request ID being acted on const [actionError, setActionError] = useState(null) const [showLoginPrompt, setShowLoginPrompt] = useState(false) @@ -370,25 +371,41 @@ export function FeatureRequestModal({ isOpen, onClose, initialTab, initialReques } } + const forceClose = () => { + setShowDiscardConfirm(false) + setDescription('') + setDescriptionTab('write') + setRequestType(initialRequestType || 'bug') + setTargetRepo('console') + setError(null) + setSuccess(null) + setScreenshots([]) + setActiveTab(initialTab || 'submit') + onClose() + } + const handleClose = () => { if (!isSubmitting) { - if (description.trim() !== '' && !window.confirm(t('common:common.discardUnsavedChanges', 'Discard unsaved changes?'))) { + if (description.trim() !== '') { + setShowDiscardConfirm(true) return } - setDescription('') - setDescriptionTab('write') - setRequestType(initialRequestType || 'bug') - setTargetRepo('console') - setError(null) - setSuccess(null) - setScreenshots([]) - setActiveTab(initialTab || 'submit') - onClose() + forceClose() } } return ( + setShowDiscardConfirm(false)} + onConfirm={forceClose} + title={t('common:common.discardUnsavedChanges', 'Discard unsaved changes?')} + message={t('common:common.discardUnsavedChangesMessage', 'You have unsaved changes that will be lost.')} + confirmLabel={t('common:common.discard', 'Discard')} + cancelLabel={t('common:common.keepEditing', 'Keep editing')} + variant="warning" + /> {/* Login Prompt Dialog */} {showLoginPrompt && ( <> diff --git a/web/src/components/feedback/FeedbackModal.tsx b/web/src/components/feedback/FeedbackModal.tsx index c915cf51a..8676f4854 100644 --- a/web/src/components/feedback/FeedbackModal.tsx +++ b/web/src/components/feedback/FeedbackModal.tsx @@ -13,6 +13,7 @@ import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { X, Bug, Lightbulb, Send, CheckCircle2, ExternalLink, Linkedin, ImagePlus, Trash2, Copy, Check, AlertTriangle, Loader2 } from 'lucide-react' +import { ConfirmDialog } from '../../lib/modals' import { StatusBadge } from '../ui/StatusBadge' import { useRewards, REWARD_ACTIONS } from '../../hooks/useRewards' import { useToast } from '../ui/Toast' @@ -54,6 +55,7 @@ export function FeedbackModal({ isOpen, onClose, initialType = 'feature' }: Feed const [screenshots, setScreenshots] = useState<{ file: File; preview: string }[]>([]) const [isDragOver, setIsDragOver] = useState(false) const [copiedIndex, setCopiedIndex] = useState(null) + const [showDiscardConfirm, setShowDiscardConfirm] = useState(false) const fileInputRef = useRef(null) const handleScreenshotFiles = (files: FileList | null) => { @@ -183,10 +185,8 @@ export function FeedbackModal({ isOpen, onClose, initialType = 'feature' }: Feed } } - const handleClose = () => { - if (!success && (title.trim() !== '' || description.trim() !== '') && !window.confirm(t('common:common.discardUnsavedChanges', 'Discard unsaved changes?'))) { - return - } + const forceClose = () => { + setShowDiscardConfirm(false) localStorage.removeItem(DRAFT_KEY) setSuccess(null) setSubmitError(null) @@ -196,6 +196,14 @@ export function FeedbackModal({ isOpen, onClose, initialType = 'feature' }: Feed onClose() } + const handleClose = () => { + if (!success && (title.trim() !== '' || description.trim() !== '')) { + setShowDiscardConfirm(true) + return + } + forceClose() + } + // Keyboard navigation - ESC to close, Space to close when not typing useEffect(() => { if (!isOpen) return @@ -232,6 +240,16 @@ export function FeedbackModal({ isOpen, onClose, initialType = 'feature' }: Feed return createPortal(
+ setShowDiscardConfirm(false)} + onConfirm={forceClose} + title={t('common:common.discardUnsavedChanges', 'Discard unsaved changes?')} + message={t('common:common.discardUnsavedChangesMessage', 'You have unsaved changes that will be lost.')} + confirmLabel={t('common:common.discard', 'Discard')} + cancelLabel={t('common:common.keepEditing', 'Keep editing')} + variant="warning" + />
{/* Header */}
diff --git a/web/src/components/gpu/ReservationFormModal.tsx b/web/src/components/gpu/ReservationFormModal.tsx index a8fe833d5..2ee8b0503 100644 --- a/web/src/components/gpu/ReservationFormModal.tsx +++ b/web/src/components/gpu/ReservationFormModal.tsx @@ -7,7 +7,7 @@ import { Trash2, Loader2, } from 'lucide-react' -import { BaseModal } from '../../lib/modals' +import { BaseModal, ConfirmDialog } from '../../lib/modals' import { useNamespaces, createOrUpdateResourceQuota, @@ -72,10 +72,17 @@ export function ReservationFormModal({ const [extraResources, setExtraResources] = useState>([]) const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState(null) + const [showDiscardConfirm, setShowDiscardConfirm] = useState(false) + + const forceClose = () => { + setShowDiscardConfirm(false) + onClose() + } const handleClose = () => { const hasChanges = title.trim() !== '' || description.trim() !== '' - if (hasChanges && !window.confirm(t('common:common.discardUnsavedChanges', 'Discard unsaved changes?'))) { + if (hasChanges) { + setShowDiscardConfirm(true) return } onClose() @@ -218,6 +225,16 @@ export function ReservationFormModal({ return ( + setShowDiscardConfirm(false)} + onConfirm={forceClose} + title={t('common:common.discardUnsavedChanges', 'Discard unsaved changes?')} + message={t('common:common.discardUnsavedChangesMessage', 'You have unsaved changes that will be lost.')} + confirmLabel={t('common:common.discard', 'Discard')} + cancelLabel={t('common:common.keepEditing', 'Keep editing')} + variant="warning" + /> !initialAccess.some(a => a.type === 'Group' && a.name === g) ) + const [showDiscardConfirm, setShowDiscardConfirm] = useState(false) + + const forceClose = () => { + setShowDiscardConfirm(false) + onClose() + } + const handleClose = () => { - if ((name.trim() !== '' || teamLabel.trim() !== '') && !window.confirm(t('common:common.discardUnsavedChanges', 'Discard unsaved changes?'))) { + if (name.trim() !== '' || teamLabel.trim() !== '') { + setShowDiscardConfirm(true) return } onClose() @@ -110,6 +118,16 @@ export function CreateNamespaceModal({ clusters, onClose, onCreated }: CreateNam return ( + setShowDiscardConfirm(false)} + onConfirm={forceClose} + title={t('common:common.discardUnsavedChanges', 'Discard unsaved changes?')} + message={t('common:common.discardUnsavedChangesMessage', 'You have unsaved changes that will be lost.')} + confirmLabel={t('common:common.discard', 'Discard')} + cancelLabel={t('common:common.keepEditing', 'Keep editing')} + variant="warning" + /> { + setShowDiscardConfirm(false) + onClose() + } + const handleClose = () => { - if (subjectName.trim() !== '' && !window.confirm(t('common:common.discardUnsavedChanges', 'Discard unsaved changes?'))) { + if (subjectName.trim() !== '') { + setShowDiscardConfirm(true) return } onClose() @@ -97,6 +105,16 @@ export function GrantAccessModal({ namespace, existingAccess, onClose, onGranted return ( + setShowDiscardConfirm(false)} + onConfirm={forceClose} + title={t('common:common.discardUnsavedChanges', 'Discard unsaved changes?')} + message={t('common:common.discardUnsavedChangesMessage', 'You have unsaved changes that will be lost.')} + confirmLabel={t('common:common.discard', 'Discard')} + cancelLabel={t('common:common.keepEditing', 'Keep editing')} + variant="warning" + />