@@ -3,6 +3,7 @@ import { memo, useEffect, useRef, useState } from "react"
33import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
44import { Dropdown } from "vscrui"
55import type { DropdownOption } from "vscrui"
6+ import { Dialog , DialogContent } from "../ui/dialog"
67
78interface ApiConfigManagerProps {
89 currentApiConfigName ?: string
@@ -21,55 +22,113 @@ const ApiConfigManager = ({
2122 onRenameConfig,
2223 onUpsertConfig,
2324} : ApiConfigManagerProps ) => {
24- const [ editState , setEditState ] = useState < "new" | "rename" | null > ( null )
25+ const [ isRenaming , setIsRenaming ] = useState ( false )
26+ const [ isCreating , setIsCreating ] = useState ( false )
2527 const [ inputValue , setInputValue ] = useState ( "" )
26- const inputRef = useRef < HTMLInputElement > ( )
28+ const [ newProfileName , setNewProfileName ] = useState ( "" )
29+ const [ error , setError ] = useState < string | null > ( null )
30+ const inputRef = useRef < any > ( null )
31+ const newProfileInputRef = useRef < any > ( null )
2732
28- // Focus input when entering edit mode
33+ const validateName = ( name : string , isNewProfile : boolean ) : string | null => {
34+ const trimmed = name . trim ( )
35+ if ( ! trimmed ) return "Name cannot be empty"
36+
37+ const nameExists = listApiConfigMeta ?. some ( ( config ) => config . name . toLowerCase ( ) === trimmed . toLowerCase ( ) )
38+
39+ // For new profiles, any existing name is invalid
40+ if ( isNewProfile && nameExists ) {
41+ return "A profile with this name already exists"
42+ }
43+
44+ // For rename, only block if trying to rename to a different existing profile
45+ if ( ! isNewProfile && nameExists && trimmed . toLowerCase ( ) !== currentApiConfigName ?. toLowerCase ( ) ) {
46+ return "A profile with this name already exists"
47+ }
48+
49+ return null
50+ }
51+
52+ const resetCreateState = ( ) => {
53+ setIsCreating ( false )
54+ setNewProfileName ( "" )
55+ setError ( null )
56+ }
57+
58+ const resetRenameState = ( ) => {
59+ setIsRenaming ( false )
60+ setInputValue ( "" )
61+ setError ( null )
62+ }
63+
64+ // Focus input when entering rename mode
2965 useEffect ( ( ) => {
30- if ( editState ) {
31- setTimeout ( ( ) => inputRef . current ?. focus ( ) , 0 )
66+ if ( isRenaming ) {
67+ const timeoutId = setTimeout ( ( ) => inputRef . current ?. focus ( ) , 0 )
68+ return ( ) => clearTimeout ( timeoutId )
3269 }
33- } , [ editState ] )
70+ } , [ isRenaming ] )
3471
35- // Reset edit state when current profile changes
72+ // Focus input when opening new dialog
3673 useEffect ( ( ) => {
37- setEditState ( null )
38- setInputValue ( "" )
74+ if ( isCreating ) {
75+ const timeoutId = setTimeout ( ( ) => newProfileInputRef . current ?. focus ( ) , 0 )
76+ return ( ) => clearTimeout ( timeoutId )
77+ }
78+ } , [ isCreating ] )
79+
80+ // Reset state when current profile changes
81+ useEffect ( ( ) => {
82+ resetCreateState ( )
83+ resetRenameState ( )
3984 } , [ currentApiConfigName ] )
4085
4186 const handleAdd = ( ) => {
42- const newConfigName = currentApiConfigName + " (copy)"
43- onUpsertConfig ( newConfigName )
87+ resetCreateState ( )
88+ setIsCreating ( true )
4489 }
4590
4691 const handleStartRename = ( ) => {
47- setEditState ( "rename" )
92+ setIsRenaming ( true )
4893 setInputValue ( currentApiConfigName || "" )
94+ setError ( null )
4995 }
5096
5197 const handleCancel = ( ) => {
52- setEditState ( null )
53- setInputValue ( "" )
98+ resetRenameState ( )
5499 }
55100
56101 const handleSave = ( ) => {
57102 const trimmedValue = inputValue . trim ( )
58- if ( ! trimmedValue ) return
103+ const error = validateName ( trimmedValue , false )
59104
60- if ( editState === "new" ) {
61- onUpsertConfig ( trimmedValue )
62- } else if ( editState === "rename" && currentApiConfigName ) {
105+ if ( error ) {
106+ setError ( error )
107+ return
108+ }
109+
110+ if ( isRenaming && currentApiConfigName ) {
63111 if ( currentApiConfigName === trimmedValue ) {
64- setEditState ( null )
65- setInputValue ( "" )
112+ resetRenameState ( )
66113 return
67114 }
68115 onRenameConfig ( currentApiConfigName , trimmedValue )
69116 }
70117
71- setEditState ( null )
72- setInputValue ( "" )
118+ resetRenameState ( )
119+ }
120+
121+ const handleNewProfileSave = ( ) => {
122+ const trimmedValue = newProfileName . trim ( )
123+ const error = validateName ( trimmedValue , true )
124+
125+ if ( error ) {
126+ setError ( error )
127+ return
128+ }
129+
130+ onUpsertConfig ( trimmedValue )
131+ resetCreateState ( )
73132 }
74133
75134 const handleDelete = ( ) => {
@@ -93,49 +152,63 @@ const ApiConfigManager = ({
93152 < span style = { { fontWeight : "500" } } > Configuration Profile</ span >
94153 </ label >
95154
96- { editState ? (
97- < div style = { { display : "flex" , gap : "4px" , alignItems : "center" } } >
98- < VSCodeTextField
99- ref = { inputRef as any }
100- value = { inputValue }
101- onInput = { ( e : any ) => setInputValue ( e . target . value ) }
102- placeholder = { editState === "new" ? "Enter profile name" : "Enter new name" }
103- style = { { flexGrow : 1 } }
104- onKeyDown = { ( e : any ) => {
105- if ( e . key === "Enter" && inputValue . trim ( ) ) {
106- handleSave ( )
107- } else if ( e . key === "Escape" ) {
108- handleCancel ( )
109- }
110- } }
111- />
112- < VSCodeButton
113- appearance = "icon"
114- disabled = { ! inputValue . trim ( ) }
115- onClick = { handleSave }
116- title = "Save"
117- style = { {
118- padding : 0 ,
119- margin : 0 ,
120- height : "28px" ,
121- width : "28px" ,
122- minWidth : "28px" ,
123- } } >
124- < span className = "codicon codicon-check" />
125- </ VSCodeButton >
126- < VSCodeButton
127- appearance = "icon"
128- onClick = { handleCancel }
129- title = "Cancel"
130- style = { {
131- padding : 0 ,
132- margin : 0 ,
133- height : "28px" ,
134- width : "28px" ,
135- minWidth : "28px" ,
136- } } >
137- < span className = "codicon codicon-close" />
138- </ VSCodeButton >
155+ { isRenaming ? (
156+ < div
157+ data-testid = "rename-form"
158+ style = { { display : "flex" , gap : "4px" , alignItems : "center" , flexDirection : "column" } } >
159+ < div style = { { display : "flex" , gap : "4px" , alignItems : "center" , width : "100%" } } >
160+ < VSCodeTextField
161+ ref = { inputRef }
162+ value = { inputValue }
163+ onInput = { ( e : unknown ) => {
164+ const target = e as { target : { value : string } }
165+ setInputValue ( target . target . value )
166+ setError ( null )
167+ } }
168+ placeholder = "Enter new name"
169+ style = { { flexGrow : 1 } }
170+ onKeyDown = { ( e : unknown ) => {
171+ const event = e as { key : string }
172+ if ( event . key === "Enter" && inputValue . trim ( ) ) {
173+ handleSave ( )
174+ } else if ( event . key === "Escape" ) {
175+ handleCancel ( )
176+ }
177+ } }
178+ />
179+ < VSCodeButton
180+ appearance = "icon"
181+ disabled = { ! inputValue . trim ( ) }
182+ onClick = { handleSave }
183+ title = "Save"
184+ style = { {
185+ padding : 0 ,
186+ margin : 0 ,
187+ height : "28px" ,
188+ width : "28px" ,
189+ minWidth : "28px" ,
190+ } } >
191+ < span className = "codicon codicon-check" />
192+ </ VSCodeButton >
193+ < VSCodeButton
194+ appearance = "icon"
195+ onClick = { handleCancel }
196+ title = "Cancel"
197+ style = { {
198+ padding : 0 ,
199+ margin : 0 ,
200+ height : "28px" ,
201+ width : "28px" ,
202+ minWidth : "28px" ,
203+ } } >
204+ < span className = "codicon codicon-close" />
205+ </ VSCodeButton >
206+ </ div >
207+ { error && (
208+ < p className = "text-red-500 text-sm mt-2" data-testid = "error-message" >
209+ { error }
210+ </ p >
211+ ) }
139212 </ div >
140213 ) : (
141214 < >
@@ -211,6 +284,63 @@ const ApiConfigManager = ({
211284 </ p >
212285 </ >
213286 ) }
287+
288+ < Dialog
289+ open = { isCreating }
290+ onOpenChange = { ( open : boolean ) => {
291+ if ( open ) {
292+ setIsCreating ( true )
293+ setNewProfileName ( "" )
294+ setError ( null )
295+ } else {
296+ resetCreateState ( )
297+ }
298+ } }
299+ aria-labelledby = "new-profile-title" >
300+ < DialogContent className = "p-4 max-w-sm" >
301+ < h2 id = "new-profile-title" className = "text-lg font-semibold mb-4" >
302+ New Configuration Profile
303+ </ h2 >
304+ < button className = "absolute right-4 top-4" aria-label = "Close dialog" onClick = { resetCreateState } >
305+ < span className = "codicon codicon-close" />
306+ </ button >
307+ < VSCodeTextField
308+ ref = { newProfileInputRef }
309+ value = { newProfileName }
310+ onInput = { ( e : unknown ) => {
311+ const target = e as { target : { value : string } }
312+ setNewProfileName ( target . target . value )
313+ setError ( null )
314+ } }
315+ placeholder = "Enter profile name"
316+ style = { { width : "100%" } }
317+ onKeyDown = { ( e : unknown ) => {
318+ const event = e as { key : string }
319+ if ( event . key === "Enter" && newProfileName . trim ( ) ) {
320+ handleNewProfileSave ( )
321+ } else if ( event . key === "Escape" ) {
322+ resetCreateState ( )
323+ }
324+ } }
325+ />
326+ { error && (
327+ < p className = "text-red-500 text-sm mt-2" data-testid = "error-message" >
328+ { error }
329+ </ p >
330+ ) }
331+ < div className = "flex justify-end gap-2 mt-4" >
332+ < VSCodeButton appearance = "secondary" onClick = { resetCreateState } >
333+ Cancel
334+ </ VSCodeButton >
335+ < VSCodeButton
336+ appearance = "primary"
337+ disabled = { ! newProfileName . trim ( ) }
338+ onClick = { handleNewProfileSave } >
339+ Create Profile
340+ </ VSCodeButton >
341+ </ div >
342+ </ DialogContent >
343+ </ Dialog >
214344 </ div >
215345 </ div >
216346 )
0 commit comments