1- import React , { useEffect , useState } from 'react'
2- import type { ChangeEvent } from 'react'
1+ import { useEffect , useState } from 'react'
2+ import type { ChangeEvent , ClipboardEvent } from 'react'
33import { v4 as uuidv4 } from 'uuid'
44
55// EnvironmentVariable is a single row in the table
@@ -9,7 +9,7 @@ type EnvironmentVariable = {
99 value : string
1010}
1111
12- const INITIAL_ROW_COUNT = 20
12+ const INITIAL_ROW_COUNT = 5
1313const KEY_REGEX = / ^ [ A - Z a - z 0 - 9 _ ] + $ /
1414
1515// createEmptyVariable is a helper function to create an empty row
@@ -40,7 +40,49 @@ export const VarManager = () => {
4040 setRawText ( event . target . value )
4141 }
4242
43+ // Parse lines with multiple vars
44+ const parseLinesToItems = ( text : string ) => {
45+ return text
46+ . split ( / \r ? \n / )
47+ . map ( ( l ) => l . trim ( ) )
48+ . filter ( ( l ) => l && ! l . startsWith ( '#' ) )
49+ . map ( ( l ) => {
50+ const idx = l . indexOf ( '=' )
51+ if ( idx === - 1 ) {
52+ return { key : l , value : '' }
53+ }
54+ return {
55+ key : l . slice ( 0 , idx ) . trim ( ) ,
56+ value : l . slice ( idx + 1 ) . trim ( ) ,
57+ }
58+ } )
59+ }
4360
61+ // onPaste handler for any row's input
62+ const handleRowPaste = (
63+ e : ClipboardEvent < HTMLTextAreaElement | HTMLInputElement > ,
64+ rowIndex : number
65+ ) => {
66+ const clipText = e . clipboardData . getData ( 'text/plain' )
67+ if ( ! clipText . includes ( '\n' ) ) {
68+ return
69+ }
70+ e . preventDefault ( )
71+
72+ const items = parseLinesToItems ( clipText )
73+ setVariables ( ( prev ) => {
74+ const next = [ ...prev ]
75+ items . forEach ( ( it , i ) => {
76+ const idx = rowIndex + i
77+ if ( idx < next . length ) {
78+ next [ idx ] = { id : uuidv4 ( ) , key : it . key , value : it . value }
79+ } else {
80+ next . push ( { id : uuidv4 ( ) , key : it . key , value : it . value } )
81+ }
82+ } )
83+ return next
84+ } )
85+ }
4486
4587 // parseAndPopulate parses the raw text for env vars
4688 const parseAndPopulate = ( ) => {
@@ -49,7 +91,7 @@ export const VarManager = () => {
4991 const lines = rawText . split ( '\n' )
5092 const parsedItems : { key : string ; value : string ; line : number } [ ] = [ ]
5193 const parseWarningMessages : string [ ] = [ ]
52- const keyCounts : Record < string , number > = { }
94+ const keyCounts : Record < string , number [ ] > = { }
5395
5496 lines . forEach ( ( line , index ) => {
5597 const lineNum = index + 1
@@ -72,6 +114,11 @@ export const VarManager = () => {
72114
73115 if ( key ) {
74116 parsedItems . push ( { key, value, line : lineNum } )
117+ // Track line numbers for duplicate detection
118+ if ( ! keyCounts [ key ] ) {
119+ keyCounts [ key ] = [ ]
120+ }
121+ keyCounts [ key ] . push ( lineNum )
75122 } else {
76123 // Key is empty even if '=' was present
77124 parseWarningMessages . push (
@@ -83,10 +130,10 @@ export const VarManager = () => {
83130 } )
84131
85132 // Detect duplicates
86- Object . entries ( keyCounts ) . forEach ( ( [ k , occ ] ) => {
87- if ( occ > 1 && Array . isArray ( keyCounts [ k ] ) ) {
133+ Object . entries ( keyCounts ) . forEach ( ( [ k , lineNumbers ] ) => {
134+ if ( lineNumbers . length > 1 ) {
88135 parseWarningMessages . push (
89- `Duplicate key "${ k } " on lines ${ keyCounts [ k ] . join ( ', ' ) } `
136+ `Duplicate key "${ k } " on lines ${ lineNumbers . join ( ', ' ) } ` // ✅ Correct!
90137 )
91138 }
92139 } )
@@ -137,10 +184,11 @@ export const VarManager = () => {
137184 [ id ] : 'Only letters, digits, and underscore allowed.' ,
138185 } ) )
139186 } else {
140- setRowErrors ( ( e ) => ( {
141- ...e ,
142- [ id ] : `Error in ${ id } ` ,
143- } ) )
187+ setRowErrors ( ( e ) => {
188+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
189+ const { [ id ] : _ , ...rest } = e // ✅ Disable the rule for this line
190+ return rest
191+ } )
144192 }
145193 }
146194 }
@@ -184,12 +232,12 @@ export const VarManager = () => {
184232 }
185233 } , [ status ] )
186234
187- useEffect ( ( ) => {
188-
189- if ( rawText !== '' ) {
190- parseAndPopulate ( )
191- }
192- } , [ rawText , parseAndPopulate ] )
235+ useEffect ( ( ) => {
236+ if ( rawText !== '' ) {
237+ parseAndPopulate ( )
238+ }
239+ // eslint-disable-next-line react-hooks/exhaustive-deps
240+ } , [ rawText ] )
193241
194242 return (
195243 < div className = "w-full p-4 md:p-6 bg-gray-800 shadow-xl rounded-lg" >
@@ -210,7 +258,6 @@ export const VarManager = () => {
210258 />
211259
212260 < div className = "flex flex-wrap gap-2 mb-4" >
213-
214261 < button
215262 onClick = { copyToClipboard }
216263 className = "bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded"
@@ -267,6 +314,7 @@ export const VarManager = () => {
267314 onChange = { ( e ) =>
268315 handleVariableChange ( row . id , 'key' , e . target . value )
269316 }
317+ onPaste = { ( e ) => handleRowPaste ( e , i ) }
270318 className = { `w-full text-sm p-1 rounded border ${
271319 rowErrors [ row . id ]
272320 ? 'border-red-500 bg-red-50'
0 commit comments