@mytec: iter1.1.1 ready for testing
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProjectsStore } from '@/store/projects.ts';
|
||||
import { useToastStore } from '@/components/ui/Toast.tsx';
|
||||
import { useDirtyStore } from '@/hooks/useUnsavedChanges.ts';
|
||||
import Button from '@/components/ui/Button.tsx';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog.tsx';
|
||||
import { logger } from '@/utils/logger.ts';
|
||||
|
||||
export default function ProjectPanel() {
|
||||
@@ -12,11 +14,19 @@ export default function ProjectPanel() {
|
||||
const loadProject = useProjectsStore((s) => s.loadProject);
|
||||
const deleteProject = useProjectsStore((s) => s.deleteProject);
|
||||
const addToast = useToastStore((s) => s.addToast);
|
||||
const isDirty = useDirtyStore((s) => s.isDirty);
|
||||
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [showSaveForm, setShowSaveForm] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Confirm dialog state
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
type: 'load' | 'delete';
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
}, [loadProjects]);
|
||||
@@ -30,6 +40,7 @@ export default function ProjectPanel() {
|
||||
|
||||
try {
|
||||
await saveProject(name);
|
||||
useDirtyStore.getState().markClean();
|
||||
addToast(`Project "${name}" saved`, 'success');
|
||||
setProjectName('');
|
||||
setShowSaveForm(false);
|
||||
@@ -39,11 +50,20 @@ export default function ProjectPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoad = async (id: string, name: string) => {
|
||||
const handleLoadRequest = (id: string, name: string) => {
|
||||
if (isDirty) {
|
||||
setConfirmAction({ type: 'load', id, name });
|
||||
} else {
|
||||
executeLoad(id, name);
|
||||
}
|
||||
};
|
||||
|
||||
const executeLoad = async (id: string, name: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const project = await loadProject(id);
|
||||
if (project) {
|
||||
useDirtyStore.getState().markClean();
|
||||
addToast(`Loaded project "${name}" (${project.sites.length} sites)`, 'success');
|
||||
} else {
|
||||
addToast('Project not found', 'error');
|
||||
@@ -56,7 +76,11 @@ export default function ProjectPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string, name: string) => {
|
||||
const handleDeleteRequest = (id: string, name: string) => {
|
||||
setConfirmAction({ type: 'delete', id, name });
|
||||
};
|
||||
|
||||
const executeDelete = async (id: string, name: string) => {
|
||||
try {
|
||||
await deleteProject(id);
|
||||
addToast(`Project "${name}" deleted`, 'info');
|
||||
@@ -66,6 +90,18 @@ export default function ProjectPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!confirmAction) return;
|
||||
const { type, id, name } = confirmAction;
|
||||
setConfirmAction(null);
|
||||
|
||||
if (type === 'load') {
|
||||
await executeLoad(id, name);
|
||||
} else {
|
||||
await executeDelete(id, name);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
@@ -142,7 +178,7 @@ export default function ProjectPanel() {
|
||||
</div>
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => handleLoad(project.id, project.name)}
|
||||
onClick={() => handleLoadRequest(project.id, project.name)}
|
||||
disabled={isLoading}
|
||||
className="px-2 py-1 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded
|
||||
min-w-[40px] min-h-[32px] flex items-center justify-center disabled:opacity-50"
|
||||
@@ -150,7 +186,7 @@ export default function ProjectPanel() {
|
||||
Load
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(project.id, project.name)}
|
||||
onClick={() => handleDeleteRequest(project.id, project.name)}
|
||||
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded
|
||||
min-w-[32px] min-h-[32px] flex items-center justify-center"
|
||||
>
|
||||
@@ -161,6 +197,27 @@ export default function ProjectPanel() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation dialog */}
|
||||
{confirmAction && (
|
||||
<ConfirmDialog
|
||||
title={
|
||||
confirmAction.type === 'delete'
|
||||
? 'Delete Project?'
|
||||
: 'Unsaved Changes'
|
||||
}
|
||||
message={
|
||||
confirmAction.type === 'delete'
|
||||
? `Delete project "${confirmAction.name}"? This cannot be undone.`
|
||||
: `You have unsaved changes. Load project "${confirmAction.name}" anyway?`
|
||||
}
|
||||
confirmLabel={confirmAction.type === 'delete' ? 'Delete' : 'Load'}
|
||||
cancelLabel="Cancel"
|
||||
variant={confirmAction.type === 'delete' ? 'danger' : 'warning'}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={() => setConfirmAction(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,17 @@ interface ConfirmDialogProps {
|
||||
cancelLabel?: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
/** @deprecated Use variant instead */
|
||||
danger?: boolean;
|
||||
variant?: 'danger' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
const VARIANT_STYLES = {
|
||||
danger: 'bg-red-600 hover:bg-red-700 focus:ring-red-500',
|
||||
warning: 'bg-amber-500 hover:bg-amber-600 focus:ring-amber-400',
|
||||
info: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500',
|
||||
} as const;
|
||||
|
||||
export default function ConfirmDialog({
|
||||
title,
|
||||
message,
|
||||
@@ -18,11 +26,15 @@ export default function ConfirmDialog({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
danger = false,
|
||||
variant,
|
||||
}: ConfirmDialogProps) {
|
||||
const confirmRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Resolve variant: explicit variant prop takes priority, then legacy danger boolean
|
||||
const resolvedVariant = variant ?? (danger ? 'danger' : 'info');
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-focus the cancel button (safer default)
|
||||
// Auto-focus the confirm button
|
||||
confirmRef.current?.focus();
|
||||
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
@@ -67,10 +79,7 @@ export default function ConfirmDialog({
|
||||
ref={confirmRef}
|
||||
onClick={onConfirm}
|
||||
className={`px-4 py-2 text-sm rounded-md text-white transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
${danger
|
||||
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
||||
: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'
|
||||
}`}
|
||||
${VARIANT_STYLES[resolvedVariant]}`}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user