1
1
import { XIcon } from "lucide-react" ;
2
- import { useRef } from "react" ;
2
+ import { useCallback , useEffect , useRef } from "react" ;
3
3
import {
4
4
Control ,
5
5
Controller ,
@@ -11,8 +11,8 @@ import { Checkbox } from "zudoku/ui/Checkbox.js";
11
11
import { Autocomplete } from "../../../components/Autocomplete.js" ;
12
12
import { Button } from "../../../ui/Button.js" ;
13
13
import { Input } from "../../../ui/Input.js" ;
14
- import ParamsGrid from "./ParamsGrid.js" ;
15
- import { type PlaygroundForm } from "./Playground.js" ;
14
+ import ParamsGrid , { ParamsGridItem } from "./ParamsGrid.js" ;
15
+ import { Header , type PlaygroundForm } from "./Playground.js" ;
16
16
17
17
const headerOptions = Object . freeze ( [
18
18
"Accept" ,
@@ -43,22 +43,31 @@ const headerOptions = Object.freeze([
43
43
"X-Requested-With" ,
44
44
] ) ;
45
45
46
- export const Headers = ( { control } : { control : Control < PlaygroundForm > } ) => {
47
- const { fields, append, remove } = useFieldArray < PlaygroundForm > ( {
46
+ export const Headers = ( {
47
+ control,
48
+ headers : schemaHeaders ,
49
+ } : {
50
+ control : Control < PlaygroundForm > ;
51
+ headers : Header [ ] ;
52
+ } ) => {
53
+ const { fields, append, remove } = useFieldArray < PlaygroundForm , "headers" > ( {
48
54
control,
49
55
name : "headers" ,
50
56
} ) ;
51
- const { setValue } = useFormContext < PlaygroundForm > ( ) ;
57
+ const { setValue, watch } = useFormContext < PlaygroundForm > ( ) ;
52
58
const valueRefs = useRef < Array < HTMLInputElement | null > > ( [ ] ) ;
53
59
const nameRefs = useRef < Array < HTMLInputElement | null > > ( [ ] ) ;
60
+ const watchedHeaders = watch ( "headers" ) ;
54
61
55
- const addNewHeader = ( ) => {
56
- append ( {
57
- name : "" ,
58
- value : "" ,
59
- active : false ,
60
- } as PlaygroundForm [ "headers" ] [ number ] ) ;
61
- } ;
62
+ const addNewHeader = useCallback ( ( ) => {
63
+ append ( { name : "" , value : "" , active : false } ) ;
64
+ } , [ append ] ) ;
65
+
66
+ useEffect ( ( ) => {
67
+ if ( watchedHeaders . length === 0 ) {
68
+ addNewHeader ( ) ;
69
+ }
70
+ } , [ watchedHeaders , addNewHeader ] ) ;
62
71
63
72
const handleHeaderEnter = ( index : number ) => {
64
73
valueRefs . current [ index ] ?. focus ( ) ;
@@ -69,86 +78,113 @@ export const Headers = ({ control }: { control: Control<PlaygroundForm> }) => {
69
78
requestAnimationFrame ( ( ) => nameRefs . current [ index + 1 ] ?. focus ( ) ) ;
70
79
} ;
71
80
81
+ const missingHeaders = schemaHeaders
82
+ . filter ( ( h ) => ! watchedHeaders . some ( ( f ) => f . name === h . name ) )
83
+ . map ( ( { name } ) => name ) ;
84
+
72
85
return (
73
86
< div className = "flex flex-col gap-2" >
74
87
< Card className = "overflow-hidden" >
75
88
< ParamsGrid >
76
- { fields . map ( ( header , i ) => (
77
- < div
78
- key = { header . name }
79
- className = "group grid col-span-full grid-cols-subgrid"
80
- >
81
- < div className = "flex items-center gap-2 " >
82
- < Controller
83
- control = { control }
84
- name = { `headers.${ i } .active` }
85
- render = { ( { field } ) => (
86
- < Checkbox
87
- variant = "outline"
88
- id = { `headers.${ i } .active` }
89
- checked = { field . value }
90
- onCheckedChange = { ( checked ) => {
91
- field . onChange ( checked ) ;
92
- } }
93
- />
94
- ) }
95
- />
96
- < Controller
97
- control = { control }
98
- name = { `headers.${ i } .name` }
99
- render = { ( { field } ) => (
100
- < Autocomplete
101
- { ...field }
102
- placeholder = "Name"
103
- className = "border-0 shadow-none bg-transparent text-xs font-mono"
104
- options = { headerOptions }
105
- onEnterPress = { ( ) => handleHeaderEnter ( i ) }
106
- onChange = { ( e ) => {
107
- field . onChange ( e ) ;
108
- setValue ( `headers.${ i } .active` , true ) ;
109
- } }
110
- ref = { ( el ) => {
111
- nameRefs . current [ i ] = el ;
112
- } }
113
- />
114
- ) }
115
- />
116
- </ div >
117
- < div className = "flex items-center gap-2" >
118
- < Controller
119
- control = { control }
120
- name = { `headers.${ i } .value` }
121
- render = { ( { field } ) => (
122
- < Input
123
- placeholder = "Value"
124
- className = "w-full border-0 shadow-none text-xs font-mono focus-visible:ring-0"
125
- { ...field }
126
- ref = { ( el ) => {
127
- valueRefs . current [ i ] = el ;
128
- } }
129
- onKeyDown = { ( e ) => {
130
- if ( e . key === "Enter" && e . currentTarget . value . trim ( ) ) {
131
- handleValueEnter ( i ) ;
132
- }
133
- } }
134
- autoComplete = "off"
135
- />
136
- ) }
137
- />
138
- < Button
139
- size = "icon"
140
- variant = "ghost"
141
- className = "text-muted-foreground opacity-0 group-hover:opacity-100 rounded-full w-8 h-7"
142
- onClick = { ( ) => {
143
- remove ( i ) ;
144
- } }
145
- type = "button"
146
- >
147
- < XIcon size = { 16 } />
148
- </ Button >
149
- </ div >
150
- </ div >
151
- ) ) }
89
+ { fields . map ( ( field , i ) => {
90
+ const currentHeader = schemaHeaders . find (
91
+ ( h ) => h . name === watch ( `headers.${ i } .name` ) ,
92
+ ) ;
93
+ return (
94
+ < ParamsGridItem key = { field . id } >
95
+ < div className = "flex items-center gap-2 " >
96
+ < Controller
97
+ control = { control }
98
+ name = { `headers.${ i } .active` }
99
+ render = { ( { field } ) => (
100
+ < Checkbox
101
+ variant = "outline"
102
+ id = { `headers.${ i } .active` }
103
+ checked = { field . value }
104
+ onCheckedChange = { ( checked ) => {
105
+ field . onChange ( checked ) ;
106
+ } }
107
+ />
108
+ ) }
109
+ />
110
+ < Controller
111
+ control = { control }
112
+ name = { `headers.${ i } .name` }
113
+ render = { ( { field } ) => (
114
+ < Autocomplete
115
+ { ...field }
116
+ placeholder = "Name"
117
+ className = "border-0 shadow-none bg-transparent text-xs font-mono"
118
+ options = { [ ...missingHeaders , ...headerOptions ] }
119
+ onEnterPress = { ( ) => handleHeaderEnter ( i ) }
120
+ onChange = { ( e ) => {
121
+ field . onChange ( e ) ;
122
+ setValue ( `headers.${ i } .active` , true ) ;
123
+ } }
124
+ ref = { ( el ) => {
125
+ nameRefs . current [ i ] = el ;
126
+ } }
127
+ />
128
+ ) }
129
+ />
130
+ </ div >
131
+ < div className = "flex items-center gap-2" >
132
+ < Controller
133
+ control = { control }
134
+ name = { `headers.${ i } .value` }
135
+ render = { ( { field } ) => {
136
+ const hasEnum =
137
+ currentHeader ?. enum && currentHeader . enum . length > 0 ;
138
+
139
+ if ( ! hasEnum ) {
140
+ return (
141
+ < Input
142
+ placeholder = "Value"
143
+ className = "w-full border-0 shadow-none text-xs font-mono focus-visible:ring-0"
144
+ { ...field }
145
+ ref = { ( el ) => {
146
+ valueRefs . current [ i ] = el ;
147
+ } }
148
+ onKeyDown = { ( e ) => {
149
+ if (
150
+ e . key === "Enter" &&
151
+ e . currentTarget . value . trim ( )
152
+ ) {
153
+ handleValueEnter ( i ) ;
154
+ }
155
+ } }
156
+ autoComplete = "off"
157
+ />
158
+ ) ;
159
+ }
160
+
161
+ return (
162
+ < Autocomplete
163
+ shouldFilter = { false }
164
+ value = { field . value }
165
+ options = { currentHeader . enum ?? [ ] }
166
+ onChange = { ( e ) => {
167
+ field . onChange ( e ) ;
168
+ setValue ( `headers.${ i } .active` , true ) ;
169
+ } }
170
+ className = "font-mono text-xs border-0"
171
+ />
172
+ ) ;
173
+ } }
174
+ />
175
+ < Button
176
+ size = "icon"
177
+ variant = "ghost"
178
+ className = "text-muted-foreground opacity-0 group-hover:opacity-100 rounded-full w-8 h-7"
179
+ onClick = { ( ) => remove ( i ) }
180
+ type = "button"
181
+ >
182
+ < XIcon size = { 16 } />
183
+ </ Button >
184
+ </ div >
185
+ </ ParamsGridItem >
186
+ ) ;
187
+ } ) }
152
188
</ ParamsGrid >
153
189
</ Card >
154
190
< div className = "text-end" >
0 commit comments