@@ -10,17 +10,164 @@ import { Container, Row, Col, Form, Button } from "react-bootstrap";
10
10
11
11
import { useToastsContext } from "../hooks/useToasts" ;
12
12
import { evaluateJexl } from "../jexlParser" ;
13
+ import { debounce } from "../utils/functional" ;
14
+
15
+ type ContextValue = object | string | boolean | number | Date ;
16
+ type FieldType = "object" | "string" | "boolean" | "number" | "Date" ;
17
+ type FormDataValue = string | boolean ;
18
+
19
+ type OnChangeFn = {
20
+ ( key : string , value : boolean , fieldType : "boolean" ) : void ;
21
+ ( key : string , value : string , fieldType : Exclude < FieldType , "boolean" > ) : void ;
22
+ } ;
23
+
24
+ type ContextFieldProps < TValue extends FormDataValue > = {
25
+ fieldType : TValue extends boolean ? "boolean" : Exclude < FieldType , "boolean" > ;
26
+ onChange : OnChangeFn ;
27
+ contextKey : string ;
28
+ value : TValue ;
29
+ } ;
30
+
31
+ const getFieldType = ( value : ContextValue ) : FieldType => {
32
+ switch ( typeof value ) {
33
+ case "string" :
34
+ return "string" ;
35
+ case "number" :
36
+ return "number" ;
37
+ case "boolean" :
38
+ return "boolean" ;
39
+ case "object" :
40
+ if ( value instanceof Date ) {
41
+ return "Date" ;
42
+ } else {
43
+ return "object" ;
44
+ }
45
+ }
46
+ } ;
47
+
48
+ function ContextField < TValue extends FormDataValue > ( {
49
+ fieldType,
50
+ onChange,
51
+ contextKey,
52
+ value,
53
+ } : ContextFieldProps < TValue > ) {
54
+ const handleChange = useCallback (
55
+ ( e : ChangeEvent < HTMLInputElement | HTMLTextAreaElement > ) => {
56
+ if ( fieldType === "boolean" ) {
57
+ onChange ( contextKey , ( e . target as HTMLInputElement ) . checked , fieldType ) ;
58
+ } else {
59
+ onChange ( contextKey , e . target . value , fieldType ) ;
60
+ }
61
+ } ,
62
+ [ onChange , contextKey , fieldType ] ,
63
+ ) ;
64
+
65
+ switch ( fieldType ) {
66
+ case "number" :
67
+ return (
68
+ < Form . Control
69
+ type = "number"
70
+ value = { value as string }
71
+ onChange = { handleChange }
72
+ className = "p-3 w-50 grey-border short-text"
73
+ />
74
+ ) ;
75
+
76
+ case "string" :
77
+ return (
78
+ < Form . Control
79
+ type = "text"
80
+ value = { value as string }
81
+ onChange = { handleChange }
82
+ className = "p-3 w-50 grey-border short-text"
83
+ />
84
+ ) ;
85
+
86
+ case "boolean" :
87
+ return (
88
+ < Form . Check
89
+ type = "checkbox"
90
+ checked = { value as boolean }
91
+ onChange = { handleChange }
92
+ className = "p-3 w-50 ms-4 ps-5 mb-2 large-checkbox short-text"
93
+ />
94
+ ) ;
95
+
96
+ case "Date" :
97
+ return (
98
+ < Form . Control
99
+ type = "datetime-local"
100
+ step = "1"
101
+ value = { value as string }
102
+ onChange = { handleChange }
103
+ className = "p-3 w-50 grey-border short-text"
104
+ />
105
+ ) ;
106
+
107
+ case "object" :
108
+ return (
109
+ < Form . Control
110
+ as = "textarea"
111
+ value = { value as string }
112
+ onChange = { handleChange }
113
+ className = "p-3 w-50 grey-border long-text"
114
+ placeholder = "null"
115
+ />
116
+ ) ;
117
+
118
+ default :
119
+ return null ;
120
+ }
121
+ }
13
122
14
123
const JEXLDebuggerPage : FC = ( ) => {
15
- const [ clientContext , setClientContext ] = useState ( { } ) ;
124
+ const [ originalContext , setOriginalContext ] = useState <
125
+ Record < string , ContextValue >
126
+ > ( { } ) ;
127
+ const [ modifiedContext , setModifiedContext ] = useState <
128
+ Record < string , ContextValue >
129
+ > ( { } ) ;
130
+ const [ formData , setFormData ] = useState < Record < string , FormDataValue > > ( { } ) ;
16
131
const [ jexlExpression , setJexlExpression ] = useState ( "" ) ;
17
132
const [ output , setOutput ] = useState ( "" ) ;
18
133
const { addToast } = useToastsContext ( ) ;
19
134
20
135
const fetchClientContext = useCallback ( async ( ) => {
21
136
try {
22
- const context = await browser . experiments . nimbus . getClientContext ( ) ;
23
- setClientContext ( context ) ;
137
+ const context =
138
+ ( await browser . experiments . nimbus . getClientContext ( ) ) as Record <
139
+ string ,
140
+ ContextValue
141
+ > ;
142
+ setOriginalContext ( context ) ;
143
+ setModifiedContext ( { } ) ;
144
+ setFormData (
145
+ Object . fromEntries (
146
+ Object . entries ( context ) . map ( ( [ key , value ] ) => {
147
+ let formValue : FormDataValue ;
148
+ const fieldType = getFieldType ( value ) ;
149
+
150
+ switch ( fieldType ) {
151
+ case "string" :
152
+ formValue = value as string ;
153
+ break ;
154
+ case "boolean" :
155
+ formValue = value as boolean ;
156
+ break ;
157
+ case "number" :
158
+ formValue = ( value as number ) . toString ( ) ;
159
+ break ;
160
+ case "Date" :
161
+ formValue = ( value as Date ) . toISOString ( ) . slice ( 0 , 19 ) ;
162
+ break ;
163
+ case "object" :
164
+ formValue = JSON . stringify ( value , null , 2 ) ;
165
+ break ;
166
+ }
167
+ return [ key , formValue ] ;
168
+ } ) ,
169
+ ) ,
170
+ ) ;
24
171
} catch ( error ) {
25
172
addToast ( {
26
173
message : `Error fetching client context: ${ ( error as Error ) . message ?? String ( error ) } ` ,
@@ -45,7 +192,10 @@ const JEXLDebuggerPage: FC = () => {
45
192
if ( jexlExpression === "" ) {
46
193
setOutput ( "Error evaluating expression" ) ;
47
194
} else {
48
- const result = await evaluateJexl ( jexlExpression , clientContext ) ;
195
+ const result = await evaluateJexl ( jexlExpression , {
196
+ ...originalContext ,
197
+ ...modifiedContext ,
198
+ } ) ;
49
199
setOutput ( result ) ;
50
200
}
51
201
} catch ( error ) {
@@ -55,11 +205,78 @@ const JEXLDebuggerPage: FC = () => {
55
205
variant : "danger" ,
56
206
} ) ;
57
207
}
58
- } , [ jexlExpression , clientContext , addToast ] ) ;
208
+ } , [ jexlExpression , modifiedContext , originalContext , addToast ] ) ;
59
209
60
- const memoizedClientContextEntries = useMemo (
61
- ( ) => Object . entries ( clientContext ) ,
62
- [ clientContext ] ,
210
+ const parseAndSetContext = useMemo ( ( ) => {
211
+ return debounce ( ( key : string , value : string , isNumber : boolean ) => {
212
+ if ( isNumber ) {
213
+ const numVal = parseInt ( value ) ;
214
+ if ( ! isNaN ( numVal ) ) {
215
+ setModifiedContext ( ( prevContext ) => ( {
216
+ ...prevContext ,
217
+ [ key ] : numVal ,
218
+ } ) ) ;
219
+ } else {
220
+ addToast ( {
221
+ message : "Error changing context: Value entered must be a number" ,
222
+ variant : "danger" ,
223
+ } ) ;
224
+ }
225
+ } else {
226
+ try {
227
+ const parsedValue = JSON . parse ( value ) as ContextValue ;
228
+ setModifiedContext ( ( prevContext ) => ( {
229
+ ...prevContext ,
230
+ [ key ] : parsedValue ,
231
+ } ) ) ;
232
+ } catch ( error ) {
233
+ addToast ( {
234
+ message : `Error changing context: ${ ( error as Error ) . message ?? String ( error ) } ` ,
235
+ variant : "danger" ,
236
+ } ) ;
237
+ }
238
+ }
239
+ } , 1000 ) ;
240
+ } , [ addToast , setModifiedContext ] ) ;
241
+
242
+ const handleContextChange = useCallback < OnChangeFn > (
243
+ ( key : string , value : FormDataValue , fieldType : FieldType ) => {
244
+ switch ( fieldType ) {
245
+ case "object" :
246
+ parseAndSetContext ( key , String ( value ) , false ) ;
247
+ break ;
248
+
249
+ case "number" :
250
+ parseAndSetContext ( key , String ( value ) , true ) ;
251
+ break ;
252
+
253
+ case "boolean" :
254
+ setModifiedContext ( ( prevContext ) => ( {
255
+ ...prevContext ,
256
+ [ key ] : value ,
257
+ } ) ) ;
258
+ break ;
259
+
260
+ case "Date" :
261
+ setModifiedContext ( ( prevContext ) => ( {
262
+ ...prevContext ,
263
+ [ key ] : new Date ( value as string ) ,
264
+ } ) ) ;
265
+ break ;
266
+
267
+ case "string" :
268
+ setModifiedContext ( ( prevContext ) => ( {
269
+ ...prevContext ,
270
+ [ key ] : value ,
271
+ } ) ) ;
272
+ break ;
273
+ }
274
+ setFormData ( ( prevContext ) => ( {
275
+ ...prevContext ,
276
+ [ key ] : value ,
277
+ } ) ) ;
278
+ } ,
279
+ [ setModifiedContext , setFormData , parseAndSetContext ] ,
63
280
) ;
64
281
65
282
return (
@@ -100,27 +317,18 @@ const JEXLDebuggerPage: FC = () => {
100
317
>
101
318
Refresh Context
102
319
</ Button >
103
- { memoizedClientContextEntries . map ( ( [ key , value ] ) => (
320
+ { Object . entries ( originalContext ) . map ( ( [ key ] ) => (
104
321
< Row key = { key } className = "mb-4 d-flex align-items-center" >
105
322
< Col xs = { 3 } className = "secondary-fg fw-bold" >
106
323
{ key }
107
324
</ Col >
108
325
< Col xs = { 9 } >
109
- { [ "number" , "string" , "boolean" ] . includes ( typeof value ) ? (
110
- < Form . Control
111
- type = "text"
112
- readOnly
113
- value = { String ( value ) }
114
- className = "p-3 w-50 grey-border short-text"
115
- />
116
- ) : (
117
- < Form . Control
118
- as = "textarea"
119
- readOnly
120
- value = { JSON . stringify ( value , null , 2 ) }
121
- className = "p-3 w-50 grey-border long-text"
122
- />
123
- ) }
326
+ < ContextField
327
+ contextKey = { key }
328
+ value = { formData [ key ] }
329
+ onChange = { handleContextChange }
330
+ fieldType = { getFieldType ( originalContext [ key ] ) }
331
+ />
124
332
</ Col >
125
333
</ Row >
126
334
) ) }
0 commit comments