@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
66import { Input } from "@/components/ui/input"
77import { Checkbox } from "@/components/ui/checkbox"
88import { useStateManager } from "./useStateManager"
9- import { validateSource } from "@roo/shared/MarketplaceValidation"
9+ import { validateSource , ValidationError } from "@roo/shared/MarketplaceValidation"
1010import { cn } from "@src/lib/utils"
1111
1212export interface MarketplaceSourcesConfigProps {
@@ -19,18 +19,90 @@ export function MarketplaceSourcesConfig({ stateManager }: MarketplaceSourcesCon
1919 const [ newSourceUrl , setNewSourceUrl ] = useState ( "" )
2020 const [ newSourceName , setNewSourceName ] = useState ( "" )
2121 const [ error , setError ] = useState ( "" )
22+ const [ fieldErrors , setFieldErrors ] = useState < {
23+ name ?: string
24+ url ?: string
25+ } > ( { } )
26+
27+ // Check if name contains emoji characters
28+ const containsEmoji = ( str : string ) : boolean => {
29+ // Simple emoji detection using common emoji ranges
30+ // This avoids using Unicode property escapes which require ES2018+
31+ return (
32+ / [ \ud83c \ud83d \ud83e ] [ \ud000 - \udfff ] / . test ( str ) || // Common emoji surrogate pairs
33+ / [ \u2600 - \u27BF ] / . test ( str ) || // Misc symbols and pictographs
34+ / [ \u2300 - \u23FF ] / . test ( str ) || // Miscellaneous Technical
35+ / [ \u2700 - \u27FF ] / . test ( str ) || // Dingbats
36+ / [ \u2B50 \u2B55 ] / . test ( str ) || // Star, Circle
37+ / [ \u203C \u2049 \u20E3 \u2122 \u2139 \u2194 - \u2199 \u21A9 \u21AA ] / . test ( str )
38+ ) // Punctuation
39+ }
40+
41+ // Validate input fields without submitting
42+ const validateFields = ( ) => {
43+ const newErrors : { name ?: string ; url ?: string } = { }
44+
45+ // Validate name if provided
46+ if ( newSourceName ) {
47+ if ( newSourceName . length > 20 ) {
48+ newErrors . name = t ( "marketplace:sources.errors.nameTooLong" )
49+ } else if ( containsEmoji ( newSourceName ) ) {
50+ newErrors . name = t ( "marketplace:sources.errors.emojiName" )
51+ } else {
52+ // Check for duplicate names
53+ const hasDuplicateName = state . sources . some (
54+ ( source ) => source . name && source . name . toLowerCase ( ) === newSourceName . toLowerCase ( ) ,
55+ )
56+ if ( hasDuplicateName ) {
57+ newErrors . name = t ( "marketplace:sources.errors.duplicateName" )
58+ }
59+ }
60+ }
61+
62+ // Validate URL
63+ if ( ! newSourceUrl . trim ( ) ) {
64+ newErrors . url = t ( "marketplace:sources.errors.emptyUrl" )
65+ } else {
66+ // Check for duplicate URLs
67+ const hasDuplicateUrl = state . sources . some (
68+ ( source ) => source . url . toLowerCase ( ) . trim ( ) === newSourceUrl . toLowerCase ( ) . trim ( ) ,
69+ )
70+ if ( hasDuplicateUrl ) {
71+ newErrors . url = t ( "marketplace:sources.errors.duplicateUrl" )
72+ }
73+ }
74+
75+ setFieldErrors ( newErrors )
76+ return Object . keys ( newErrors ) . length === 0
77+ }
2278
2379 const handleAddSource = ( ) => {
2480 const MAX_SOURCES = 10
2581 if ( state . sources . length >= MAX_SOURCES ) {
2682 setError ( t ( "marketplace:sources.errors.maxSources" , { max : MAX_SOURCES } ) )
2783 return
2884 }
85+
86+ // Clear previous errors
87+ setError ( "" )
88+
89+ // Perform quick validation first
90+ if ( ! validateFields ( ) ) {
91+ // If we have specific field errors, show the first one as the main error
92+ if ( fieldErrors . url ) {
93+ setError ( fieldErrors . url )
94+ } else if ( fieldErrors . name ) {
95+ setError ( fieldErrors . name )
96+ }
97+ return
98+ }
99+
29100 const sourceToValidate : MarketplaceSource = {
30- url : newSourceUrl ,
31- name : newSourceName || undefined ,
101+ url : newSourceUrl . trim ( ) ,
102+ name : newSourceName . trim ( ) || undefined ,
32103 enabled : true ,
33104 }
105+
34106 const validationErrors = validateSource ( sourceToValidate , state . sources )
35107 if ( validationErrors . length > 0 ) {
36108 const errorMessages : Record < string , string > = {
@@ -42,7 +114,34 @@ export function MarketplaceSourcesConfig({ stateManager }: MarketplaceSourcesCon
42114 "name:nonvisible" : "marketplace:sources.errors.nonVisibleCharsName" ,
43115 "name:duplicate" : "marketplace:sources.errors.duplicateName" ,
44116 }
45- const error = validationErrors [ 0 ]
117+
118+ // Group errors by field for better user feedback
119+ const fieldErrorMap : Record < string , ValidationError [ ] > = { }
120+ for ( const error of validationErrors ) {
121+ if ( ! fieldErrorMap [ error . field ] ) {
122+ fieldErrorMap [ error . field ] = [ ]
123+ }
124+ fieldErrorMap [ error . field ] . push ( error )
125+ }
126+
127+ // Update field-specific errors
128+ const newFieldErrors : { name ?: string ; url ?: string } = { }
129+ if ( fieldErrorMap . name ) {
130+ const error = fieldErrorMap . name [ 0 ]
131+ const errorKey = `name:${ error . message . toLowerCase ( ) . split ( " " ) [ 0 ] } `
132+ newFieldErrors . name = t ( errorMessages [ errorKey ] || error . message )
133+ }
134+
135+ if ( fieldErrorMap . url ) {
136+ const error = fieldErrorMap . url [ 0 ]
137+ const errorKey = `url:${ error . message . toLowerCase ( ) . split ( " " ) [ 0 ] } `
138+ newFieldErrors . url = t ( errorMessages [ errorKey ] || error . message )
139+ }
140+
141+ setFieldErrors ( newFieldErrors )
142+
143+ // Set the main error message (prioritize URL errors)
144+ const error = fieldErrorMap . url ?. [ 0 ] || validationErrors [ 0 ]
46145 const errorKey = `${ error . field } :${ error . message . toLowerCase ( ) . split ( " " ) [ 0 ] } `
47146 setError ( t ( errorMessages [ errorKey ] || "marketplace:sources.errors.invalidGitUrl" ) )
48147 return
@@ -97,16 +196,40 @@ export function MarketplaceSourcesConfig({ stateManager }: MarketplaceSourcesCon
97196 onChange = { ( e ) => {
98197 setNewSourceName ( e . target . value . slice ( 0 , 20 ) )
99198 setError ( "" )
199+ setFieldErrors ( ( prev ) => ( { ...prev , name : undefined } ) )
200+
201+ // Live validation for emojis and length
202+ const value = e . target . value
203+ if ( value && containsEmoji ( value ) ) {
204+ setFieldErrors ( ( prev ) => ( {
205+ ...prev ,
206+ name : t ( "marketplace:sources.errors.emojiName" ) ,
207+ } ) )
208+ } else if ( value . length >= 20 ) {
209+ setFieldErrors ( ( prev ) => ( {
210+ ...prev ,
211+ name : t ( "marketplace:sources.errors.nameTooLong" ) ,
212+ } ) )
213+ }
100214 } }
101215 maxLength = { 20 }
102- className = "pl-10"
216+ className = { cn ( "pl-10" , {
217+ "border-red-500 focus-visible:ring-red-500" : fieldErrors . name ,
218+ } ) }
219+ onBlur = { ( ) => validateFields ( ) }
103220 />
104221 < span className = "absolute left-3 top-1/2 -translate-y-1/2 text-vscode-descriptionForeground" >
105222 < span className = "codicon codicon-tag" > </ span >
106223 </ span >
107- < span className = "absolute right-3 top-1/2 -translate-y-1/2 text-xs text-vscode-descriptionForeground" >
224+ < span
225+ className = { cn (
226+ "absolute right-3 top-1/2 -translate-y-1/2 text-xs" ,
227+ newSourceName . length >= 18 ? "text-amber-500" : "text-vscode-descriptionForeground" ,
228+ newSourceName . length >= 20 ? "text-red-500" : "" ,
229+ ) } >
108230 { newSourceName . length } /20
109231 </ span >
232+ { fieldErrors . name && < p className = "text-xs text-red-500 mt-1 mb-0" > { fieldErrors . name } </ p > }
110233 </ div >
111234 < div className = "relative" >
112235 < Input
@@ -116,12 +239,25 @@ export function MarketplaceSourcesConfig({ stateManager }: MarketplaceSourcesCon
116239 onChange = { ( e ) => {
117240 setNewSourceUrl ( e . target . value )
118241 setError ( "" )
242+ setFieldErrors ( ( prev ) => ( { ...prev , url : undefined } ) )
243+
244+ // Live validation for empty URL
245+ if ( ! e . target . value . trim ( ) ) {
246+ setFieldErrors ( ( prev ) => ( {
247+ ...prev ,
248+ url : t ( "marketplace:sources.errors.emptyUrl" ) ,
249+ } ) )
250+ }
119251 } }
120- className = "pl-10"
252+ className = { cn ( "pl-10" , {
253+ "border-red-500 focus-visible:ring-red-500" : fieldErrors . url ,
254+ } ) }
255+ onBlur = { ( ) => validateFields ( ) }
121256 />
122257 < span className = "absolute left-3 top-1/2 -translate-y-1/2 text-vscode-descriptionForeground" >
123258 < span className = "codicon codicon-link" > </ span >
124259 </ span >
260+ { fieldErrors . url && < p className = "text-xs text-red-500 mt-1 mb-0" > { fieldErrors . url } </ p > }
125261 </ div >
126262 < p className = "text-xs text-vscode-descriptionForeground m-0" >
127263 { t ( "marketplace:sources.add.urlFormats" ) }
@@ -135,7 +271,10 @@ export function MarketplaceSourcesConfig({ stateManager }: MarketplaceSourcesCon
135271 </ p >
136272 </ div >
137273 ) }
138- < Button onClick = { handleAddSource } className = "mt-2 w-full shadow-none border-none" >
274+ < Button
275+ onClick = { handleAddSource }
276+ className = "mt-2 w-full shadow-none border-none"
277+ disabled = { ! ! fieldErrors . name || ! ! fieldErrors . url || ! newSourceUrl . trim ( ) } >
139278 < span className = "codicon codicon-add" > </ span >
140279 { t ( "marketplace:sources.add.button" ) }
141280 </ Button >
0 commit comments