mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 20:24:21 +00:00
Merge pull request #170 from kaitranntt/kai/feat/standardize-input-state-persistence
fix(ui): add unsaved changes confirmation when switching profiles
This commit is contained in:
@@ -38,7 +38,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
description: '349+ models from OpenAI, Anthropic, Google, Meta',
|
||||
baseUrl: OPENROUTER_BASE_URL,
|
||||
defaultProfileName: 'openrouter',
|
||||
defaultModel: 'anthropic/claude-sonnet-4',
|
||||
defaultModel: 'anthropic/claude-opus-4.5',
|
||||
apiKeyPlaceholder: 'sk-or-...',
|
||||
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
||||
category: 'recommended',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ConfirmDialog({
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
className={variant === 'destructive' ? 'bg-red-600 hover:bg-red-700' : ''}
|
||||
className={variant === 'destructive' ? 'bg-red-600 hover:bg-red-700 text-white' : ''}
|
||||
>
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
badge: '349+ models',
|
||||
featured: true,
|
||||
icon: '/icons/openrouter.svg',
|
||||
defaultModel: 'anthropic/claude-sonnet-4',
|
||||
defaultModel: 'anthropic/claude-opus-4.5',
|
||||
requiresApiKey: true,
|
||||
apiKeyPlaceholder: 'sk-or-...',
|
||||
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
||||
|
||||
+35
-4
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
@@ -206,8 +220,10 @@ export function ApiPage() {
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{selectedProfileData ? (
|
||||
<ProfileEditor
|
||||
key={selectedProfileData.name}
|
||||
profileName={selectedProfileData.name}
|
||||
onDelete={() => setDeleteConfirm(selectedProfileData.name)}
|
||||
onHasChangesUpdate={setEditorHasChanges}
|
||||
/>
|
||||
) : (
|
||||
<OpenRouterQuickStart
|
||||
@@ -242,6 +258,21 @@ 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={() => {
|
||||
setEditorHasChanges(false);
|
||||
setSelectedProfile(pendingSwitch);
|
||||
setPendingSwitch(null);
|
||||
}}
|
||||
onCancel={() => setPendingSwitch(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user