fix(ui): add unsaved changes confirmation when switching profiles

- Add onHasChangesUpdate callback to ProfileEditor

- Track editor dirty state in ApiPage parent

- Show confirmation dialog before discarding unsaved changes

- Handle edge case for 'New' profile button

Closes #163
This commit is contained in:
kaitranntt
2025-12-21 01:05:23 -05:00
parent bf65de55cf
commit b790005c85
3 changed files with 41 additions and 6 deletions
+7 -2
View File
@@ -4,7 +4,7 @@
*/
/* eslint-disable react-refresh/only-export-components */
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
@@ -16,7 +16,7 @@ import { FriendlyUISection } from './friendly-ui-section';
import { RawEditorSection } from './raw-editor-section';
import type { ProfileEditorProps, Settings, SettingsResponse } from './types';
export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
export function ProfileEditor({ profileName, onDelete, onHasChangesUpdate }: ProfileEditorProps) {
const [localEdits, setLocalEdits] = useState<Record<string, string>>({});
const [conflictDialog, setConflictDialog] = useState(false);
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
@@ -100,6 +100,11 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
return Object.keys(localEdits).length > 0;
}, [rawJsonEdits, localEdits, settings]);
// Notify parent of hasChanges state
useEffect(() => {
onHasChangesUpdate?.(computedHasChanges);
}, [computedHasChanges, onHasChangesUpdate]);
// Save mutation
const saveMutation = useMutation({
mutationFn: async () => {
@@ -16,4 +16,5 @@ export interface SettingsResponse {
export interface ProfileEditorProps {
profileName: string;
onDelete?: () => void;
onHasChangesUpdate?: (hasChanges: boolean) => void;
}
+33 -4
View File
@@ -37,6 +37,8 @@ export function ApiPage() {
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
const [createMode, setCreateMode] = useState<'normal' | 'openrouter'>('normal');
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [editorHasChanges, setEditorHasChanges] = useState(false);
const [pendingSwitch, setPendingSwitch] = useState<string | null>(null);
// Prefetch OpenRouter models when page loads (lazy - won't block render)
useOpenRouterModels();
@@ -71,7 +73,21 @@ export function ApiPage() {
// Handle create success
const handleCreateSuccess = (name: string) => {
setCreateDialogOpen(false);
setSelectedProfile(name);
// Use the same unsaved changes check as profile selection
if (editorHasChanges && selectedProfile !== null) {
setPendingSwitch(name);
} else {
setSelectedProfile(name);
}
};
// Handle profile selection with unsaved changes check
const handleProfileSelect = (name: string) => {
if (editorHasChanges && selectedProfile !== name) {
setPendingSwitch(name);
} else {
setSelectedProfile(name);
}
};
return (
@@ -168,9 +184,7 @@ export function ApiPage() {
key={profile.name}
profile={profile}
isSelected={selectedProfile === profile.name}
onSelect={() => {
setSelectedProfile(profile.name);
}}
onSelect={() => handleProfileSelect(profile.name)}
onDelete={() => setDeleteConfirm(profile.name)}
/>
))}
@@ -208,6 +222,7 @@ export function ApiPage() {
<ProfileEditor
profileName={selectedProfileData.name}
onDelete={() => setDeleteConfirm(selectedProfileData.name)}
onHasChangesUpdate={setEditorHasChanges}
/>
) : (
<OpenRouterQuickStart
@@ -242,6 +257,20 @@ export function ApiPage() {
onConfirm={() => deleteConfirm && handleDelete(deleteConfirm)}
onCancel={() => setDeleteConfirm(null)}
/>
{/* Unsaved Changes Confirmation */}
<ConfirmDialog
open={!!pendingSwitch}
title="Unsaved Changes"
description={`You have unsaved changes in "${selectedProfile}". Discard and switch to "${pendingSwitch}"?`}
confirmText="Discard & Switch"
variant="destructive"
onConfirm={() => {
setSelectedProfile(pendingSwitch);
setPendingSwitch(null);
}}
onCancel={() => setPendingSwitch(null)}
/>
</div>
);
}